f
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions

This commit is contained in:
Dmitry 2025-12-14 02:35:14 +03:00
parent 9c132174c5
commit 1ab5071345
61 changed files with 2701 additions and 2073 deletions

View file

@ -53,4 +53,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)
Button.displayName = "Button"
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }

View file

@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"

View file

@ -1,5 +1,6 @@
import { adminApiClient } from './client'
import type { AuthResponse, RequestCodeResponse, CodeStatusResponse } from '@/types/models'
import type { AxiosError } from 'axios'
export const authApi = {
// Request authentication code (creates web code)
@ -13,9 +14,10 @@ export const authApi = {
try {
const response = await adminApiClient.get(`/api/v2/admin/auth/code-status/${code}`)
return response.data
} catch (error: any) {
} catch (error) {
const axiosError = error as AxiosError
// If 404, return a proper error response
if (error.response?.status === 404) {
if (axiosError.response?.status === 404) {
return {
success: false,
code: code,
@ -40,7 +42,7 @@ export const authApi = {
},
// Get current admin info (for token validation)
getCurrentUser: async (): Promise<{ success: boolean; user?: any; message?: string }> => {
getCurrentUser: async (): Promise<{ success: boolean; user?: unknown; message?: string }> => {
const response = await adminApiClient.get('/api/v2/admin/auth/me')
return response.data
},

View file

@ -44,8 +44,8 @@ describe('API Clients', () => {
const token = 'test-admin-token'
localStorage.setItem('admin_token', token)
const requestConfig = { headers: {} }
const addAuthToken = (config: any) => {
const requestConfig = { headers: {} as Record<string, string> }
const addAuthToken = (config: { headers: Record<string, string> }) => {
const storedToken = localStorage.getItem('admin_token')
if (storedToken) {
config.headers.Authorization = `Bearer ${storedToken}`
@ -58,8 +58,8 @@ describe('API Clients', () => {
})
it('should not add authorization header when no token exists', () => {
const requestConfig = { headers: {} }
const addAuthToken = (config: any) => {
const requestConfig = { headers: {} as Record<string, string> }
const addAuthToken = (config: { headers: Record<string, string> }) => {
const storedToken = localStorage.getItem('admin_token')
if (storedToken) {
config.headers.Authorization = `Bearer ${storedToken}`
@ -81,7 +81,7 @@ describe('API Clients', () => {
writable: true,
})
const handleAuthError = (error: any) => {
const handleAuthError = (error: { response?: { status?: number } }) => {
if (error.response?.status === 401) {
localStorage.removeItem('admin_token')
window.location.href = '/login'
@ -108,7 +108,7 @@ describe('API Clients', () => {
writable: true,
})
const handleAuthError = (error: any) => {
const handleAuthError = (error: { response?: { status?: number } }) => {
if (error.response?.status === 401) {
localStorage.removeItem('admin_token')
window.location.href = '/login'

View file

@ -1,4 +1,4 @@
import axios from 'axios'
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'
import { useAuthStore } from '@/stores/authStore'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.mnemo-cards.online'
@ -23,7 +23,7 @@ export const adminApiClient = axios.create({
})
// Request interceptor to add auth token
const addAuthToken = (config: any) => {
const addAuthToken = (config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('admin_token')
const isPublicAuthEndpoint =
config.url?.includes('/admin/auth/request-code') ||
@ -33,7 +33,7 @@ const addAuthToken = (config: any) => {
if (token && token.trim()) {
// Ensure headers object exists
if (!config.headers) {
config.headers = {}
config.headers = {} as InternalAxiosRequestConfig['headers']
}
const trimmedToken = token.trim()
// Set Authorization header - axios will normalize it to lowercase for HTTP
@ -61,14 +61,15 @@ const addAuthToken = (config: any) => {
let isLoggingOut = false
// Response interceptor for error handling
const handleAuthError = (error: any) => {
if (error.response?.status === 401) {
const handleAuthError = (error: unknown) => {
const axiosError = error as AxiosError
if (axiosError.response?.status === 401) {
// Check if we're already on login page
const currentPath = window.location.pathname
const isPublicAuthEndpoint =
error.config?.url?.includes('/admin/auth/request-code') ||
error.config?.url?.includes('/admin/auth/verify-code') ||
error.config?.url?.includes('/admin/auth/code-status')
axiosError.config?.url?.includes('/admin/auth/request-code') ||
axiosError.config?.url?.includes('/admin/auth/verify-code') ||
axiosError.config?.url?.includes('/admin/auth/code-status')
// Don't logout for public auth endpoints or if already on login page
if (currentPath === '/login' || isPublicAuthEndpoint) {

View file

@ -33,4 +33,5 @@ function Badge({ className, variant, ...props }: BadgeProps) {
)
}
// eslint-disable-next-line react-refresh/only-export-components
export { Badge, badgeVariants }

View file

@ -55,4 +55,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)
Button.displayName = "Button"
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }

View file

@ -2,8 +2,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {

View file

@ -2,8 +2,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {

View file

@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { cardsApi } from '@/api/cards'
import type { GameCardDto, PaginatedResponse } from '@/types/models'
import type { AxiosError } from 'axios'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@ -71,8 +72,9 @@ export default function CardsPage() {
toast.success('Card created successfully')
closeDialog()
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to create card')
onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }>
toast.error(axiosError.response?.data?.message || 'Failed to create card')
},
})
@ -83,8 +85,9 @@ export default function CardsPage() {
toast.success('Card updated successfully')
closeDialog()
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to update card')
onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }>
toast.error(axiosError.response?.data?.message || 'Failed to update card')
},
})
@ -96,8 +99,9 @@ export default function CardsPage() {
setIsDeleteDialogOpen(false)
setCardToDelete(null)
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to delete card')
onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }>
toast.error(axiosError.response?.data?.message || 'Failed to delete card')
},
})

View file

@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { packsApi } from '@/api/packs'
import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse } from '@/types/models'
import type { AxiosError } from 'axios'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@ -77,8 +78,9 @@ export default function PacksPage() {
toast.success('Pack created successfully')
closeDialog()
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to create pack')
onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }>
toast.error(axiosError.response?.data?.message || 'Failed to create pack')
},
})
@ -89,8 +91,9 @@ export default function PacksPage() {
toast.success('Pack updated successfully')
closeDialog()
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to update pack')
onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }>
toast.error(axiosError.response?.data?.message || 'Failed to update pack')
},
})
@ -102,8 +105,9 @@ export default function PacksPage() {
setIsDeleteDialogOpen(false)
setPackToDelete(null)
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to delete pack')
onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }>
toast.error(axiosError.response?.data?.message || 'Failed to delete pack')
},
})
@ -141,7 +145,7 @@ export default function PacksPage() {
order: fullPack.order || 0,
})
setIsDialogOpen(true)
} catch (error) {
} catch {
toast.error('Failed to load pack details')
}
}

View file

@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { usersApi } from '@/api/users'
import type { UserDto, PaginatedResponse, PaymentDto } from '@/types/models'
import type { AxiosError } from 'axios'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@ -71,8 +72,9 @@ export default function UsersPage() {
toast.success('User updated successfully')
closeEditDialog()
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to update user')
onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }>
toast.error(axiosError.response?.data?.message || 'Failed to update user')
},
})
@ -84,8 +86,9 @@ export default function UsersPage() {
setIsDeleteDialogOpen(false)
setUserToDelete(null)
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to delete user')
onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }>
toast.error(axiosError.response?.data?.message || 'Failed to delete user')
},
})
@ -112,7 +115,7 @@ export default function UsersPage() {
setUserPurchases(purchases)
setSelectedUser(user)
setIsPurchasesDialogOpen(true)
} catch (error) {
} catch {
toast.error('Failed to load user purchases')
}
}

View file

@ -55,8 +55,8 @@ export interface UserDto {
purchases: string[]
subscription?: boolean
subscriptionFeatures: string[]
userDataDto?: any
userSettingsDto?: any
userDataDto?: unknown
userSettingsDto?: unknown
}
export interface PaymentDto {

View file

@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:drift/drift.dart' as drift;
@ -32,7 +33,7 @@ extension PaymentToDto on Payment {
),
externalToken: externalToken,
meta: meta,
date: date,
date: date.dateTime,
products: productsList,
);
}
@ -52,7 +53,7 @@ extension PaymentFromDto on PaymentDto {
paymentSystem: paymentSystem.name,
externalToken: drift.Value(externalToken),
meta: drift.Value(meta),
date: drift.Value(date),
date: drift.Value(PgDateTime(date)),
products: drift.Value(productsJson),
);
}
@ -65,7 +66,7 @@ extension PaymentUpdate on PaymentDto {
status: drift.Value(status.name),
externalToken: drift.Value(externalToken),
meta: drift.Value(meta),
updatedAt: drift.Value(DateTime.now()),
updatedAt: drift.Value(PgDateTime(DateTime.now())),
);
}
}

