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" Button.displayName = "Button"
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants } export { Button, buttonVariants }

View file

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

View file

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

View file

@ -44,8 +44,8 @@ describe('API Clients', () => {
const token = 'test-admin-token' const token = 'test-admin-token'
localStorage.setItem('admin_token', token) localStorage.setItem('admin_token', token)
const requestConfig = { headers: {} } const requestConfig = { headers: {} as Record<string, string> }
const addAuthToken = (config: any) => { const addAuthToken = (config: { headers: Record<string, string> }) => {
const storedToken = localStorage.getItem('admin_token') const storedToken = localStorage.getItem('admin_token')
if (storedToken) { if (storedToken) {
config.headers.Authorization = `Bearer ${storedToken}` config.headers.Authorization = `Bearer ${storedToken}`
@ -58,8 +58,8 @@ describe('API Clients', () => {
}) })
it('should not add authorization header when no token exists', () => { it('should not add authorization header when no token exists', () => {
const requestConfig = { headers: {} } const requestConfig = { headers: {} as Record<string, string> }
const addAuthToken = (config: any) => { const addAuthToken = (config: { headers: Record<string, string> }) => {
const storedToken = localStorage.getItem('admin_token') const storedToken = localStorage.getItem('admin_token')
if (storedToken) { if (storedToken) {
config.headers.Authorization = `Bearer ${storedToken}` config.headers.Authorization = `Bearer ${storedToken}`
@ -81,7 +81,7 @@ describe('API Clients', () => {
writable: true, writable: true,
}) })
const handleAuthError = (error: any) => { const handleAuthError = (error: { response?: { status?: number } }) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
localStorage.removeItem('admin_token') localStorage.removeItem('admin_token')
window.location.href = '/login' window.location.href = '/login'
@ -108,7 +108,7 @@ describe('API Clients', () => {
writable: true, writable: true,
}) })
const handleAuthError = (error: any) => { const handleAuthError = (error: { response?: { status?: number } }) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
localStorage.removeItem('admin_token') localStorage.removeItem('admin_token')
window.location.href = '/login' 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' import { useAuthStore } from '@/stores/authStore'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.mnemo-cards.online' 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 // Request interceptor to add auth token
const addAuthToken = (config: any) => { const addAuthToken = (config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('admin_token') const token = localStorage.getItem('admin_token')
const isPublicAuthEndpoint = const isPublicAuthEndpoint =
config.url?.includes('/admin/auth/request-code') || config.url?.includes('/admin/auth/request-code') ||
@ -33,7 +33,7 @@ const addAuthToken = (config: any) => {
if (token && token.trim()) { if (token && token.trim()) {
// Ensure headers object exists // Ensure headers object exists
if (!config.headers) { if (!config.headers) {
config.headers = {} config.headers = {} as InternalAxiosRequestConfig['headers']
} }
const trimmedToken = token.trim() const trimmedToken = token.trim()
// Set Authorization header - axios will normalize it to lowercase for HTTP // Set Authorization header - axios will normalize it to lowercase for HTTP
@ -61,14 +61,15 @@ const addAuthToken = (config: any) => {
let isLoggingOut = false let isLoggingOut = false
// Response interceptor for error handling // Response interceptor for error handling
const handleAuthError = (error: any) => { const handleAuthError = (error: unknown) => {
if (error.response?.status === 401) { const axiosError = error as AxiosError
if (axiosError.response?.status === 401) {
// Check if we're already on login page // Check if we're already on login page
const currentPath = window.location.pathname const currentPath = window.location.pathname
const isPublicAuthEndpoint = const isPublicAuthEndpoint =
error.config?.url?.includes('/admin/auth/request-code') || axiosError.config?.url?.includes('/admin/auth/request-code') ||
error.config?.url?.includes('/admin/auth/verify-code') || axiosError.config?.url?.includes('/admin/auth/verify-code') ||
error.config?.url?.includes('/admin/auth/code-status') axiosError.config?.url?.includes('/admin/auth/code-status')
// Don't logout for public auth endpoints or if already on login page // Don't logout for public auth endpoints or if already on login page
if (currentPath === '/login' || isPublicAuthEndpoint) { 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 } export { Badge, badgeVariants }

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner' import { toast } from 'sonner'
import { cardsApi } from '@/api/cards' import { cardsApi } from '@/api/cards'
import type { GameCardDto, PaginatedResponse } from '@/types/models' import type { GameCardDto, PaginatedResponse } from '@/types/models'
import type { AxiosError } from 'axios'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@ -71,8 +72,9 @@ export default function CardsPage() {
toast.success('Card created successfully') toast.success('Card created successfully')
closeDialog() closeDialog()
}, },
onError: (error: any) => { onError: (error: unknown) => {
toast.error(error.response?.data?.message || 'Failed to create card') 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') toast.success('Card updated successfully')
closeDialog() closeDialog()
}, },
onError: (error: any) => { onError: (error: unknown) => {
toast.error(error.response?.data?.message || 'Failed to update card') 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) setIsDeleteDialogOpen(false)
setCardToDelete(null) setCardToDelete(null)
}, },
onError: (error: any) => { onError: (error: unknown) => {
toast.error(error.response?.data?.message || 'Failed to delete card') 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 { toast } from 'sonner'
import { packsApi } from '@/api/packs' import { packsApi } from '@/api/packs'
import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse } from '@/types/models' import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse } from '@/types/models'
import type { AxiosError } from 'axios'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@ -77,8 +78,9 @@ export default function PacksPage() {
toast.success('Pack created successfully') toast.success('Pack created successfully')
closeDialog() closeDialog()
}, },
onError: (error: any) => { onError: (error: unknown) => {
toast.error(error.response?.data?.message || 'Failed to create pack') 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') toast.success('Pack updated successfully')
closeDialog() closeDialog()
}, },
onError: (error: any) => { onError: (error: unknown) => {
toast.error(error.response?.data?.message || 'Failed to update pack') 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) setIsDeleteDialogOpen(false)
setPackToDelete(null) setPackToDelete(null)
}, },
onError: (error: any) => { onError: (error: unknown) => {
toast.error(error.response?.data?.message || 'Failed to delete pack') 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, order: fullPack.order || 0,
}) })
setIsDialogOpen(true) setIsDialogOpen(true)
} catch (error) { } catch {
toast.error('Failed to load pack details') 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 { toast } from 'sonner'
import { usersApi } from '@/api/users' import { usersApi } from '@/api/users'
import type { UserDto, PaginatedResponse, PaymentDto } from '@/types/models' import type { UserDto, PaginatedResponse, PaymentDto } from '@/types/models'
import type { AxiosError } from 'axios'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@ -71,8 +72,9 @@ export default function UsersPage() {
toast.success('User updated successfully') toast.success('User updated successfully')
closeEditDialog() closeEditDialog()
}, },
onError: (error: any) => { onError: (error: unknown) => {
toast.error(error.response?.data?.message || 'Failed to update user') 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) setIsDeleteDialogOpen(false)
setUserToDelete(null) setUserToDelete(null)
}, },
onError: (error: any) => { onError: (error: unknown) => {
toast.error(error.response?.data?.message || 'Failed to delete user') 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) setUserPurchases(purchases)
setSelectedUser(user) setSelectedUser(user)
setIsPurchasesDialogOpen(true) setIsPurchasesDialogOpen(true)
} catch (error) { } catch {
toast.error('Failed to load user purchases') toast.error('Failed to load user purchases')
} }
} }

View file

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

View file

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
@ -32,7 +33,7 @@ extension PaymentToDto on Payment {
), ),
externalToken: externalToken, externalToken: externalToken,
meta: meta, meta: meta,
date: date, date: date.dateTime,
products: productsList, products: productsList,
); );
} }
@ -52,7 +53,7 @@ extension PaymentFromDto on PaymentDto {
paymentSystem: paymentSystem.name, paymentSystem: paymentSystem.name,
externalToken: drift.Value(externalToken), externalToken: drift.Value(externalToken),
meta: drift.Value(meta), meta: drift.Value(meta),
date: drift.Value(date), date: drift.Value(PgDateTime(date)),
products: drift.Value(productsJson), products: drift.Value(productsJson),
); );
} }
@ -65,7 +66,7 @@ extension PaymentUpdate on PaymentDto {
status: drift.Value(status.name), status: drift.Value(status.name),
externalToken: drift.Value(externalToken), externalToken: drift.Value(externalToken),
meta: drift.Value(meta), 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 'dart:developer';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/api/purchase/google_play_purchase_handler.dart'; import 'package:mnemo_cards_backend/api/purchase/google_play_purchase_handler.dart';
import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/database/database.dart';
@ -69,8 +70,8 @@ class PaymentManager {
await _db.subscriptionDao.createUserSubscription( await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert( UserSubscriptionsCompanion.insert(
userId: userId, userId: userId,
start: now, start: PgDateTime(now),
finish: endDate, finish: PgDateTime(endDate),
features: drift.Value(plan.features as List<dynamic>), features: drift.Value(plan.features as List<dynamic>),
), ),
); );
@ -149,8 +150,8 @@ class PaymentManager {
await _db.subscriptionDao.createUserSubscription( await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert( UserSubscriptionsCompanion.insert(
userId: payment.userId, userId: payment.userId,
start: now, start: PgDateTime(now),
finish: endDate, finish: PgDateTime(endDate),
features: drift.Value(plan.features), 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:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/database/database.dart';
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
@ -25,8 +26,8 @@ class SubscriptionManager {
return SubscriptionDto( return SubscriptionDto(
page: null, page: null,
isActive: subscription != null, isActive: subscription != null,
start: subscription?.start, start: subscription?.start.dateTime,
finish: subscription?.finish, finish: subscription?.finish.dateTime,
); );
} }
@ -41,8 +42,8 @@ class SubscriptionManager {
await _db.subscriptionDao.createUserSubscription( await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert( UserSubscriptionsCompanion.insert(
userId: user.id!, userId: user.id!,
start: now, start: PgDateTime(now),
finish: endDate, finish: PgDateTime(endDate),
features: const Value([]), features: const Value([]),
), ),
); );
@ -86,8 +87,8 @@ class SubscriptionManager {
await _db.subscriptionDao.createUserSubscription( await _db.subscriptionDao.createUserSubscription(
UserSubscriptionsCompanion.insert( UserSubscriptionsCompanion.insert(
userId: userId, userId: userId,
start: now, start: PgDateTime(now),
finish: endDate, finish: PgDateTime(endDate),
features: Value(plan.features as List<dynamic>), features: Value(plan.features as List<dynamic>),
), ),
); );

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart'; import '../database.dart';
import '../tables/achievements.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)) ..where((ua) => ua.userId.equals(userId) & ua.achievementId.equals(achievementId))
).write(UserAchievementsCompanion( ).write(UserAchievementsCompanion(
progress: Value(progress), progress: Value(progress),
updatedAt: Value(DateTime.now()), updatedAt: Value(PgDateTime(DateTime.now())),
)); ));
return count > 0; return count > 0;
} }

View file

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

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart'; import '../database.dart';
import '../tables/packs.dart'; import '../tables/packs.dart';
import '../tables/relations.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'); if (packId == null) throw ArgumentError('Pack ID is required');
return (update(cardPacks)..where((p) => p.id.equals(packId))) 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) /// Удалить пак (soft delete)
@ -68,7 +69,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
return (update(cardPacks)..where((p) => p.id.equals(packId))) return (update(cardPacks)..where((p) => p.id.equals(packId)))
.write(CardPacksCompanion( .write(CardPacksCompanion(
isDeleted: const Value(true), 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))) return (update(gameCards)..where((c) => c.id.equals(cardId)))
.write(GameCardsCompanion( .write(GameCardsCompanion(
isDeleted: const Value(true), 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/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart'; import '../database.dart';
import '../tables/payments.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', 'SELECT id FROM payments WHERE user_id = ? AND date = ? AND amount = ? ORDER BY created_at DESC LIMIT 1',
variables: [ variables: [
Variable.withString(payment.userId.value), Variable.withString(payment.userId.value),
Variable.withDateTime(inserted.date), Variable.withDateTime(inserted.date.dateTime),
Variable.withString(inserted.amount), Variable.withString(inserted.amount),
], ],
readsFrom: {payments}, readsFrom: {payments},
@ -81,7 +82,7 @@ class PaymentDao extends DatabaseAccessor<AppDatabase> with _$PaymentDaoMixin {
return (update(payments)..where((p) => p.id.equals(paymentId))) return (update(payments)..where((p) => p.id.equals(paymentId)))
.write(PaymentsCompanion( .write(PaymentsCompanion(
status: Value(status), 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/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart'; import '../database.dart';
import '../tables/promo_codes.dart'; import '../tables/promo_codes.dart';
import '../tables/users.dart'; import '../tables/users.dart';
@ -18,7 +19,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
/// Получить все активные кампании /// Получить все активные кампании
Future<List<PromoCodesCampaign>> getActiveCampaigns() { Future<List<PromoCodesCampaign>> getActiveCampaigns() {
final now = DateTime.now(); final now = PgDateTime(DateTime.now());
return (select(db.promoCodesCampaigns) return (select(db.promoCodesCampaigns)
..where((c) => c.status.equals('active')) ..where((c) => c.status.equals('active'))
..where((c) => c.start.isSmallerOrEqualValue(now)) ..where((c) => c.start.isSmallerOrEqualValue(now))
@ -44,7 +45,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
..where((c) => c.id.equals(campaignId)) ..where((c) => c.id.equals(campaignId))
).write(PromoCodesCampaignsCompanion( ).write(PromoCodesCampaignsCompanion(
status: Value(status), 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) { if (code != null) {
await update(db.promoCodes).replace(code.copyWith( await update(db.promoCodes).replace(code.copyWith(
activations: code.activations + 1, 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/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart'; import '../database.dart';
import '../tables/statistics.dart'; import '../tables/statistics.dart';
import '../tables/users.dart'; import '../tables/users.dart';
@ -42,10 +43,10 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
..orderBy([(s) => OrderingTerm.desc(s.startTime)]); ..orderBy([(s) => OrderingTerm.desc(s.startTime)]);
if (fromDate != null) { if (fromDate != null) {
query.where((s) => s.startTime.isBiggerOrEqualValue(fromDate)); query.where((s) => s.startTime.isBiggerOrEqualValue(PgDateTime(fromDate)));
} }
if (toDate != null) { if (toDate != null) {
query.where((s) => s.startTime.isSmallerOrEqualValue(toDate)); query.where((s) => s.startTime.isSmallerOrEqualValue(PgDateTime(toDate)));
} }
if (limit != null) { if (limit != null) {
@ -74,8 +75,8 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
}) { }) {
final updates = StudySessionsCompanion( final updates = StudySessionsCompanion(
id: Value(sessionId), id: Value(sessionId),
endTime: Value(DateTime.now()), endTime: Value(PgDateTime(DateTime.now())),
updatedAt: Value(DateTime.now()), updatedAt: Value(PgDateTime(DateTime.now())),
wordsLearned: wordsLearned != null ? Value(wordsLearned) : const Value.absent(), wordsLearned: wordsLearned != null ? Value(wordsLearned) : const Value.absent(),
testsCompleted: testsCompleted != null ? Value(testsCompleted) : const Value.absent(), testsCompleted: testsCompleted != null ? Value(testsCompleted) : const Value.absent(),
accuracy: accuracy != null ? Value(accuracy) : 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/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart'; import '../database.dart';
import '../tables/subscriptions.dart'; import '../tables/subscriptions.dart';
import '../tables/users.dart'; import '../tables/users.dart';
@ -46,7 +47,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
/// Получить активную подписку пользователя /// Получить активную подписку пользователя
Future<UserSubscription?> getActiveSubscription(String userId) async { Future<UserSubscription?> getActiveSubscription(String userId) async {
final now = DateTime.now(); final now = PgDateTime(DateTime.now());
return (select(userSubscriptions) return (select(userSubscriptions)
..where((us) => us.userId.equals(userId)) ..where((us) => us.userId.equals(userId))
..where((us) => us.start.isSmallerThanValue(now)) ..where((us) => us.start.isSmallerThanValue(now))
@ -81,7 +82,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
/// Получить всех пользователей с активными подписками /// Получить всех пользователей с активными подписками
Future<List<UserSubscription>> getActiveSubscriptions() async { Future<List<UserSubscription>> getActiveSubscriptions() async {
final now = DateTime.now(); final now = PgDateTime(DateTime.now());
return (select(userSubscriptions) return (select(userSubscriptions)
..where((us) => us.start.isSmallerThanValue(now)) ..where((us) => us.start.isSmallerThanValue(now))
..where((us) => us.finish.isBiggerThanValue(now)) ..where((us) => us.finish.isBiggerThanValue(now))
@ -100,7 +101,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
if (subscription != null) { if (subscription != null) {
await update(userSubscriptions).replace( await update(userSubscriptions).replace(
subscription.copyWith( subscription.copyWith(
finish: DateTime.now(), finish: PgDateTime(DateTime.now()),
), ),
); );
} }

View file

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

View file

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

View file

@ -32,7 +32,6 @@ import 'daos/promo_code_dao.dart';
import 'daos/discount_dao.dart'; import 'daos/discount_dao.dart';
import 'daos/statistics_dao.dart'; import 'daos/statistics_dao.dart';
import 'daos/achievement_dao.dart'; import 'daos/achievement_dao.dart';
import 'postgres_constants.dart';
// Сгенерированный код будет здесь // Сгенерированный код будет здесь
part 'database.g.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 'package:drift/drift.dart';
import '../postgres_constants.dart'; import 'package:drift_postgres/drift_postgres.dart';
import 'users.dart'; import 'users.dart';
/// Таблица UserAchievements - достижения пользователей /// Таблица UserAchievements - достижения пользователей
class UserAchievements extends Table { 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) // Achievement ID (from AchievementDefinitions)
TextColumn get achievementId => text()(); TextColumn get achievementId => text()();
// When achievement was unlocked // 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) // Progress (0.0 to 1.0)
RealColumn get progress => real().withDefault(const Constant(1.0))(); RealColumn get progress => real().withDefault(const Constant(1.0))();
// Audit // Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)(); Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)(); .withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,21 +1,27 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../converters.dart' show generateUuid; import '../converters.dart' show generateUuid;
import '../postgres_constants.dart';
/// Таблица ShareRequests - запросы на шаринг через Telegram /// Таблица ShareRequests - запросы на шаринг через Telegram
class ShareRequests extends Table { 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 telegramUserId => text()();
TextColumn get telegramUsername => text().nullable()(); 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 // Audit
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)(); Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)(); .withDefault(now())();
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
.withDefault(now())();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:math'; import 'dart:math';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/database/database.dart';
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
@ -45,7 +46,7 @@ class SessionTracker {
// Create new session // Create new session
final sessionId = _generateSessionId(); final sessionId = _generateSessionId();
final now = DateTime.now(); final now = PgDateTime(DateTime.now());
await _db.statisticsDao.createSession( await _db.statisticsDao.createSession(
StudySessionsCompanion.insert( StudySessionsCompanion.insert(
@ -116,7 +117,7 @@ class SessionTracker {
wordsLearned: wordsLearned ?? session.wordsLearned, wordsLearned: wordsLearned ?? session.wordsLearned,
testsCompleted: testsCompleted ?? session.testsCompleted, testsCompleted: testsCompleted ?? session.testsCompleted,
accuracy: accuracy ?? session.accuracy, accuracy: accuracy ?? session.accuracy,
updatedAt: DateTime.now(), updatedAt: PgDateTime(DateTime.now()),
); );
await _db.statisticsDao.updateSession(updatedSession); await _db.statisticsDao.updateSession(updatedSession);
@ -155,7 +156,7 @@ class SessionTracker {
if (session != null && if (session != null &&
session.endTime == null && session.endTime == null &&
now.difference(session.startTime).inMinutes > _sessionTimeout.inMinutes) { now.difference(session.startTime.dateTime).inMinutes > _sessionTimeout.inMinutes) {
expiredSessions.add(sessionId); expiredSessions.add(sessionId);
} }
} }
@ -190,7 +191,7 @@ class SessionTracker {
final session = await _db.statisticsDao.getSessionBySessionId(sessionId); final session = await _db.statisticsDao.getSessionBySessionId(sessionId);
if (session != null) { if (session != null) {
await _db.statisticsDao.updateSession( 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:injectable/injectable.dart';
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.dart'; 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( await _db.taskDao.updateUserTask(
task.copyWith( task.copyWith(
status: 'completed', status: 'completed',
@ -77,7 +78,7 @@ class TaskManager {
UserTaskResultsCompanion.insert( UserTaskResultsCompanion.insert(
userId: userId, userId: userId,
taskId: taskId, 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:developer';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/database/database.dart';
@ -48,7 +49,7 @@ class UserManager {
final now = DateTime.now(); final now = DateTime.now();
final token = await _db.userDao.getTokenByUserId(user.id!); final token = await _db.userDao.getTokenByUserId(user.id!);
if (token != null) { if (token != null) {
if (token.expires.isAfter(now)) { if (token.expires.dateTime.isAfter(now)) {
return token.token; return token.token;
} }
await _db.userDao.deleteToken(token.id); await _db.userDao.deleteToken(token.id);
@ -59,7 +60,7 @@ class UserManager {
token: userToken, token: userToken,
externalUserId: externalId, externalUserId: externalId,
userId: user.id!, userId: user.id!,
expires: now.add(const Duration(days: 360)), expires: PgDateTime(now.add(const Duration(days: 360))),
), ),
); );
return userToken; return userToken;
@ -71,7 +72,7 @@ class UserManager {
return null; return null;
} }
final now = DateTime.now(); final now = DateTime.now();
if (token.expires.isBefore(now)) { if (token.expires.dateTime.isBefore(now)) {
await _db.userDao.deleteToken(token.id); await _db.userDao.deleteToken(token.id);
return null; return null;
} }
@ -98,8 +99,8 @@ class UserManager {
await _db.userDao.updateUserDataPartial( await _db.userDao.updateUserDataPartial(
UserDatasCompanion( UserDatasCompanion(
userId: drift.Value(userId), userId: drift.Value(userId),
lastTimeOnline: drift.Value(lastOnline), lastTimeOnline: drift.Value(PgDateTime(lastOnline)),
updatedAt: drift.Value(DateTime.now()), updatedAt: drift.Value(PgDateTime(DateTime.now())),
), ),
); );
} }
@ -137,7 +138,7 @@ class UserManager {
await _db.userDao.createUserData( await _db.userDao.createUserData(
UserDatasCompanion.insert( UserDatasCompanion.insert(
userId: userId, userId: userId,
registrationDate: drift.Value(DateTime.now()), registrationDate: drift.Value(PgDateTime(DateTime.now())),
), ),
); );
@ -166,7 +167,7 @@ class UserManager {
UsersCompanion( UsersCompanion(
id: drift.Value(user.id!), id: drift.Value(user.id!),
userSettings: drift.Value(jsonEncode(settings.toJson())), 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( final updatedStat = existingStat.copyWith(
results: drift.Value(updatedResults), results: drift.Value(updatedResults),
updatedAt: DateTime.now(), updatedAt: PgDateTime(DateTime.now()),
); );
await _db.testDao.updateTestStatistics(updatedStat); await _db.testDao.updateTestStatistics(updatedStat);
} else { } else {
@ -229,7 +230,7 @@ class UserManager {
userId: user.id!, userId: user.id!,
testId: testStat.testId, testId: testStat.testId,
results: drift.Value(updatedResults), 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), studyDates: drift.Value(studyDates),
currentStreak: drift.Value(currentStreak), currentStreak: drift.Value(currentStreak),
longestStreak: drift.Value(longestStreak), longestStreak: drift.Value(longestStreak),
lastTimeOnline: drift.Value(now), lastTimeOnline: drift.Value(PgDateTime(now)),
updatedAt: drift.Value(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:injectable/injectable.dart';
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_backend/database/database.dart';
@ -44,7 +45,7 @@ class UserManager {
final now = DateTime.now(); final now = DateTime.now();
final token = await _db.userDao.getTokenByUserId(user.id!); final token = await _db.userDao.getTokenByUserId(user.id!);
if (token != null) { if (token != null) {
if (token.expires.isAfter(now)) { if (token.expires.dateTime.isAfter(now)) {
return token.token; return token.token;
} }
await _db.userDao.deleteToken(token.id); await _db.userDao.deleteToken(token.id);
@ -55,7 +56,7 @@ class UserManager {
token: userToken, token: userToken,
externalUserId: externalId, externalUserId: externalId,
userId: user.id!, userId: user.id!,
expires: now.add(const Duration(days: 360)), expires: PgDateTime(now.add(const Duration(days: 360))),
), ),
); );
return userToken; return userToken;
@ -67,7 +68,7 @@ class UserManager {
return null; return null;
} }
final now = DateTime.now(); final now = DateTime.now();
if (token.expires.isBefore(now)) { if (token.expires.dateTime.isBefore(now)) {
await _db.userDao.deleteToken(token.id); await _db.userDao.deleteToken(token.id);
return null; return null;
} }
@ -94,8 +95,8 @@ class UserManager {
await _db.userDao.updateUserDataPartial( await _db.userDao.updateUserDataPartial(
UserDatasCompanion( UserDatasCompanion(
userId: drift.Value(userId), userId: drift.Value(userId),
lastTimeOnline: drift.Value(lastOnline), lastTimeOnline: drift.Value(PgDateTime(lastOnline)),
updatedAt: drift.Value(DateTime.now()), updatedAt: drift.Value(PgDateTime(DateTime.now())),
), ),
); );
} }
@ -133,7 +134,7 @@ class UserManager {
await _db.userDao.createUserData( await _db.userDao.createUserData(
UserDatasCompanion.insert( UserDatasCompanion.insert(
userId: userId, userId: userId,
registrationDate: drift.Value(DateTime.now()), registrationDate: drift.Value(PgDateTime(DateTime.now())),
), ),
); );

View file

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

View file

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