View file

@ -1,4 +1,5 @@
import 'dart:developer';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/purchase/google_play_purchase_handler.dart';
import 'package:mnemo_cards_backend/database/database.dart';
@ -69,8 +70,8 @@ class PaymentManager {
await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert(
userId: userId,
start: now,
finish: endDate,
start: PgDateTime(now),
finish: PgDateTime(endDate),
features: drift.Value(plan.features as List<dynamic>),
),
);
@ -149,8 +150,8 @@ class PaymentManager {
await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert(
userId: payment.userId,
start: now,
finish: endDate,
start: PgDateTime(now),
finish: PgDateTime(endDate),
features: drift.Value(plan.features),
),
);

View file

@ -1,3 +1,4 @@
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
@ -25,8 +26,8 @@ class SubscriptionManager {
return SubscriptionDto(
page: null,
isActive: subscription != null,
start: subscription?.start,
finish: subscription?.finish,
start: subscription?.start.dateTime,
finish: subscription?.finish.dateTime,
);
}
@ -41,8 +42,8 @@ class SubscriptionManager {
await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert(
userId: user.id!,
start: now,
finish: endDate,
start: PgDateTime(now),
finish: PgDateTime(endDate),
features: const Value([]),
),
);
@ -86,8 +87,8 @@ class SubscriptionManager {
await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert(
userId: userId,
start: now,
finish: endDate,
start: PgDateTime(now),
finish: PgDateTime(endDate),
features: Value(plan.features as List<dynamic>),
),
);

View file

@ -71,7 +71,7 @@ class AdminAnalyticsApiV2 {
'id': u.id,
'name': u.name,
'email': u.email,
'createdAt': u.createdAt.toIso8601String(),
'createdAt': u.createdAt.dateTime.toIso8601String(),
})
.toList();

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:mnemo_cards_backend/database/database.dart';
@ -41,7 +42,7 @@ class AdminCardsApiV2 {
'image': card.image,
'back': card.back,
'transcription': card.transcription,
'createdAt': card.createdAt.toIso8601String(),
'createdAt': card.createdAt.dateTime.toIso8601String(),
}).toList(),
'total': total,
}),
@ -83,8 +84,8 @@ class AdminCardsApiV2 {
'image': card.image,
'back': card.back,
'transcription': card.transcription,
'createdAt': card.createdAt.toIso8601String(),
'updatedAt': card.updatedAt?.toIso8601String(),
'createdAt': card.createdAt.dateTime.toIso8601String(),
'updatedAt': card.updatedAt?.dateTime.toIso8601String(),
}),
headers: {'Content-Type': 'application/json'},
);
@ -155,7 +156,7 @@ class AdminCardsApiV2 {
image: data['image'] ?? existing.image,
back: data['back'] ?? existing.back,
transcription: data['transcription'] ?? existing.transcription,
updatedAt: DateTime.now(),
updatedAt: PgDateTime(DateTime.now()),
);
await _db.packDao.updateCard(updated);

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
@ -245,7 +246,7 @@ class JwtService {
RefreshTokensCompanion.insert(
jti: jti,
userId: userId,
expiresAt: expiresAt, // required field - raw DateTime
expiresAt: PgDateTime(expiresAt), // required field - raw DateTime
// createdAt and isBlacklisted use defaults from table
),
);
@ -260,7 +261,7 @@ class JwtService {
}
// Check if expired
if (token.expiresAt.isBefore(DateTime.now())) {
if (token.expiresAt.dateTime.isBefore(DateTime.now())) {
return true; // Expired tokens are considered invalid
}

View file

@ -4,7 +4,6 @@ import 'dart:developer';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/tasks/task_manager.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
@ -104,9 +103,9 @@ class TasksApiV2 {
'difficulty': t.difficulty,
'status': t.status,
'rewards': t.rewards,
'createdAt': t.createdAt.toIso8601String(),
'expiresAt': t.expiresAt.toIso8601String(),
'completedAt': t.completedAt?.toIso8601String(),
'createdAt': t.createdAt.dateTime.toIso8601String(),
'expiresAt': t.expiresAt.dateTime.toIso8601String(),
'completedAt': t.completedAt?.dateTime.toIso8601String(),
'proofUrl': t.proofUrl,
'instructions': t.instructions,
'tags': t.tags,
@ -146,9 +145,9 @@ class TasksApiV2 {
'difficulty': task.difficulty,
'status': task.status,
'rewards': task.rewards,
'createdAt': task.createdAt.toIso8601String(),
'expiresAt': task.expiresAt.toIso8601String(),
'completedAt': task.completedAt?.toIso8601String(),
'createdAt': task.createdAt.dateTime.toIso8601String(),
'expiresAt': task.expiresAt.dateTime.toIso8601String(),
'completedAt': task.completedAt?.dateTime.toIso8601String(),
'proofUrl': task.proofUrl,
'instructions': task.instructions,
'tags': task.tags,
@ -212,8 +211,8 @@ class TasksApiV2 {
'progress': progress.map((p) => {
'taskId': p.taskId,
'progress': p.progress,
'startedAt': p.startedAt.toIso8601String(),
'updatedAt': p.updatedAt.toIso8601String(),
'startedAt': p.startedAt.dateTime.toIso8601String(),
'updatedAt': p.updatedAt.dateTime.toIso8601String(),
}).toList()
});
} catch (e) {

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:math';
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_backend/main.dart' as backend_main;
@ -115,8 +116,8 @@ class TelegramBotApiV2 {
final sharesCount = await (_db.selectOnly(_db.shareRequests)
..addColumns([countExpr])
..where(_db.shareRequests.telegramUserId.equals(telegramUserId))
..where(_db.shareRequests.requestedAt.isBiggerOrEqualValue(todayStart))
..where(_db.shareRequests.requestedAt.isSmallerThanValue(todayEnd)))
..where(_db.shareRequests.requestedAt.isBiggerOrEqualValue(PgDateTime(todayStart)))
..where(_db.shareRequests.requestedAt.isSmallerThanValue(PgDateTime(todayEnd))))
.map((row) => row.read(countExpr)!)
.getSingle();
@ -155,7 +156,7 @@ class TelegramBotApiV2 {
telegramUserId: telegramUserId,
telegramUsername: Value(telegramUsername),
sharedCardId: Value(sharedCardId),
requestedAt: Value(requestedAt),
requestedAt: Value(PgDateTime(requestedAt)),
);
await _db.into(_db.shareRequests).insert(companion);

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:math';
import 'package:drift/drift.dart' as drift;
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart';
@ -117,7 +118,7 @@ class UsersApiV2 {
id: drift.Value(user.id!),
name: name != null ? drift.Value(name) : const drift.Value.absent(),
email: email != null ? drift.Value(email) : const drift.Value.absent(),
updatedAt: drift.Value(DateTime.now()),
updatedAt: drift.Value(PgDateTime(DateTime.now())),
),
);
@ -518,8 +519,8 @@ class UsersApiV2 {
totalStudyTimeMinutes: drift.Value(totalStudyTime),
currentStreak: drift.Value(currentStreak),
longestStreak: drift.Value(longestStreak),
lastTimeOnline: drift.Value(now),
updatedAt: drift.Value(now),
lastTimeOnline: drift.Value(PgDateTime(now)),
updatedAt: drift.Value(PgDateTime(now)),
),
);
});

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:mnemo_cards_backend/user/admin_ids_service.dart';
import '../database/database.dart';
@ -41,7 +42,7 @@ class CheckAdminsTask with task.Task {
UsersCompanion(
id: Value(user.id),
admin: const Value(true),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
),
);
}

View file

@ -1,6 +1,8 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:mnemo_cards_backend/cron/task.dart' as cron_task;
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
class TasksSeederTask with cron_task.Task {
@ -19,10 +21,13 @@ class TasksSeederTask with cron_task.Task {
final now = DateTime.now();
final weekFromNow = now.add(const Duration(days: 7));
final tasks = [
// App Internal Tasks
UserTaskModel(
title: 'Пройди 3 теста сегодня',
// Используем предопределенные UUID для каждой задачи
// Это позволяет избежать дубликатов при повторных запусках
final tasksWithIds = [
(
id: '00000000-0000-0000-0001-000000000001',
task: UserTaskModel(
title: 'Пройди 3 теста сегодня',
description:
'Заверши три тестовые сессии в приложении для улучшения навыков испанского.',
type: 'app_internal',
@ -36,9 +41,11 @@ class TasksSeederTask with cron_task.Task {
expiresAt: weekFromNow,
tags: ['tests', 'practice', 'daily'],
),
UserTaskModel(
title: 'Достигни 80% правильных ответов в тесте',
),
(
id: '00000000-0000-0000-0001-000000000002',
task: UserTaskModel(
title: 'Достигни 80% правильных ответов в тесте',
description:
'Пройди тест с результатом не менее 80% правильных ответов.',
type: 'app_internal',
@ -52,10 +59,12 @@ class TasksSeederTask with cron_task.Task {
expiresAt: weekFromNow,
tags: ['tests', 'accuracy', 'challenge'],
),
),
// External Tasks
UserTaskModel(
title: 'Подпишись на Telegram канал',
(
id: '00000000-0000-0000-0001-000000000003',
task: UserTaskModel(
title: 'Подпишись на Telegram канал',
description:
'Подпишись на наш Telegram канал @mnemo_cards для получения обновлений и советов по изучению испанского.',
type: 'external',
@ -69,9 +78,11 @@ class TasksSeederTask with cron_task.Task {
expiresAt: weekFromNow,
tags: ['telegram', 'social', 'subscription'],
),
UserTaskModel(
title: 'Закажи еду на испанском',
),
(
id: '00000000-0000-0000-0001-000000000004',
task: UserTaskModel(
title: 'Закажи еду на испанском',
description:
'Сделай заказ в ресторане или кафе, используя испанский язык. Запиши процесс на видео для подтверждения.',
type: 'external',
@ -91,9 +102,11 @@ class TasksSeederTask with cron_task.Task {
'Запиши видео, где ты делаешь заказ на испанском. Убедись, что официант понимает тебя.',
tags: ['speaking', 'restaurant', 'video', 'real_world'],
),
UserTaskModel(
title: 'Посмотри испанский фильм с субтитрами',
),
(
id: '00000000-0000-0000-0001-000000000005',
task: UserTaskModel(
title: 'Посмотри испанский фильм с субтитрами',
description:
'Посмотри испанский фильм или сериал с испанскими субтитрами в течение 30 минут.',
type: 'external',
@ -107,10 +120,12 @@ class TasksSeederTask with cron_task.Task {
expiresAt: weekFromNow,
tags: ['listening', 'media', 'subtitles'],
),
),
// Social Tasks
UserTaskModel(
title: 'Поделись прогрессом в соцсетях',
(
id: '00000000-0000-0000-0001-000000000006',
task: UserTaskModel(
title: 'Поделись прогрессом в соцсетях',
description:
'Поделись своим прогрессом в изучении испанского в социальных сетях с хэштегом #MnemoCards.',
type: 'social',
@ -124,9 +139,11 @@ class TasksSeederTask with cron_task.Task {
expiresAt: weekFromNow,
tags: ['social', 'sharing', 'progress'],
),
UserTaskModel(
title: 'Найди партнёра для практики',
),
(
id: '00000000-0000-0000-0001-000000000007',
task: UserTaskModel(
title: 'Найди партнёра для практики',
description:
'Найди собеседника для практики испанского языка (друга, коллегу или через языковые приложения).',
type: 'social',
@ -140,10 +157,12 @@ class TasksSeederTask with cron_task.Task {
expiresAt: weekFromNow.add(const Duration(days: 7)), // 2 weeks
tags: ['conversation', 'partner', 'practice'],
),
),
// Additional App Tasks
UserTaskModel(
title: 'Изучай слова ежедневно 7 дней подряд',
(
id: '00000000-0000-0000-0001-000000000008',
task: UserTaskModel(
title: 'Изучай слова ежедневно 7 дней подряд',
description:
'Учи новые слова в приложении каждый день в течение недели.',
type: 'app_internal',
@ -161,9 +180,11 @@ class TasksSeederTask with cron_task.Task {
expiresAt: weekFromNow.add(const Duration(days: 21)), // 4 weeks
tags: ['vocabulary', 'daily', 'streak', 'consistency'],
),
UserTaskModel(
title: 'Пройди тест по грамматике',
),
(
id: '00000000-0000-0000-0001-000000000009',
task: UserTaskModel(
title: 'Пройди тест по грамматике',
description:
'Заверши специализированный тест по грамматике испанского языка.',
type: 'app_internal',
@ -177,9 +198,11 @@ class TasksSeederTask with cron_task.Task {
expiresAt: weekFromNow,
tags: ['grammar', 'tests', 'knowledge'],
),
UserTaskModel(
title: 'Практикуй произношение',
),
(
id: '00000000-0000-0000-0001-000000000010',
task: UserTaskModel(
title: 'Практикуй произношение',
description:
'Заверши урок по произношению или запиши себя, читая текст на испанском.',
type: 'app_internal',
@ -193,24 +216,28 @@ class TasksSeederTask with cron_task.Task {
expiresAt: weekFromNow,
tags: ['pronunciation', 'speaking', 'practice'],
),
),
];
// Convert UserTaskModel to UserTasksCompanion and insert
// Используем InsertMode.insertOrIgnore для автоматического игнорирования дубликатов через ON CONFLICT
for (final taskModel in tasks) {
// PostgreSQL автоматически генерирует UUID через DEFAULT gen_random_uuid()
// Используем InsertMode.insertOrIgnore с предопределенными UUID для идемпотентности
for (final taskData in tasksWithIds) {
final taskModel = taskData.task;
final rewardsJson = taskModel.rewards.map((r) => r.toJson()).toList();
await _db.into(_db.userTasks).insert(
UserTasksCompanion.insert(
id: Value(taskData.id), // Предопределенный UUID для идемпотентности seeder'а
title: taskModel.title,
description: taskModel.description,
type: taskModel.type,
difficulty: taskModel.difficulty,
status: taskModel.status,
rewards: Value(rewardsJson),
createdAt: Value(taskModel.createdAt),
expiresAt: taskModel.expiresAt,
completedAt: Value(taskModel.completedAt),
createdAt: Value(PgDateTime(taskModel.createdAt)),
expiresAt: PgDateTime(taskModel.expiresAt),
completedAt: Value(taskModel.completedAt?.let(PgDateTime.new)),
proofUrl: Value(taskModel.proofUrl),
instructions: Value(taskModel.instructions),
tags: Value(taskModel.tags),
@ -220,7 +247,7 @@ class TasksSeederTask with cron_task.Task {
);
}
print('Successfully seeded ${tasks.length} user tasks (duplicates ignored)');
print('Successfully seeded ${tasksWithIds.length} user tasks (duplicates ignored)');
} catch (e) {
print('Error seeding tasks: $e');
rethrow;

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/achievements.dart';
@ -45,7 +46,7 @@ class AchievementDao extends DatabaseAccessor<AppDatabase> with _$AchievementDao
..where((ua) => ua.userId.equals(userId) & ua.achievementId.equals(achievementId))
).write(UserAchievementsCompanion(
progress: Value(progress),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
));
return count > 0;
}

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/discounts.dart';
import '../tables/users.dart';
@ -18,7 +19,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
/// Получить все активные кампании
Future<List<DiscountCampaign>> getActiveCampaigns() {
final now = DateTime.now();
final now = PgDateTime(DateTime.now());
return (select(db.discountCampaigns)
..where((c) => c.status.equals('active'))
..where((c) => c.start.isSmallerOrEqualValue(now))
@ -44,7 +45,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
..where((c) => c.id.equals(campaignId))
).write(DiscountCampaignsCompanion(
status: Value(status),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
));
}
@ -77,7 +78,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
..where((c) => c.id.equals(campaignId))
).write(DiscountCampaignsCompanion(
isDeleted: const Value(true),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
));
}
@ -171,7 +172,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
String? productType,
String? productId,
}) async {
final now = DateTime.now();
final now = PgDateTime(DateTime.now());
final query = select(db.discountCampaigns)
..where((c) => c.status.equals('active'))
..where((c) => c.start.isSmallerOrEqualValue(now))

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/packs.dart';
import '../tables/relations.dart';
@ -60,7 +61,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
if (packId == null) throw ArgumentError('Pack ID is required');
return (update(cardPacks)..where((p) => p.id.equals(packId)))
.write(updates.copyWith(updatedAt: Value(DateTime.now())));
.write(updates.copyWith(updatedAt: Value(PgDateTime(DateTime.now()))));
}
/// Удалить пак (soft delete)
@ -68,7 +69,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
return (update(cardPacks)..where((p) => p.id.equals(packId)))
.write(CardPacksCompanion(
isDeleted: const Value(true),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
));
}
@ -137,7 +138,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
return (update(gameCards)..where((c) => c.id.equals(cardId)))
.write(GameCardsCompanion(
isDeleted: const Value(true),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
));
}

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/payments.dart';
@ -56,7 +57,7 @@ class PaymentDao extends DatabaseAccessor<AppDatabase> with _$PaymentDaoMixin {
'SELECT id FROM payments WHERE user_id = ? AND date = ? AND amount = ? ORDER BY created_at DESC LIMIT 1',
variables: [
Variable.withString(payment.userId.value),
Variable.withDateTime(inserted.date),
Variable.withDateTime(inserted.date.dateTime),
Variable.withString(inserted.amount),
],
readsFrom: {payments},
@ -81,7 +82,7 @@ class PaymentDao extends DatabaseAccessor<AppDatabase> with _$PaymentDaoMixin {
return (update(payments)..where((p) => p.id.equals(paymentId)))
.write(PaymentsCompanion(
status: Value(status),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
));
}

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/promo_codes.dart';
import '../tables/users.dart';
@ -18,7 +19,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
/// Получить все активные кампании
Future<List<PromoCodesCampaign>> getActiveCampaigns() {
final now = DateTime.now();
final now = PgDateTime(DateTime.now());
return (select(db.promoCodesCampaigns)
..where((c) => c.status.equals('active'))
..where((c) => c.start.isSmallerOrEqualValue(now))
@ -44,7 +45,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
..where((c) => c.id.equals(campaignId))
).write(PromoCodesCampaignsCompanion(
status: Value(status),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
));
}
@ -109,7 +110,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
if (code != null) {
await update(db.promoCodes).replace(code.copyWith(
activations: code.activations + 1,
updatedAt: DateTime.now(),
updatedAt: PgDateTime(DateTime.now()),
));
}
}

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/statistics.dart';
import '../tables/users.dart';
@ -42,10 +43,10 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
..orderBy([(s) => OrderingTerm.desc(s.startTime)]);
if (fromDate != null) {
query.where((s) => s.startTime.isBiggerOrEqualValue(fromDate));
query.where((s) => s.startTime.isBiggerOrEqualValue(PgDateTime(fromDate)));
}
if (toDate != null) {
query.where((s) => s.startTime.isSmallerOrEqualValue(toDate));
query.where((s) => s.startTime.isSmallerOrEqualValue(PgDateTime(toDate)));
}
if (limit != null) {
@ -74,8 +75,8 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
}) {
final updates = StudySessionsCompanion(
id: Value(sessionId),
endTime: Value(DateTime.now()),
updatedAt: Value(DateTime.now()),
endTime: Value(PgDateTime(DateTime.now())),
updatedAt: Value(PgDateTime(DateTime.now())),
wordsLearned: wordsLearned != null ? Value(wordsLearned) : const Value.absent(),
testsCompleted: testsCompleted != null ? Value(testsCompleted) : const Value.absent(),
accuracy: accuracy != null ? Value(accuracy) : const Value.absent(),

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/subscriptions.dart';
import '../tables/users.dart';
@ -46,7 +47,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
/// Получить активную подписку пользователя
Future<UserSubscription?> getActiveSubscription(String userId) async {
final now = DateTime.now();
final now = PgDateTime(DateTime.now());
return (select(userSubscriptions)
..where((us) => us.userId.equals(userId))
..where((us) => us.start.isSmallerThanValue(now))
@ -81,7 +82,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
/// Получить всех пользователей с активными подписками
Future<List<UserSubscription>> getActiveSubscriptions() async {
final now = DateTime.now();
final now = PgDateTime(DateTime.now());
return (select(userSubscriptions)
..where((us) => us.start.isSmallerThanValue(now))
..where((us) => us.finish.isBiggerThanValue(now))
@ -100,7 +101,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
if (subscription != null) {
await update(userSubscriptions).replace(
subscription.copyWith(
finish: DateTime.now(),
finish: PgDateTime(DateTime.now()),
),
);
}

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
part 'task_dao.g.dart';
@ -32,10 +33,11 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
/// Обновить время последнего выполнения
Future<void> updateLastExecution(String taskId) {
final now = PgDateTime(DateTime.now());
return (update(db.tasks)..where((t) => t.id.equals(taskId)))
.write(TasksCompanion(
lastExecution: Value(DateTime.now()),
updatedAt: Value(DateTime.now()),
lastExecution: Value(now),
updatedAt: Value(now),
));
}
@ -66,7 +68,7 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
}
if (activeOnly) {
final now = DateTime.now();
final now = PgDateTime(DateTime.now());
query.where((ut) => ut.expiresAt.isBiggerThanValue(now));
}
@ -88,11 +90,12 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
/// Завершить задачу пользователя
Future<void> completeUserTask(String taskId) {
final now = PgDateTime(DateTime.now());
return (update(db.userTasks)..where((ut) => ut.id.equals(taskId)))
.write(UserTasksCompanion(
status: const Value('completed'),
completedAt: Value(DateTime.now()),
updatedAt: Value(DateTime.now()),
completedAt: Value(now),
updatedAt: Value(now),
));
}

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/tests.dart';
import '../tables/packs.dart';
@ -52,7 +53,7 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
return (update(tests)..where((t) => t.id.equals(testId)))
.write(TestsCompanion(
isDeleted: const Value(true),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
));
}

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/users.dart';
import '../tables/auth.dart';
@ -82,7 +83,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
return (update(users)..where((u) => u.id.equals(userId)))
.write(UsersCompanion(
isDeleted: const Value(true),
updatedAt: Value(DateTime.now()),
updatedAt: Value(PgDateTime(DateTime.now())),
));
}
@ -115,7 +116,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
/// Получить пользователей с активной подпиской
Stream<List<User>> watchUsersWithActiveSubscription() {
final now = DateTime.now();
final now = PgDateTime(DateTime.now());
final query = select(users).join([
innerJoin(
db.userSubscriptions,
@ -155,15 +156,16 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
if (userId == null) throw ArgumentError('User ID is required');
return (update(userDatas)..where((ud) => ud.userId.equals(userId)))
.write(updates.copyWith(updatedAt: Value(DateTime.now())));
.write(updates.copyWith(updatedAt: Value(PgDateTime(DateTime.now()))));
}
/// Обновить время последнего визита
Future<void> updateLastOnline(String userId) async {
final now = PgDateTime(DateTime.now());
await (update(userDatas)..where((ud) => ud.userId.equals(userId)))
.write(UserDatasCompanion(
lastTimeOnline: Value(DateTime.now()),
updatedAt: Value(DateTime.now()),
lastTimeOnline: Value(now),
updatedAt: Value(now),
));
}
@ -188,7 +190,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
Future<Token?> getTokenByUserId(String userId) {
return (select(tokens)
..where((t) => t.userId.equals(userId))
..where((t) => t.expires.isBiggerThanValue(DateTime.now()))
..where((t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())))
..orderBy([(t) => OrderingTerm.desc(t.created)])
).getSingleOrNull();
}
@ -212,7 +214,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
/// Удалить истекшие токены
Future<int> deleteExpiredTokens() {
return (delete(tokens)
..where((t) => t.expires.isSmallerThanValue(DateTime.now()))
..where((t) => t.expires.isSmallerThanValue(PgDateTime(DateTime.now())))
).go();
}
@ -229,7 +231,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
return (select(refreshTokens)
..where((rt) => rt.userId.equals(userId))
..where((rt) => rt.isBlacklisted.equals(false))
..where((rt) => rt.expiresAt.isBiggerThanValue(DateTime.now()))
..where((rt) => rt.expiresAt.isBiggerThanValue(PgDateTime(DateTime.now())))
..orderBy([(rt) => OrderingTerm.desc(rt.createdAt)])
).get();
}
@ -259,7 +261,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
/// Удалить истекшие refresh токены
Future<int> deleteExpiredRefreshTokens() {
return (delete(refreshTokens)
..where((rt) => rt.expiresAt.isSmallerThanValue(DateTime.now()))
..where((rt) => rt.expiresAt.isSmallerThanValue(PgDateTime(DateTime.now())))
).go();
}
@ -282,14 +284,14 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
return (update(db.telegramAuthCodes)..where((ac) => ac.code.equals(code)))
.write(TelegramAuthCodesCompanion(
isUsed: const Value(true),
usedAt: Value(DateTime.now()),
usedAt: Value(PgDateTime(DateTime.now())),
));
}
/// Удалить истекшие коды
Future<int> deleteExpiredAuthCodes() {
return (delete(db.telegramAuthCodes)
..where((ac) => ac.expiresAt.isSmallerThanValue(DateTime.now()))
..where((ac) => ac.expiresAt.isSmallerThanValue(PgDateTime(DateTime.now())))
).go();
}

View file

@ -32,7 +32,6 @@ import 'daos/promo_code_dao.dart';
import 'daos/discount_dao.dart';
import 'daos/statistics_dao.dart';
import 'daos/achievement_dao.dart';
import 'postgres_constants.dart';
// Сгенерированный код будет здесь
part 'database.g.dart';

File diff suppressed because it is too large Load diff

View file

@ -1,6 +0,0 @@
import 'package:drift/drift.dart';
/// PostgreSQL-совместимое выражение для текущей даты и времени как Unix timestamp (bigint)
/// Используется вместо currentDateAndTime, который использует SQLite-специфичную функцию strftime
/// Drift хранит DateTime как bigint (Unix timestamp в секундах), поэтому используем EXTRACT(EPOCH FROM NOW())::BIGINT
const Expression<DateTime> currentTimestamp = CustomExpression<DateTime>('EXTRACT(EPOCH FROM NOW())::BIGINT');

View file

@ -1,25 +1,31 @@
import 'package:drift/drift.dart';
import '../postgres_constants.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'users.dart';
/// Таблица UserAchievements - достижения пользователей
class UserAchievements extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get userId => text()
.references(Users, #id, onDelete: KeyAction.cascade)();
// Achievement ID (from AchievementDefinitions)
TextColumn get achievementId => text()();
// When achievement was unlocked
DateTimeColumn get unlockedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get unlockedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
// Progress (0.0 to 1.0)
RealColumn get progress => real().withDefault(const Constant(1.0))();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,18 +1,22 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'users.dart';
import '../converters.dart' show generateUuid;
import '../postgres_constants.dart';
/// Таблица Tokens - токены авторизации пользователей
class Tokens extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text()
.references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get token => text().unique()();
TextColumn get externalUserId => text()();
DateTimeColumn get created => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get expires => dateTime()();
Column<PgDateTime> get created => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get expires => customType(PgTypes.timestampWithTimezone)();
@override
Set<Column> get primaryKey => {id};
@ -25,17 +29,21 @@ class Tokens extends Table {
/// Таблица RefreshTokens - refresh токены для JWT
class RefreshTokens extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text()
.references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get jti => text().unique()(); // JWT ID
BoolColumn get isBlacklisted => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get expiresAt => dateTime()();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get expiresAt => customType(PgTypes.timestampWithTimezone)();
@override
Set<Column> get primaryKey => {id};
@ -43,7 +51,9 @@ class RefreshTokens extends Table {
/// Таблица TelegramAuthCodes - коды для авторизации через Telegram
class TelegramAuthCodes extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get code => text().unique()();
TextColumn get telegramUserId => text()();
@ -53,11 +63,13 @@ class TelegramAuthCodes extends Table {
BoolColumn get isUsed => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
DateTimeColumn get usedAt => dateTime().nullable()();
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
Column<PgDateTime> get usedAt => customType(PgTypes.timestampWithTimezone)
.nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get expiresAt => dateTime()();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get expiresAt => customType(PgTypes.timestampWithTimezone)();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,16 +1,18 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart' show generateUuid, StringListConverter, JsonListConverter;
import '../postgres_constants.dart';
import 'users.dart';
/// Таблица DiscountCampaigns - кампании скидок
class DiscountCampaigns extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get name => text().nullable()();
DateTimeColumn get start => dateTime()();
DateTimeColumn get finish => dateTime()();
Column<PgDateTime> get start => customType(PgTypes.timestampWithTimezone)();
Column<PgDateTime> get finish => customType(PgTypes.timestampWithTimezone)();
// Статус (enum as string)
TextColumn get status => text()(); // created, active, expired, disabled
@ -21,11 +23,13 @@ class DiscountCampaigns extends Table {
.map(const StringListConverter())();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
BoolColumn get isDeleted => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
@override
Set<Column> get primaryKey => {id};
@ -33,8 +37,11 @@ class DiscountCampaigns extends Table {
/// Таблица Discounts - скидки
class Discounts extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get campaignId => text().references(DiscountCampaigns, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get campaignId => text()
.references(DiscountCampaigns, #id, onDelete: KeyAction.cascade)();
// Процент скидки (0-100)
RealColumn get discountPercent => real()();
@ -45,11 +52,13 @@ class Discounts extends Table {
.map(const JsonListConverter())();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
BoolColumn get isDeleted => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
@override
Set<Column> get primaryKey => {id};
@ -57,10 +66,13 @@ class Discounts extends Table {
/// Junction table для связи Discounts UserDatas (many-to-many)
class DiscountUserDatas extends Table {
TextColumn get discountId => text().references(Discounts, #id, onDelete: KeyAction.cascade)();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get discountId => text()
.references(Discounts, #id, onDelete: KeyAction.cascade)();
TextColumn get userId => text()
.references(Users, #id, onDelete: KeyAction.cascade)();
DateTimeColumn get grantedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get grantedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {discountId, userId};

View file

@ -1,10 +1,12 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart' show generateUuid, IntListConverter, StringListConverter;
import '../postgres_constants.dart';
/// Таблица CardPacks - наборы карточек
class CardPacks extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
// Основная информация
TextColumn get title => text()();
@ -21,7 +23,7 @@ class CardPacks extends Table {
IntColumn get order => integer().withDefault(const Constant(0))();
BoolColumn get enabled => boolean()
.withDefault(const Constant(true))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
// Порядок карточек (JSON array of IDs)
TextColumn get cardsOrder => text()
@ -38,11 +40,13 @@ class CardPacks extends Table {
TextColumn get currency => text().withDefault(const Constant('RUB'))();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
BoolColumn get isDeleted => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
@override
Set<Column> get primaryKey => {id};
@ -50,8 +54,11 @@ class CardPacks extends Table {
/// Таблица GameCards - карточки для изучения
class GameCards extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get packId => text()
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
// Основной контент
TextColumn get original => text()(); // слово на иностранном языке
@ -70,11 +77,13 @@ class GameCards extends Table {
TextColumn get back => text().nullable()(); // дополнительный текст
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
BoolColumn get isDeleted => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
@override
Set<Column> get primaryKey => {id};
@ -82,13 +91,17 @@ class GameCards extends Table {
/// Таблица VoiceModels - голосовые файлы для карточек
class VoiceModels extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get cardId => text()
.references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get voiceUrl => text()(); // URL аудиофайла
TextColumn get language => text()(); // язык озвучки
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,12 +1,15 @@
import 'package:drift/drift.dart';
import '../converters.dart' show generateUuid, JsonListConverter, StringListConverter;
import '../postgres_constants.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart' show JsonListConverter, StringListConverter;
import 'users.dart';
/// Таблица Payments - платежи
class Payments extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text()
.references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get amount => text()();
TextColumn get currency => text()();
@ -18,7 +21,8 @@ class Payments extends Table {
TextColumn get externalToken => text().nullable()();
TextColumn get meta => text().nullable()();
DateTimeColumn get date => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get date => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
// Продукты (JSON array)
TextColumn get products => text()
@ -31,11 +35,13 @@ class Payments extends Table {
.map(const StringListConverter())();
BoolColumn get subscription => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,11 +1,13 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart' show generateUuid, JsonListConverter, StringListConverter;
import '../postgres_constants.dart';
import 'users.dart';
/// Таблица PromoCodesCampaigns - кампании промокодов
class PromoCodesCampaigns extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get template => text()();
TextColumn get name => text().nullable()();
@ -19,8 +21,8 @@ class PromoCodesCampaigns extends Table {
IntColumn get activationsPerUser => integer()();
IntColumn get generationSize => integer()();
DateTimeColumn get start => dateTime()();
DateTimeColumn get finish => dateTime()();
Column<PgDateTime> get start => customType(PgTypes.timestampWithTimezone)();
Column<PgDateTime> get finish => customType(PgTypes.timestampWithTimezone)();
// Статус (enum as string)
TextColumn get status => text()(); // created, preparing, ready, active, disabled
@ -31,11 +33,13 @@ class PromoCodesCampaigns extends Table {
.map(const StringListConverter())();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
BoolColumn get isDeleted => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
@override
Set<Column> get primaryKey => {id};
@ -43,18 +47,25 @@ class PromoCodesCampaigns extends Table {
/// Таблица PromoCodes - промокоды
class PromoCodes extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get campaignId => text().references(PromoCodesCampaigns, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get campaignId => text()
.references(PromoCodesCampaigns, #id, onDelete: KeyAction.cascade)();
TextColumn get code => text().unique()();
IntColumn get activations => integer().withDefault(const Constant(0))();
// Индивидуальный промокод (связан с пользователем)
TextColumn get userId => text().nullable().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get userId => text()
.nullable()
.references(Users, #id, onDelete: KeyAction.cascade)();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,16 +1,19 @@
import 'package:drift/drift.dart';
import '../postgres_constants.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'users.dart';
import 'packs.dart' show CardPacks, GameCards, VoiceModels;
/// Junction table для связи Users CardPacks (многие ко многим)
/// Хранит информацию о том, какие паки куплены пользователем
class UserPacks extends Table {
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get userId => text()
.references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text()
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
// Когда пользователь получил доступ к паку
DateTimeColumn get grantedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get grantedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
// Как пользователь получил пак (purchase, promo, free, admin)
TextColumn get grantType => text().withDefault(const Constant('purchase'))();
@ -22,8 +25,10 @@ class UserPacks extends Table {
/// Таблица PreviewCards - связь CardPacks с preview карточками
/// Хранит какие карточки показывать в превью пака
class PreviewCards extends Table {
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text()
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get cardId => text()
.references(GameCards, #id, onDelete: KeyAction.cascade)();
IntColumn get order => integer().withDefault(const Constant(0))();
@ -34,8 +39,10 @@ class PreviewCards extends Table {
/// Таблица CardPackCards - связь CardPacks с GameCards (many-to-many)
/// Хранит какие карточки принадлежат какому паку
class CardPackCards extends Table {
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text()
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get cardId => text()
.references(GameCards, #id, onDelete: KeyAction.cascade)();
IntColumn get order => integer().withDefault(const Constant(0))();
@ -46,8 +53,10 @@ class CardPackCards extends Table {
/// Таблица CardVoices - связь GameCards с VoiceModels (many-to-many)
/// Хранит какие голосовые файлы принадлежат какой карточке
class CardVoices extends Table {
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get voiceId => text().references(VoiceModels, #id, onDelete: KeyAction.cascade)();
TextColumn get cardId => text()
.references(GameCards, #id, onDelete: KeyAction.cascade)();
TextColumn get voiceId => text()
.references(VoiceModels, #id, onDelete: KeyAction.cascade)();
@override
Set<Column> get primaryKey => {cardId, voiceId};

View file

@ -1,17 +1,21 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'users.dart';
import '../converters.dart' show generateUuid;
import '../postgres_constants.dart';
/// Таблица StudySessions - сессии изучения
class StudySessions extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text()
.references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get sessionId => text().nullable().unique()();
DateTimeColumn get startTime => dateTime()();
DateTimeColumn get endTime => dateTime().nullable()();
Column<PgDateTime> get startTime => customType(PgTypes.timestampWithTimezone)();
Column<PgDateTime> get endTime => customType(PgTypes.timestampWithTimezone)
.nullable()();
IntColumn get wordsLearned => integer().withDefault(const Constant(0))();
IntColumn get testsCompleted => integer().withDefault(const Constant(0))();
@ -21,8 +25,10 @@ class StudySessions extends Table {
TextColumn get testId => text().nullable()();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,12 +1,13 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart' show JsonMapConverter, JsonListConverter;
import '../postgres_constants.dart';
import 'users.dart';
/// Таблица SubscriptionPlans - планы подписки
class SubscriptionPlans extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
// UI информация (JSON)
TextColumn get ui => text().nullable().map(const JsonMapConverter())();
@ -26,11 +27,13 @@ class SubscriptionPlans extends Table {
TextColumn get paymentSystem => text()(); // enum as string
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
BoolColumn get isDeleted => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
@override
Set<Column> get primaryKey => {id};
@ -38,11 +41,15 @@ class SubscriptionPlans extends Table {
/// Таблица UserSubscriptions - подписки пользователей
class UserSubscriptions extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().unique().references(Users, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text()
.unique()
.references(Users, #id, onDelete: KeyAction.cascade)();
DateTimeColumn get start => dateTime()();
DateTimeColumn get finish => dateTime()();
Column<PgDateTime> get start => customType(PgTypes.timestampWithTimezone)();
Column<PgDateTime> get finish => customType(PgTypes.timestampWithTimezone)();
// Функции подписки (JSON array)
TextColumn get features => text()
@ -50,8 +57,10 @@ class UserSubscriptions extends Table {
.map(const JsonListConverter())();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,11 +1,13 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart' show generateUuid, JsonListConverter, JsonMapConverter, StringListConverter;
import '../postgres_constants.dart';
import 'users.dart';
/// Таблица Tasks - задачи системы
class Tasks extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get name => text()();
IntColumn get minCycleMillis => integer()();
@ -15,11 +17,14 @@ class Tasks extends Table {
TextColumn get status => text().nullable()();
TextColumn get description => text().nullable()();
DateTimeColumn get lastExecution => dateTime().nullable()();
Column<PgDateTime> get lastExecution => customType(PgTypes.timestampWithTimezone)
.nullable()();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};
@ -27,7 +32,9 @@ class Tasks extends Table {
/// Таблица UserTasks - задачи пользователей
class UserTasks extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get title => text()();
TextColumn get description => text()();
@ -40,9 +47,11 @@ class UserTasks extends Table {
.withDefault(const Constant('[]'))
.map(const JsonListConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get expiresAt => dateTime()();
DateTimeColumn get completedAt => dateTime().nullable()();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get expiresAt => customType(PgTypes.timestampWithTimezone)();
Column<PgDateTime> get completedAt => customType(PgTypes.timestampWithTimezone)
.nullable()();
TextColumn get proofUrl => text().nullable()();
TextColumn get instructions => text().nullable()();
@ -55,7 +64,8 @@ class UserTasks extends Table {
TextColumn get imageUrl => text().nullable()();
// Audit
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};
@ -63,8 +73,11 @@ class UserTasks extends Table {
/// Таблица UserTaskProgresses - прогресс выполнения задач пользователями
class UserTaskProgresses extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text()
.references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get taskId => text()(); // Reference to UserTasks, but not FK to avoid circular deps
// Прогресс (JSON)
@ -72,8 +85,10 @@ class UserTaskProgresses extends Table {
.withDefault(const Constant('{}'))
.map(const JsonMapConverter())();
DateTimeColumn get startedAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get startedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};
@ -81,8 +96,11 @@ class UserTaskProgresses extends Table {
/// Таблица UserTaskResults - результаты выполнения задач
class UserTaskResults extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text()
.references(Users, #id, onDelete: KeyAction.cascade)();
TextColumn get taskId => text()(); // Reference to UserTasks
// Результаты (JSON)
@ -90,10 +108,12 @@ class UserTaskResults extends Table {
.withDefault(const Constant('{}'))
.map(const JsonMapConverter())();
DateTimeColumn get completedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get completedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,21 +1,27 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart' show generateUuid;
import '../postgres_constants.dart';
/// Таблица ShareRequests - запросы на шаринг через Telegram
class ShareRequests extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get telegramUserId => text()();
TextColumn get telegramUsername => text().nullable()();
TextColumn get sharedCardId => text().nullable()();
TextColumn get sharedCardId => text()
.nullable()();
DateTimeColumn get requestedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get requestedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,11 +1,13 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart' show generateUuid, JsonMapConverter;
import '../postgres_constants.dart';
import 'packs.dart';
/// Таблица Tests - тесты
class Tests extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get name => text()();
TextColumn get color => text().nullable()();
@ -15,11 +17,13 @@ class Tests extends Table {
TextColumn get timeSubtitle => text().nullable()();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
BoolColumn get isDeleted => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
@override
Set<Column> get primaryKey => {id};
@ -27,16 +31,21 @@ class Tests extends Table {
/// Таблица TestQuestions - вопросы тестов
class TestQuestions extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get testId => text().references(Tests, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get testId => text()
.references(Tests, #id, onDelete: KeyAction.cascade)();
// Тип вопроса (enum as string)
TextColumn get questionType => text()();
TextColumn get body => text()();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};
@ -44,8 +53,10 @@ class TestQuestions extends Table {
/// Таблица TestPackRelations - связь Tests с CardPacks (many-to-many)
class TestPackRelations extends Table {
TextColumn get testId => text().references(Tests, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
TextColumn get testId => text()
.references(Tests, #id, onDelete: KeyAction.cascade)();
TextColumn get packId => text()
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
@override
Set<Column> get primaryKey => {testId, packId};
@ -53,20 +64,26 @@ class TestPackRelations extends Table {
/// Таблица TestStatistics - статистика прохождения тестов
class TestStatistics extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text()(); // Reference to Users, but not FK to avoid circular deps
TextColumn get testId => text().references(Tests, #id, onDelete: KeyAction.cascade)();
TextColumn get testId => text()
.references(Tests, #id, onDelete: KeyAction.cascade)();
// Результаты (JSON)
TextColumn get results => text()
.withDefault(const Constant('{}'))
.map(const JsonMapConverter())();
DateTimeColumn get completedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get completedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,16 +1,18 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart';
import '../postgres_constants.dart';
/// Таблица Users - основная информация о пользователях
class Users extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get externalUserId => text().unique()();
TextColumn get name => text().nullable()();
TextColumn get email => text().nullable()();
BoolColumn get admin => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
// UserSettings (JSON)
TextColumn get userSettings => text().nullable()();
@ -21,11 +23,13 @@ class Users extends Table {
.map(const StringListConverter())();
// Audit fields
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
BoolColumn get isDeleted => boolean()
.withDefault(const Constant(false))
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
@override
Set<Column> get primaryKey => {id};
@ -38,8 +42,12 @@ class Users extends Table {
/// Таблица UserDatas - расширенная информация о пользователе
class UserDatas extends Table {
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
TextColumn get userId => text().unique().references(Users, #id, onDelete: KeyAction.cascade)();
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
TextColumn get id => text()
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
TextColumn get userId => text()
.unique()
.references(Users, #id, onDelete: KeyAction.cascade)();
// Статистика
IntColumn get totalStudyTimeMinutes => integer().withDefault(const Constant(0))();
@ -49,8 +57,10 @@ class UserDatas extends Table {
IntColumn get totalTests => integer().withDefault(const Constant(0))();
// Временные метки
DateTimeColumn get lastTimeOnline => dateTime().nullable()();
DateTimeColumn get registrationDate => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get lastTimeOnline => customType(PgTypes.timestampWithTimezone)
.nullable()();
Column<PgDateTime> get registrationDate => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
TextColumn get lastTestSessionToken => text().nullable()();
// Сложные структуры (JSON)
@ -79,8 +89,10 @@ class UserDatas extends Table {
.map(const JsonMapConverter())();
// Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override
Set<Column> get primaryKey => {id};

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart' as drift;
import 'package:drift_postgres/drift_postgres.dart';
import 'package:mnemo_cards_backend/database/daos/discount_dao.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
@ -30,8 +31,8 @@ extension DiscountCampaignToDto on DiscountCampaign {
return DiscountCampaignDto(
id: id,
start: start,
finish: finish,
start: start.dateTime,
finish: finish.dateTime,
status: statusEnum,
name: name,
tags: tags,
@ -80,8 +81,8 @@ extension DiscountCampaignDtoToCompanion on DiscountCampaignDto {
return DiscountCampaignsCompanion.insert(
id: id != null ? drift.Value(id!) : const drift.Value.absent(),
name: drift.Value(name),
start: start,
finish: finish,
start: PgDateTime(start),
finish: PgDateTime(finish),
status: statusStr,
tags: drift.Value(tags),
);

View file

@ -42,12 +42,12 @@ class DiscountsManager {
String? targetStatus;
if (campaign.status == 'created' &&
now.isBetween(campaign.start, campaign.finish)) {
now.isBetween(campaign.start.dateTime, campaign.finish.dateTime)) {
targetStatus = 'active';
} else if (campaign.status == 'active') {
if (now.isBefore(campaign.start)) {
if (now.isBefore(campaign.start.dateTime)) {
targetStatus = 'created';
} else if (now.isAfter(campaign.finish)) {
} else if (now.isAfter(campaign.finish.dateTime)) {
targetStatus = 'expired';
}
}
@ -59,8 +59,8 @@ class DiscountsManager {
Future<DiscountError?> applyCampaign(DiscountCampaign campaign) async {
final now = DateTime.now();
if (!now.isBetween(campaign.start, campaign.finish)) {
if (now.isBefore(campaign.start)) {
if (!now.isBetween(campaign.start.dateTime, campaign.finish.dateTime)) {
if (now.isBefore(campaign.start.dateTime)) {
return DiscountError(DiscountErrorType.notStarted);
}
return DiscountError(DiscountErrorType.expired);

View file

@ -1,3 +1,4 @@
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart';
import 'package:mnemo_cards_backend/database/database.dart';
@ -47,8 +48,8 @@ class PromoCodesManager {
activationsPerCode: campaign.activationsPerCode,
activationsPerUser: campaign.activationsPerUser,
generationSize: campaign.generationSize,
start: campaign.start,
finish: campaign.finish,
start: campaign.start.dateTime,
finish: campaign.finish.dateTime,
status: _parseCampaignStatus(campaign.status),
tags: campaign.tags,
promoCodes: promoCodes,
@ -77,8 +78,8 @@ class PromoCodesManager {
activationsPerCode: campaign.activationsPerCode,
activationsPerUser: campaign.activationsPerUser,
generationSize: campaign.generationSize,
start: campaign.start,
finish: campaign.finish,
start: campaign.start.dateTime,
finish: campaign.finish.dateTime,
status: _parseCampaignStatus(campaign.status),
tags: campaign.tags,
promoCodes: promoCodes,
@ -95,8 +96,8 @@ class PromoCodesManager {
activationsPerCode: dto.activationsPerCode,
activationsPerUser: dto.activationsPerUser,
generationSize: dto.generationSize,
start: dto.start,
finish: dto.finish,
start: PgDateTime(dto.start),
finish: PgDateTime(dto.finish),
status: dto.status.name,
tags: drift.Value(dto.tags),
);
@ -119,11 +120,11 @@ class PromoCodesManager {
activationsPerCode: dto.activationsPerCode,
activationsPerUser: dto.activationsPerUser,
generationSize: dto.generationSize,
start: dto.start,
finish: dto.finish,
start: PgDateTime(dto.start),
finish: PgDateTime(dto.finish),
status: dto.status.name,
tags: dto.tags,
updatedAt: DateTime.now(),
updatedAt: PgDateTime(DateTime.now()),
);
await _db.promoCodeDao.updateCampaign(updated);
@ -137,7 +138,7 @@ class PromoCodesManager {
// Soft delete - mark as deleted
await _db.promoCodeDao.updateCampaign(existing.copyWith(
isDeleted: true,
updatedAt: DateTime.now(),
updatedAt: PgDateTime(DateTime.now()),
));
return null; // No error
@ -168,8 +169,8 @@ class PromoCodesManager {
final now = DateTime.now();
if (campaign.status != 'active' ||
campaign.start.isAfter(now) ||
campaign.finish.isBefore(now)) {
campaign.start.dateTime.isAfter(now) ||
campaign.finish.dateTime.isBefore(now)) {
return {'valid': false, 'message': 'Promo code expired'};
}
@ -254,7 +255,7 @@ class PromoCodesManager {
await _db.promoCodeDao.updateCampaign(
campaign.copyWith(
status: 'active',
updatedAt: DateTime.now(),
updatedAt: PgDateTime(DateTime.now()),
),
);
});

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
@ -57,7 +58,7 @@ class AchievementManager {
final definition = AchievementDefinitions.getById(userAchievement.achievementId);
if (definition != null) {
achievements.add(definition.copyWith(
unlockedAt: userAchievement.unlockedAt,
unlockedAt: userAchievement.unlockedAt.dateTime,
progress: userAchievement.progress,
));
}
@ -90,7 +91,7 @@ class AchievementManager {
final companion = UserAchievementsCompanion.insert(
userId: userId,
achievementId: achievementId,
unlockedAt: Value(DateTime.now()),
unlockedAt: Value(PgDateTime(DateTime.now())),
progress: Value(1.0),
);

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:collection';
import 'dart:math';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:drift/drift.dart' as drift;
@ -45,7 +46,7 @@ class SessionTracker {
// Create new session
final sessionId = _generateSessionId();
final now = DateTime.now();
final now = PgDateTime(DateTime.now());
await _db.statisticsDao.createSession(
StudySessionsCompanion.insert(
@ -116,7 +117,7 @@ class SessionTracker {
wordsLearned: wordsLearned ?? session.wordsLearned,
testsCompleted: testsCompleted ?? session.testsCompleted,
accuracy: accuracy ?? session.accuracy,
updatedAt: DateTime.now(),
updatedAt: PgDateTime(DateTime.now()),
);
await _db.statisticsDao.updateSession(updatedSession);
@ -155,7 +156,7 @@ class SessionTracker {
if (session != null &&
session.endTime == null &&
now.difference(session.startTime).inMinutes > _sessionTimeout.inMinutes) {
now.difference(session.startTime.dateTime).inMinutes > _sessionTimeout.inMinutes) {
expiredSessions.add(sessionId);
}
}
@ -190,7 +191,7 @@ class SessionTracker {
final session = await _db.statisticsDao.getSessionBySessionId(sessionId);
if (session != null) {
await _db.statisticsDao.updateSession(
session.copyWith(updatedAt: DateTime.now()),
session.copyWith(updatedAt: PgDateTime(DateTime.now())),
);
}
}

View file

@ -1,3 +1,4 @@
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.dart';
@ -64,7 +65,7 @@ class TaskManager {
}
// Обновить статус задачи
final now = DateTime.now();
final now = PgDateTime(DateTime.now());
await _db.taskDao.updateUserTask(
task.copyWith(
status: 'completed',
@ -77,7 +78,7 @@ class TaskManager {
UserTaskResultsCompanion.insert(
userId: userId,
taskId: taskId,
results: drift.Value({'completed': true, 'completedAt': now.toIso8601String()}),
results: drift.Value({'completed': true, 'completedAt': now.dateTime.toIso8601String()}),
),
);
});

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:developer';
import 'dart:math' as math;
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.dart';
@ -48,7 +49,7 @@ class UserManager {
final now = DateTime.now();
final token = await _db.userDao.getTokenByUserId(user.id!);
if (token != null) {
if (token.expires.isAfter(now)) {
if (token.expires.dateTime.isAfter(now)) {
return token.token;
}
await _db.userDao.deleteToken(token.id);
@ -59,7 +60,7 @@ class UserManager {
token: userToken,
externalUserId: externalId,
userId: user.id!,
expires: now.add(const Duration(days: 360)),
expires: PgDateTime(now.add(const Duration(days: 360))),
),
);
return userToken;
@ -71,7 +72,7 @@ class UserManager {
return null;
}
final now = DateTime.now();
if (token.expires.isBefore(now)) {
if (token.expires.dateTime.isBefore(now)) {
await _db.userDao.deleteToken(token.id);
return null;
}
@ -98,8 +99,8 @@ class UserManager {
await _db.userDao.updateUserDataPartial(
UserDatasCompanion(
userId: drift.Value(userId),
lastTimeOnline: drift.Value(lastOnline),
updatedAt: drift.Value(DateTime.now()),
lastTimeOnline: drift.Value(PgDateTime(lastOnline)),
updatedAt: drift.Value(PgDateTime(DateTime.now())),
),
);
}
@ -137,7 +138,7 @@ class UserManager {
await _db.userDao.createUserData(
UserDatasCompanion.insert(
userId: userId,
registrationDate: drift.Value(DateTime.now()),
registrationDate: drift.Value(PgDateTime(DateTime.now())),
),
);
@ -166,7 +167,7 @@ class UserManager {
UsersCompanion(
id: drift.Value(user.id!),
userSettings: drift.Value(jsonEncode(settings.toJson())),
updatedAt: drift.Value(DateTime.now()),
updatedAt: drift.Value(PgDateTime(DateTime.now())),
),
);
}
@ -219,7 +220,7 @@ class UserManager {
// Обновить существующую статистику
final updatedStat = existingStat.copyWith(
results: drift.Value(updatedResults),
updatedAt: DateTime.now(),
updatedAt: PgDateTime(DateTime.now()),
);
await _db.testDao.updateTestStatistics(updatedStat);
} else {
@ -229,7 +230,7 @@ class UserManager {
userId: user.id!,
testId: testStat.testId,
results: drift.Value(updatedResults),
completedAt: drift.Value(DateTime.now()),
completedAt: drift.Value(PgDateTime(DateTime.now())),
),
);
}
@ -259,8 +260,8 @@ class UserManager {
studyDates: drift.Value(studyDates),
currentStreak: drift.Value(currentStreak),
longestStreak: drift.Value(longestStreak),
lastTimeOnline: drift.Value(now),
updatedAt: drift.Value(now),
lastTimeOnline: drift.Value(PgDateTime(now)),
updatedAt: drift.Value(PgDateTime(now)),
),
);
}

View file

@ -1,3 +1,4 @@
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.dart';
@ -44,7 +45,7 @@ class UserManager {
final now = DateTime.now();
final token = await _db.userDao.getTokenByUserId(user.id!);
if (token != null) {
if (token.expires.isAfter(now)) {
if (token.expires.dateTime.isAfter(now)) {
return token.token;
}
await _db.userDao.deleteToken(token.id);
@ -55,7 +56,7 @@ class UserManager {
token: userToken,
externalUserId: externalId,
userId: user.id!,
expires: now.add(const Duration(days: 360)),
expires: PgDateTime(now.add(const Duration(days: 360))),
),
);
return userToken;
@ -67,7 +68,7 @@ class UserManager {
return null;
}
final now = DateTime.now();
if (token.expires.isBefore(now)) {
if (token.expires.dateTime.isBefore(now)) {
await _db.userDao.deleteToken(token.id);
return null;
}
@ -94,8 +95,8 @@ class UserManager {
await _db.userDao.updateUserDataPartial(
UserDatasCompanion(
userId: drift.Value(userId),
lastTimeOnline: drift.Value(lastOnline),
updatedAt: drift.Value(DateTime.now()),
lastTimeOnline: drift.Value(PgDateTime(lastOnline)),
updatedAt: drift.Value(PgDateTime(DateTime.now())),
),
);
}
@ -133,7 +134,7 @@ class UserManager {
await _db.userDao.createUserData(
UserDatasCompanion.insert(
userId: userId,
registrationDate: drift.Value(DateTime.now()),
registrationDate: drift.Value(PgDateTime(DateTime.now())),
),
);

View file

@ -53,10 +53,10 @@ packages:
dependency: "direct main"
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.13.0"
auth_header:
dependency: transitive
description:
@ -229,10 +229,10 @@ packages:
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.7"
dart_style:
dependency: transitive
description:
@ -595,10 +595,10 @@ packages:
dependency: "direct main"
description:
name: postgres
sha256: "83ba7afb0e778cc1f373e514c6110d323b81dfbaced774d3a7f4a0e21536eb45"
sha256: fefbbfe749c6130e5096588b9c4459173684c695952cd7636ab19be76f255469
url: "https://pub.dev"
source: hosted
version: "3.4.8"
version: "3.5.9"
pub_semver:
dependency: transitive
description:
@ -639,22 +639,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
sasl_scram:
dependency: transitive
description:
name: sasl_scram
sha256: a47207a436eb650f8fdcf54a2e2587b850dc3caef9973ce01f332b07a6fc9cb9
url: "https://pub.dev"
source: hosted
version: "0.1.1"
saslprep:
dependency: transitive
description:
name: saslprep
sha256: "3d421d10be9513bf4459c17c5e70e7b8bc718c9fc5ad4ba5eb4f5fd27396f740"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
shelf:
dependency: "direct main"
description:
@ -827,18 +811,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
stream_transform:
dependency: transitive
description:
@ -895,14 +879,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
unorm_dart:
dependency: transitive
description:
name: unorm_dart
sha256: "0c69186b03ca6addab0774bcc0f4f17b88d4ce78d9d4d8f0619e30a99ead58e7"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
uuid:
dependency: "direct main"
description:

View file

@ -19,9 +19,9 @@ dependencies:
path: ../mnemo_cards_common_backend
# PostgreSQL + Drift
drift: ^2.14.0
drift_postgres: ^1.1.0
postgres: ^3.0.4
drift: ^2.30.0
drift_postgres: ^1.3.1
postgres: ^3.5.9
image: ^4.1.7
http: ^1.1.0
@ -61,7 +61,7 @@ dev_dependencies:
copy_with_extension_gen: ^11.0.0
# Drift code generation
drift_dev: ^2.14.0
drift_dev: ^2.30.0
shelf_router_generator: ^1.1.3
shelf_open_api_generator: