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
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:
parent
9c132174c5
commit
1ab5071345
61 changed files with 2701 additions and 2073 deletions
|
|
@ -53,4 +53,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Button, buttonVariants }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable react-refresh/only-export-components */
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { adminApiClient } from './client'
|
||||
import type { AuthResponse, RequestCodeResponse, CodeStatusResponse } from '@/types/models'
|
||||
import type { AxiosError } from 'axios'
|
||||
|
||||
export const authApi = {
|
||||
// Request authentication code (creates web code)
|
||||
|
|
@ -13,9 +14,10 @@ export const authApi = {
|
|||
try {
|
||||
const response = await adminApiClient.get(`/api/v2/admin/auth/code-status/${code}`)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError
|
||||
// If 404, return a proper error response
|
||||
if (error.response?.status === 404) {
|
||||
if (axiosError.response?.status === 404) {
|
||||
return {
|
||||
success: false,
|
||||
code: code,
|
||||
|
|
@ -40,7 +42,7 @@ export const authApi = {
|
|||
},
|
||||
|
||||
// Get current admin info (for token validation)
|
||||
getCurrentUser: async (): Promise<{ success: boolean; user?: any; message?: string }> => {
|
||||
getCurrentUser: async (): Promise<{ success: boolean; user?: unknown; message?: string }> => {
|
||||
const response = await adminApiClient.get('/api/v2/admin/auth/me')
|
||||
return response.data
|
||||
},
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ describe('API Clients', () => {
|
|||
const token = 'test-admin-token'
|
||||
localStorage.setItem('admin_token', token)
|
||||
|
||||
const requestConfig = { headers: {} }
|
||||
const addAuthToken = (config: any) => {
|
||||
const requestConfig = { headers: {} as Record<string, string> }
|
||||
const addAuthToken = (config: { headers: Record<string, string> }) => {
|
||||
const storedToken = localStorage.getItem('admin_token')
|
||||
if (storedToken) {
|
||||
config.headers.Authorization = `Bearer ${storedToken}`
|
||||
|
|
@ -58,8 +58,8 @@ describe('API Clients', () => {
|
|||
})
|
||||
|
||||
it('should not add authorization header when no token exists', () => {
|
||||
const requestConfig = { headers: {} }
|
||||
const addAuthToken = (config: any) => {
|
||||
const requestConfig = { headers: {} as Record<string, string> }
|
||||
const addAuthToken = (config: { headers: Record<string, string> }) => {
|
||||
const storedToken = localStorage.getItem('admin_token')
|
||||
if (storedToken) {
|
||||
config.headers.Authorization = `Bearer ${storedToken}`
|
||||
|
|
@ -81,7 +81,7 @@ describe('API Clients', () => {
|
|||
writable: true,
|
||||
})
|
||||
|
||||
const handleAuthError = (error: any) => {
|
||||
const handleAuthError = (error: { response?: { status?: number } }) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('admin_token')
|
||||
window.location.href = '/login'
|
||||
|
|
@ -108,7 +108,7 @@ describe('API Clients', () => {
|
|||
writable: true,
|
||||
})
|
||||
|
||||
const handleAuthError = (error: any) => {
|
||||
const handleAuthError = (error: { response?: { status?: number } }) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('admin_token')
|
||||
window.location.href = '/login'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import axios from 'axios'
|
||||
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.mnemo-cards.online'
|
||||
|
|
@ -23,7 +23,7 @@ export const adminApiClient = axios.create({
|
|||
})
|
||||
|
||||
// Request interceptor to add auth token
|
||||
const addAuthToken = (config: any) => {
|
||||
const addAuthToken = (config: InternalAxiosRequestConfig) => {
|
||||
const token = localStorage.getItem('admin_token')
|
||||
const isPublicAuthEndpoint =
|
||||
config.url?.includes('/admin/auth/request-code') ||
|
||||
|
|
@ -33,7 +33,7 @@ const addAuthToken = (config: any) => {
|
|||
if (token && token.trim()) {
|
||||
// Ensure headers object exists
|
||||
if (!config.headers) {
|
||||
config.headers = {}
|
||||
config.headers = {} as InternalAxiosRequestConfig['headers']
|
||||
}
|
||||
const trimmedToken = token.trim()
|
||||
// Set Authorization header - axios will normalize it to lowercase for HTTP
|
||||
|
|
@ -61,14 +61,15 @@ const addAuthToken = (config: any) => {
|
|||
let isLoggingOut = false
|
||||
|
||||
// Response interceptor for error handling
|
||||
const handleAuthError = (error: any) => {
|
||||
if (error.response?.status === 401) {
|
||||
const handleAuthError = (error: unknown) => {
|
||||
const axiosError = error as AxiosError
|
||||
if (axiosError.response?.status === 401) {
|
||||
// Check if we're already on login page
|
||||
const currentPath = window.location.pathname
|
||||
const isPublicAuthEndpoint =
|
||||
error.config?.url?.includes('/admin/auth/request-code') ||
|
||||
error.config?.url?.includes('/admin/auth/verify-code') ||
|
||||
error.config?.url?.includes('/admin/auth/code-status')
|
||||
axiosError.config?.url?.includes('/admin/auth/request-code') ||
|
||||
axiosError.config?.url?.includes('/admin/auth/verify-code') ||
|
||||
axiosError.config?.url?.includes('/admin/auth/code-status')
|
||||
|
||||
// Don't logout for public auth endpoints or if already on login page
|
||||
if (currentPath === '/login' || isPublicAuthEndpoint) {
|
||||
|
|
|
|||
|
|
@ -33,4 +33,5 @@ function Badge({ className, variant, ...props }: BadgeProps) {
|
|||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Badge, badgeVariants }
|
||||
|
|
|
|||
|
|
@ -55,4 +55,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Button, buttonVariants }
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import * as React from "react"
|
|||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import * as React from "react"
|
|||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|||
import { toast } from 'sonner'
|
||||
import { cardsApi } from '@/api/cards'
|
||||
import type { GameCardDto, PaginatedResponse } from '@/types/models'
|
||||
import type { AxiosError } from 'axios'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
|
@ -71,8 +72,9 @@ export default function CardsPage() {
|
|||
toast.success('Card created successfully')
|
||||
closeDialog()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to create card')
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to create card')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -83,8 +85,9 @@ export default function CardsPage() {
|
|||
toast.success('Card updated successfully')
|
||||
closeDialog()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to update card')
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to update card')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -96,8 +99,9 @@ export default function CardsPage() {
|
|||
setIsDeleteDialogOpen(false)
|
||||
setCardToDelete(null)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to delete card')
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to delete card')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|||
import { toast } from 'sonner'
|
||||
import { packsApi } from '@/api/packs'
|
||||
import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse } from '@/types/models'
|
||||
import type { AxiosError } from 'axios'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
|
@ -77,8 +78,9 @@ export default function PacksPage() {
|
|||
toast.success('Pack created successfully')
|
||||
closeDialog()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to create pack')
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to create pack')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -89,8 +91,9 @@ export default function PacksPage() {
|
|||
toast.success('Pack updated successfully')
|
||||
closeDialog()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to update pack')
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to update pack')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -102,8 +105,9 @@ export default function PacksPage() {
|
|||
setIsDeleteDialogOpen(false)
|
||||
setPackToDelete(null)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to delete pack')
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to delete pack')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -141,7 +145,7 @@ export default function PacksPage() {
|
|||
order: fullPack.order || 0,
|
||||
})
|
||||
setIsDialogOpen(true)
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error('Failed to load pack details')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|||
import { toast } from 'sonner'
|
||||
import { usersApi } from '@/api/users'
|
||||
import type { UserDto, PaginatedResponse, PaymentDto } from '@/types/models'
|
||||
import type { AxiosError } from 'axios'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
|
@ -71,8 +72,9 @@ export default function UsersPage() {
|
|||
toast.success('User updated successfully')
|
||||
closeEditDialog()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to update user')
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to update user')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -84,8 +86,9 @@ export default function UsersPage() {
|
|||
setIsDeleteDialogOpen(false)
|
||||
setUserToDelete(null)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.message || 'Failed to delete user')
|
||||
onError: (error: unknown) => {
|
||||
const axiosError = error as AxiosError<{ message?: string }>
|
||||
toast.error(axiosError.response?.data?.message || 'Failed to delete user')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -112,7 +115,7 @@ export default function UsersPage() {
|
|||
setUserPurchases(purchases)
|
||||
setSelectedUser(user)
|
||||
setIsPurchasesDialogOpen(true)
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error('Failed to load user purchases')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ export interface UserDto {
|
|||
purchases: string[]
|
||||
subscription?: boolean
|
||||
subscriptionFeatures: string[]
|
||||
userDataDto?: any
|
||||
userSettingsDto?: any
|
||||
userDataDto?: unknown
|
||||
userSettingsDto?: unknown
|
||||
}
|
||||
|
||||
export interface PaymentDto {
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
|
|
@ -32,7 +33,7 @@ extension PaymentToDto on Payment {
|
|||
),
|
||||
externalToken: externalToken,
|
||||
meta: meta,
|
||||
date: date,
|
||||
date: date.dateTime,
|
||||
products: productsList,
|
||||
);
|
||||
}
|
||||
|
|
@ -52,7 +53,7 @@ extension PaymentFromDto on PaymentDto {
|
|||
paymentSystem: paymentSystem.name,
|
||||
externalToken: drift.Value(externalToken),
|
||||
meta: drift.Value(meta),
|
||||
date: drift.Value(date),
|
||||
date: drift.Value(PgDateTime(date)),
|
||||
products: drift.Value(productsJson),
|
||||
);
|
||||
}
|
||||
|
|
@ -65,7 +66,7 @@ extension PaymentUpdate on PaymentDto {
|
|||
status: drift.Value(status.name),
|
||||
externalToken: drift.Value(externalToken),
|
||||
meta: drift.Value(meta),
|
||||
updatedAt: drift.Value(DateTime.now()),
|
||||
updatedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:developer';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/api/purchase/google_play_purchase_handler.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
|
@ -69,8 +70,8 @@ class PaymentManager {
|
|||
await _db.subscriptionDao.createUserSubscription(
|
||||
UserSubscriptionsCompanion.insert(
|
||||
userId: userId,
|
||||
start: now,
|
||||
finish: endDate,
|
||||
start: PgDateTime(now),
|
||||
finish: PgDateTime(endDate),
|
||||
features: drift.Value(plan.features as List<dynamic>),
|
||||
),
|
||||
);
|
||||
|
|
@ -149,8 +150,8 @@ class PaymentManager {
|
|||
await _db.subscriptionDao.createUserSubscription(
|
||||
UserSubscriptionsCompanion.insert(
|
||||
userId: payment.userId,
|
||||
start: now,
|
||||
finish: endDate,
|
||||
start: PgDateTime(now),
|
||||
finish: PgDateTime(endDate),
|
||||
features: drift.Value(plan.features),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
|
@ -25,8 +26,8 @@ class SubscriptionManager {
|
|||
return SubscriptionDto(
|
||||
page: null,
|
||||
isActive: subscription != null,
|
||||
start: subscription?.start,
|
||||
finish: subscription?.finish,
|
||||
start: subscription?.start.dateTime,
|
||||
finish: subscription?.finish.dateTime,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -41,8 +42,8 @@ class SubscriptionManager {
|
|||
await _db.subscriptionDao.createUserSubscription(
|
||||
UserSubscriptionsCompanion.insert(
|
||||
userId: user.id!,
|
||||
start: now,
|
||||
finish: endDate,
|
||||
start: PgDateTime(now),
|
||||
finish: PgDateTime(endDate),
|
||||
features: const Value([]),
|
||||
),
|
||||
);
|
||||
|
|
@ -86,8 +87,8 @@ class SubscriptionManager {
|
|||
await _db.subscriptionDao.createUserSubscription(
|
||||
UserSubscriptionsCompanion.insert(
|
||||
userId: userId,
|
||||
start: now,
|
||||
finish: endDate,
|
||||
start: PgDateTime(now),
|
||||
finish: PgDateTime(endDate),
|
||||
features: Value(plan.features as List<dynamic>),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class AdminAnalyticsApiV2 {
|
|||
'id': u.id,
|
||||
'name': u.name,
|
||||
'email': u.email,
|
||||
'createdAt': u.createdAt.toIso8601String(),
|
||||
'createdAt': u.createdAt.dateTime.toIso8601String(),
|
||||
})
|
||||
.toList();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
|
@ -41,9 +42,9 @@ class AdminCardsApiV2 {
|
|||
'image': card.image,
|
||||
'back': card.back,
|
||||
'transcription': card.transcription,
|
||||
'createdAt': card.createdAt.toIso8601String(),
|
||||
'createdAt': card.createdAt.dateTime.toIso8601String(),
|
||||
}).toList(),
|
||||
'total': total,
|
||||
'total': total,
|
||||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
|
@ -83,8 +84,8 @@ class AdminCardsApiV2 {
|
|||
'image': card.image,
|
||||
'back': card.back,
|
||||
'transcription': card.transcription,
|
||||
'createdAt': card.createdAt.toIso8601String(),
|
||||
'updatedAt': card.updatedAt?.toIso8601String(),
|
||||
'createdAt': card.createdAt.dateTime.toIso8601String(),
|
||||
'updatedAt': card.updatedAt?.dateTime.toIso8601String(),
|
||||
}),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
|
@ -155,7 +156,7 @@ class AdminCardsApiV2 {
|
|||
image: data['image'] ?? existing.image,
|
||||
back: data['back'] ?? existing.back,
|
||||
transcription: data['transcription'] ?? existing.transcription,
|
||||
updatedAt: DateTime.now(),
|
||||
updatedAt: PgDateTime(DateTime.now()),
|
||||
);
|
||||
|
||||
await _db.packDao.updateCard(updated);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:io';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
|
@ -245,7 +246,7 @@ class JwtService {
|
|||
RefreshTokensCompanion.insert(
|
||||
jti: jti,
|
||||
userId: userId,
|
||||
expiresAt: expiresAt, // required field - raw DateTime
|
||||
expiresAt: PgDateTime(expiresAt), // required field - raw DateTime
|
||||
// createdAt and isBlacklisted use defaults from table
|
||||
),
|
||||
);
|
||||
|
|
@ -260,7 +261,7 @@ class JwtService {
|
|||
}
|
||||
|
||||
// Check if expired
|
||||
if (token.expiresAt.isBefore(DateTime.now())) {
|
||||
if (token.expiresAt.dateTime.isBefore(DateTime.now())) {
|
||||
return true; // Expired tokens are considered invalid
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'dart:developer';
|
|||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
|
||||
import 'package:mnemo_cards_backend/tasks/task_manager.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
|
|
@ -104,9 +103,9 @@ class TasksApiV2 {
|
|||
'difficulty': t.difficulty,
|
||||
'status': t.status,
|
||||
'rewards': t.rewards,
|
||||
'createdAt': t.createdAt.toIso8601String(),
|
||||
'expiresAt': t.expiresAt.toIso8601String(),
|
||||
'completedAt': t.completedAt?.toIso8601String(),
|
||||
'createdAt': t.createdAt.dateTime.toIso8601String(),
|
||||
'expiresAt': t.expiresAt.dateTime.toIso8601String(),
|
||||
'completedAt': t.completedAt?.dateTime.toIso8601String(),
|
||||
'proofUrl': t.proofUrl,
|
||||
'instructions': t.instructions,
|
||||
'tags': t.tags,
|
||||
|
|
@ -146,9 +145,9 @@ class TasksApiV2 {
|
|||
'difficulty': task.difficulty,
|
||||
'status': task.status,
|
||||
'rewards': task.rewards,
|
||||
'createdAt': task.createdAt.toIso8601String(),
|
||||
'expiresAt': task.expiresAt.toIso8601String(),
|
||||
'completedAt': task.completedAt?.toIso8601String(),
|
||||
'createdAt': task.createdAt.dateTime.toIso8601String(),
|
||||
'expiresAt': task.expiresAt.dateTime.toIso8601String(),
|
||||
'completedAt': task.completedAt?.dateTime.toIso8601String(),
|
||||
'proofUrl': task.proofUrl,
|
||||
'instructions': task.instructions,
|
||||
'tags': task.tags,
|
||||
|
|
@ -212,8 +211,8 @@ class TasksApiV2 {
|
|||
'progress': progress.map((p) => {
|
||||
'taskId': p.taskId,
|
||||
'progress': p.progress,
|
||||
'startedAt': p.startedAt.toIso8601String(),
|
||||
'updatedAt': p.updatedAt.toIso8601String(),
|
||||
'startedAt': p.startedAt.dateTime.toIso8601String(),
|
||||
'updatedAt': p.updatedAt.dateTime.toIso8601String(),
|
||||
}).toList()
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_backend/main.dart' as backend_main;
|
||||
|
|
@ -115,8 +116,8 @@ class TelegramBotApiV2 {
|
|||
final sharesCount = await (_db.selectOnly(_db.shareRequests)
|
||||
..addColumns([countExpr])
|
||||
..where(_db.shareRequests.telegramUserId.equals(telegramUserId))
|
||||
..where(_db.shareRequests.requestedAt.isBiggerOrEqualValue(todayStart))
|
||||
..where(_db.shareRequests.requestedAt.isSmallerThanValue(todayEnd)))
|
||||
..where(_db.shareRequests.requestedAt.isBiggerOrEqualValue(PgDateTime(todayStart)))
|
||||
..where(_db.shareRequests.requestedAt.isSmallerThanValue(PgDateTime(todayEnd))))
|
||||
.map((row) => row.read(countExpr)!)
|
||||
.getSingle();
|
||||
|
||||
|
|
@ -155,7 +156,7 @@ class TelegramBotApiV2 {
|
|||
telegramUserId: telegramUserId,
|
||||
telegramUsername: Value(telegramUsername),
|
||||
sharedCardId: Value(sharedCardId),
|
||||
requestedAt: Value(requestedAt),
|
||||
requestedAt: Value(PgDateTime(requestedAt)),
|
||||
);
|
||||
|
||||
await _db.into(_db.shareRequests).insert(companion);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/api/authorize/helpers.dart';
|
||||
import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart';
|
||||
|
|
@ -117,7 +118,7 @@ class UsersApiV2 {
|
|||
id: drift.Value(user.id!),
|
||||
name: name != null ? drift.Value(name) : const drift.Value.absent(),
|
||||
email: email != null ? drift.Value(email) : const drift.Value.absent(),
|
||||
updatedAt: drift.Value(DateTime.now()),
|
||||
updatedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -518,8 +519,8 @@ class UsersApiV2 {
|
|||
totalStudyTimeMinutes: drift.Value(totalStudyTime),
|
||||
currentStreak: drift.Value(currentStreak),
|
||||
longestStreak: drift.Value(longestStreak),
|
||||
lastTimeOnline: drift.Value(now),
|
||||
updatedAt: drift.Value(now),
|
||||
lastTimeOnline: drift.Value(PgDateTime(now)),
|
||||
updatedAt: drift.Value(PgDateTime(now)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:mnemo_cards_backend/user/admin_ids_service.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
|
|
@ -41,7 +42,7 @@ class CheckAdminsTask with task.Task {
|
|||
UsersCompanion(
|
||||
id: Value(user.id),
|
||||
admin: const Value(true),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:mnemo_cards_backend/cron/task.dart' as cron_task;
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||
|
||||
class TasksSeederTask with cron_task.Task {
|
||||
|
|
@ -19,10 +21,13 @@ class TasksSeederTask with cron_task.Task {
|
|||
final now = DateTime.now();
|
||||
final weekFromNow = now.add(const Duration(days: 7));
|
||||
|
||||
final tasks = [
|
||||
// App Internal Tasks
|
||||
UserTaskModel(
|
||||
title: 'Пройди 3 теста сегодня',
|
||||
// Используем предопределенные UUID для каждой задачи
|
||||
// Это позволяет избежать дубликатов при повторных запусках
|
||||
final tasksWithIds = [
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000001',
|
||||
task: UserTaskModel(
|
||||
title: 'Пройди 3 теста сегодня',
|
||||
description:
|
||||
'Заверши три тестовые сессии в приложении для улучшения навыков испанского.',
|
||||
type: 'app_internal',
|
||||
|
|
@ -36,9 +41,11 @@ class TasksSeederTask with cron_task.Task {
|
|||
expiresAt: weekFromNow,
|
||||
tags: ['tests', 'practice', 'daily'],
|
||||
),
|
||||
|
||||
UserTaskModel(
|
||||
title: 'Достигни 80% правильных ответов в тесте',
|
||||
),
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000002',
|
||||
task: UserTaskModel(
|
||||
title: 'Достигни 80% правильных ответов в тесте',
|
||||
description:
|
||||
'Пройди тест с результатом не менее 80% правильных ответов.',
|
||||
type: 'app_internal',
|
||||
|
|
@ -52,10 +59,12 @@ class TasksSeederTask with cron_task.Task {
|
|||
expiresAt: weekFromNow,
|
||||
tags: ['tests', 'accuracy', 'challenge'],
|
||||
),
|
||||
|
||||
),
|
||||
// External Tasks
|
||||
UserTaskModel(
|
||||
title: 'Подпишись на Telegram канал',
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000003',
|
||||
task: UserTaskModel(
|
||||
title: 'Подпишись на Telegram канал',
|
||||
description:
|
||||
'Подпишись на наш Telegram канал @mnemo_cards для получения обновлений и советов по изучению испанского.',
|
||||
type: 'external',
|
||||
|
|
@ -69,9 +78,11 @@ class TasksSeederTask with cron_task.Task {
|
|||
expiresAt: weekFromNow,
|
||||
tags: ['telegram', 'social', 'subscription'],
|
||||
),
|
||||
|
||||
UserTaskModel(
|
||||
title: 'Закажи еду на испанском',
|
||||
),
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000004',
|
||||
task: UserTaskModel(
|
||||
title: 'Закажи еду на испанском',
|
||||
description:
|
||||
'Сделай заказ в ресторане или кафе, используя испанский язык. Запиши процесс на видео для подтверждения.',
|
||||
type: 'external',
|
||||
|
|
@ -91,9 +102,11 @@ class TasksSeederTask with cron_task.Task {
|
|||
'Запиши видео, где ты делаешь заказ на испанском. Убедись, что официант понимает тебя.',
|
||||
tags: ['speaking', 'restaurant', 'video', 'real_world'],
|
||||
),
|
||||
|
||||
UserTaskModel(
|
||||
title: 'Посмотри испанский фильм с субтитрами',
|
||||
),
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000005',
|
||||
task: UserTaskModel(
|
||||
title: 'Посмотри испанский фильм с субтитрами',
|
||||
description:
|
||||
'Посмотри испанский фильм или сериал с испанскими субтитрами в течение 30 минут.',
|
||||
type: 'external',
|
||||
|
|
@ -107,10 +120,12 @@ class TasksSeederTask with cron_task.Task {
|
|||
expiresAt: weekFromNow,
|
||||
tags: ['listening', 'media', 'subtitles'],
|
||||
),
|
||||
|
||||
),
|
||||
// Social Tasks
|
||||
UserTaskModel(
|
||||
title: 'Поделись прогрессом в соцсетях',
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000006',
|
||||
task: UserTaskModel(
|
||||
title: 'Поделись прогрессом в соцсетях',
|
||||
description:
|
||||
'Поделись своим прогрессом в изучении испанского в социальных сетях с хэштегом #MnemoCards.',
|
||||
type: 'social',
|
||||
|
|
@ -124,9 +139,11 @@ class TasksSeederTask with cron_task.Task {
|
|||
expiresAt: weekFromNow,
|
||||
tags: ['social', 'sharing', 'progress'],
|
||||
),
|
||||
|
||||
UserTaskModel(
|
||||
title: 'Найди партнёра для практики',
|
||||
),
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000007',
|
||||
task: UserTaskModel(
|
||||
title: 'Найди партнёра для практики',
|
||||
description:
|
||||
'Найди собеседника для практики испанского языка (друга, коллегу или через языковые приложения).',
|
||||
type: 'social',
|
||||
|
|
@ -140,10 +157,12 @@ class TasksSeederTask with cron_task.Task {
|
|||
expiresAt: weekFromNow.add(const Duration(days: 7)), // 2 weeks
|
||||
tags: ['conversation', 'partner', 'practice'],
|
||||
),
|
||||
|
||||
),
|
||||
// Additional App Tasks
|
||||
UserTaskModel(
|
||||
title: 'Изучай слова ежедневно 7 дней подряд',
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000008',
|
||||
task: UserTaskModel(
|
||||
title: 'Изучай слова ежедневно 7 дней подряд',
|
||||
description:
|
||||
'Учи новые слова в приложении каждый день в течение недели.',
|
||||
type: 'app_internal',
|
||||
|
|
@ -161,9 +180,11 @@ class TasksSeederTask with cron_task.Task {
|
|||
expiresAt: weekFromNow.add(const Duration(days: 21)), // 4 weeks
|
||||
tags: ['vocabulary', 'daily', 'streak', 'consistency'],
|
||||
),
|
||||
|
||||
UserTaskModel(
|
||||
title: 'Пройди тест по грамматике',
|
||||
),
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000009',
|
||||
task: UserTaskModel(
|
||||
title: 'Пройди тест по грамматике',
|
||||
description:
|
||||
'Заверши специализированный тест по грамматике испанского языка.',
|
||||
type: 'app_internal',
|
||||
|
|
@ -177,9 +198,11 @@ class TasksSeederTask with cron_task.Task {
|
|||
expiresAt: weekFromNow,
|
||||
tags: ['grammar', 'tests', 'knowledge'],
|
||||
),
|
||||
|
||||
UserTaskModel(
|
||||
title: 'Практикуй произношение',
|
||||
),
|
||||
(
|
||||
id: '00000000-0000-0000-0001-000000000010',
|
||||
task: UserTaskModel(
|
||||
title: 'Практикуй произношение',
|
||||
description:
|
||||
'Заверши урок по произношению или запиши себя, читая текст на испанском.',
|
||||
type: 'app_internal',
|
||||
|
|
@ -193,24 +216,28 @@ class TasksSeederTask with cron_task.Task {
|
|||
expiresAt: weekFromNow,
|
||||
tags: ['pronunciation', 'speaking', 'practice'],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
// Convert UserTaskModel to UserTasksCompanion and insert
|
||||
// Используем InsertMode.insertOrIgnore для автоматического игнорирования дубликатов через ON CONFLICT
|
||||
for (final taskModel in tasks) {
|
||||
// PostgreSQL автоматически генерирует UUID через DEFAULT gen_random_uuid()
|
||||
// Используем InsertMode.insertOrIgnore с предопределенными UUID для идемпотентности
|
||||
for (final taskData in tasksWithIds) {
|
||||
final taskModel = taskData.task;
|
||||
final rewardsJson = taskModel.rewards.map((r) => r.toJson()).toList();
|
||||
|
||||
await _db.into(_db.userTasks).insert(
|
||||
UserTasksCompanion.insert(
|
||||
id: Value(taskData.id), // Предопределенный UUID для идемпотентности seeder'а
|
||||
title: taskModel.title,
|
||||
description: taskModel.description,
|
||||
type: taskModel.type,
|
||||
difficulty: taskModel.difficulty,
|
||||
status: taskModel.status,
|
||||
rewards: Value(rewardsJson),
|
||||
createdAt: Value(taskModel.createdAt),
|
||||
expiresAt: taskModel.expiresAt,
|
||||
completedAt: Value(taskModel.completedAt),
|
||||
createdAt: Value(PgDateTime(taskModel.createdAt)),
|
||||
expiresAt: PgDateTime(taskModel.expiresAt),
|
||||
completedAt: Value(taskModel.completedAt?.let(PgDateTime.new)),
|
||||
proofUrl: Value(taskModel.proofUrl),
|
||||
instructions: Value(taskModel.instructions),
|
||||
tags: Value(taskModel.tags),
|
||||
|
|
@ -220,7 +247,7 @@ class TasksSeederTask with cron_task.Task {
|
|||
);
|
||||
}
|
||||
|
||||
print('Successfully seeded ${tasks.length} user tasks (duplicates ignored)');
|
||||
print('Successfully seeded ${tasksWithIds.length} user tasks (duplicates ignored)');
|
||||
} catch (e) {
|
||||
print('Error seeding tasks: $e');
|
||||
rethrow;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
import '../tables/achievements.dart';
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ class AchievementDao extends DatabaseAccessor<AppDatabase> with _$AchievementDao
|
|||
..where((ua) => ua.userId.equals(userId) & ua.achievementId.equals(achievementId))
|
||||
).write(UserAchievementsCompanion(
|
||||
progress: Value(progress),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
return count > 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
import '../tables/discounts.dart';
|
||||
import '../tables/users.dart';
|
||||
|
|
@ -18,7 +19,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
|
|||
|
||||
/// Получить все активные кампании
|
||||
Future<List<DiscountCampaign>> getActiveCampaigns() {
|
||||
final now = DateTime.now();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
return (select(db.discountCampaigns)
|
||||
..where((c) => c.status.equals('active'))
|
||||
..where((c) => c.start.isSmallerOrEqualValue(now))
|
||||
|
|
@ -44,7 +45,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
|
|||
..where((c) => c.id.equals(campaignId))
|
||||
).write(DiscountCampaignsCompanion(
|
||||
status: Value(status),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
|
|||
..where((c) => c.id.equals(campaignId))
|
||||
).write(DiscountCampaignsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +172,7 @@ class DiscountDao extends DatabaseAccessor<AppDatabase> with _$DiscountDaoMixin
|
|||
String? productType,
|
||||
String? productId,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
final query = select(db.discountCampaigns)
|
||||
..where((c) => c.status.equals('active'))
|
||||
..where((c) => c.start.isSmallerOrEqualValue(now))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
import '../tables/packs.dart';
|
||||
import '../tables/relations.dart';
|
||||
|
|
@ -60,7 +61,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
|
|||
if (packId == null) throw ArgumentError('Pack ID is required');
|
||||
|
||||
return (update(cardPacks)..where((p) => p.id.equals(packId)))
|
||||
.write(updates.copyWith(updatedAt: Value(DateTime.now())));
|
||||
.write(updates.copyWith(updatedAt: Value(PgDateTime(DateTime.now()))));
|
||||
}
|
||||
|
||||
/// Удалить пак (soft delete)
|
||||
|
|
@ -68,7 +69,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
|
|||
return (update(cardPacks)..where((p) => p.id.equals(packId)))
|
||||
.write(CardPacksCompanion(
|
||||
isDeleted: const Value(true),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +138,7 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
|
|||
return (update(gameCards)..where((c) => c.id.equals(cardId)))
|
||||
.write(GameCardsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
import '../tables/payments.dart';
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ class PaymentDao extends DatabaseAccessor<AppDatabase> with _$PaymentDaoMixin {
|
|||
'SELECT id FROM payments WHERE user_id = ? AND date = ? AND amount = ? ORDER BY created_at DESC LIMIT 1',
|
||||
variables: [
|
||||
Variable.withString(payment.userId.value),
|
||||
Variable.withDateTime(inserted.date),
|
||||
Variable.withDateTime(inserted.date.dateTime),
|
||||
Variable.withString(inserted.amount),
|
||||
],
|
||||
readsFrom: {payments},
|
||||
|
|
@ -81,7 +82,7 @@ class PaymentDao extends DatabaseAccessor<AppDatabase> with _$PaymentDaoMixin {
|
|||
return (update(payments)..where((p) => p.id.equals(paymentId)))
|
||||
.write(PaymentsCompanion(
|
||||
status: Value(status),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
import '../tables/promo_codes.dart';
|
||||
import '../tables/users.dart';
|
||||
|
|
@ -18,7 +19,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
|
|||
|
||||
/// Получить все активные кампании
|
||||
Future<List<PromoCodesCampaign>> getActiveCampaigns() {
|
||||
final now = DateTime.now();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
return (select(db.promoCodesCampaigns)
|
||||
..where((c) => c.status.equals('active'))
|
||||
..where((c) => c.start.isSmallerOrEqualValue(now))
|
||||
|
|
@ -44,7 +45,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
|
|||
..where((c) => c.id.equals(campaignId))
|
||||
).write(PromoCodesCampaignsCompanion(
|
||||
status: Value(status),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +110,7 @@ class PromoCodeDao extends DatabaseAccessor<AppDatabase> with _$PromoCodeDaoMixi
|
|||
if (code != null) {
|
||||
await update(db.promoCodes).replace(code.copyWith(
|
||||
activations: code.activations + 1,
|
||||
updatedAt: DateTime.now(),
|
||||
updatedAt: PgDateTime(DateTime.now()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
import '../tables/statistics.dart';
|
||||
import '../tables/users.dart';
|
||||
|
|
@ -42,10 +43,10 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
|
|||
..orderBy([(s) => OrderingTerm.desc(s.startTime)]);
|
||||
|
||||
if (fromDate != null) {
|
||||
query.where((s) => s.startTime.isBiggerOrEqualValue(fromDate));
|
||||
query.where((s) => s.startTime.isBiggerOrEqualValue(PgDateTime(fromDate)));
|
||||
}
|
||||
if (toDate != null) {
|
||||
query.where((s) => s.startTime.isSmallerOrEqualValue(toDate));
|
||||
query.where((s) => s.startTime.isSmallerOrEqualValue(PgDateTime(toDate)));
|
||||
}
|
||||
|
||||
if (limit != null) {
|
||||
|
|
@ -74,8 +75,8 @@ class StatisticsDao extends DatabaseAccessor<AppDatabase> with _$StatisticsDaoMi
|
|||
}) {
|
||||
final updates = StudySessionsCompanion(
|
||||
id: Value(sessionId),
|
||||
endTime: Value(DateTime.now()),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
endTime: Value(PgDateTime(DateTime.now())),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
wordsLearned: wordsLearned != null ? Value(wordsLearned) : const Value.absent(),
|
||||
testsCompleted: testsCompleted != null ? Value(testsCompleted) : const Value.absent(),
|
||||
accuracy: accuracy != null ? Value(accuracy) : const Value.absent(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
import '../tables/subscriptions.dart';
|
||||
import '../tables/users.dart';
|
||||
|
|
@ -46,7 +47,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
|
|||
|
||||
/// Получить активную подписку пользователя
|
||||
Future<UserSubscription?> getActiveSubscription(String userId) async {
|
||||
final now = DateTime.now();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
return (select(userSubscriptions)
|
||||
..where((us) => us.userId.equals(userId))
|
||||
..where((us) => us.start.isSmallerThanValue(now))
|
||||
|
|
@ -81,7 +82,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
|
|||
|
||||
/// Получить всех пользователей с активными подписками
|
||||
Future<List<UserSubscription>> getActiveSubscriptions() async {
|
||||
final now = DateTime.now();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
return (select(userSubscriptions)
|
||||
..where((us) => us.start.isSmallerThanValue(now))
|
||||
..where((us) => us.finish.isBiggerThanValue(now))
|
||||
|
|
@ -100,7 +101,7 @@ class SubscriptionDao extends DatabaseAccessor<AppDatabase> with _$SubscriptionD
|
|||
if (subscription != null) {
|
||||
await update(userSubscriptions).replace(
|
||||
subscription.copyWith(
|
||||
finish: DateTime.now(),
|
||||
finish: PgDateTime(DateTime.now()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
|
||||
part 'task_dao.g.dart';
|
||||
|
|
@ -32,10 +33,11 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
|
|||
|
||||
/// Обновить время последнего выполнения
|
||||
Future<void> updateLastExecution(String taskId) {
|
||||
final now = PgDateTime(DateTime.now());
|
||||
return (update(db.tasks)..where((t) => t.id.equals(taskId)))
|
||||
.write(TasksCompanion(
|
||||
lastExecution: Value(DateTime.now()),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
lastExecution: Value(now),
|
||||
updatedAt: Value(now),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +68,7 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
|
|||
}
|
||||
|
||||
if (activeOnly) {
|
||||
final now = DateTime.now();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
query.where((ut) => ut.expiresAt.isBiggerThanValue(now));
|
||||
}
|
||||
|
||||
|
|
@ -88,11 +90,12 @@ class TaskDao extends DatabaseAccessor<AppDatabase> with _$TaskDaoMixin {
|
|||
|
||||
/// Завершить задачу пользователя
|
||||
Future<void> completeUserTask(String taskId) {
|
||||
final now = PgDateTime(DateTime.now());
|
||||
return (update(db.userTasks)..where((ut) => ut.id.equals(taskId)))
|
||||
.write(UserTasksCompanion(
|
||||
status: const Value('completed'),
|
||||
completedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
completedAt: Value(now),
|
||||
updatedAt: Value(now),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
import '../tables/tests.dart';
|
||||
import '../tables/packs.dart';
|
||||
|
|
@ -52,7 +53,7 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
|
|||
return (update(tests)..where((t) => t.id.equals(testId)))
|
||||
.write(TestsCompanion(
|
||||
isDeleted: const Value(true),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../database.dart';
|
||||
import '../tables/users.dart';
|
||||
import '../tables/auth.dart';
|
||||
|
|
@ -82,7 +83,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
return (update(users)..where((u) => u.id.equals(userId)))
|
||||
.write(UsersCompanion(
|
||||
isDeleted: const Value(true),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
updatedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +116,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
|
||||
/// Получить пользователей с активной подпиской
|
||||
Stream<List<User>> watchUsersWithActiveSubscription() {
|
||||
final now = DateTime.now();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
final query = select(users).join([
|
||||
innerJoin(
|
||||
db.userSubscriptions,
|
||||
|
|
@ -155,15 +156,16 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
if (userId == null) throw ArgumentError('User ID is required');
|
||||
|
||||
return (update(userDatas)..where((ud) => ud.userId.equals(userId)))
|
||||
.write(updates.copyWith(updatedAt: Value(DateTime.now())));
|
||||
.write(updates.copyWith(updatedAt: Value(PgDateTime(DateTime.now()))));
|
||||
}
|
||||
|
||||
/// Обновить время последнего визита
|
||||
Future<void> updateLastOnline(String userId) async {
|
||||
final now = PgDateTime(DateTime.now());
|
||||
await (update(userDatas)..where((ud) => ud.userId.equals(userId)))
|
||||
.write(UserDatasCompanion(
|
||||
lastTimeOnline: Value(DateTime.now()),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
lastTimeOnline: Value(now),
|
||||
updatedAt: Value(now),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +190,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
Future<Token?> getTokenByUserId(String userId) {
|
||||
return (select(tokens)
|
||||
..where((t) => t.userId.equals(userId))
|
||||
..where((t) => t.expires.isBiggerThanValue(DateTime.now()))
|
||||
..where((t) => t.expires.isBiggerThanValue(PgDateTime(DateTime.now())))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.created)])
|
||||
).getSingleOrNull();
|
||||
}
|
||||
|
|
@ -212,7 +214,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
/// Удалить истекшие токены
|
||||
Future<int> deleteExpiredTokens() {
|
||||
return (delete(tokens)
|
||||
..where((t) => t.expires.isSmallerThanValue(DateTime.now()))
|
||||
..where((t) => t.expires.isSmallerThanValue(PgDateTime(DateTime.now())))
|
||||
).go();
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +231,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
return (select(refreshTokens)
|
||||
..where((rt) => rt.userId.equals(userId))
|
||||
..where((rt) => rt.isBlacklisted.equals(false))
|
||||
..where((rt) => rt.expiresAt.isBiggerThanValue(DateTime.now()))
|
||||
..where((rt) => rt.expiresAt.isBiggerThanValue(PgDateTime(DateTime.now())))
|
||||
..orderBy([(rt) => OrderingTerm.desc(rt.createdAt)])
|
||||
).get();
|
||||
}
|
||||
|
|
@ -259,7 +261,7 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
/// Удалить истекшие refresh токены
|
||||
Future<int> deleteExpiredRefreshTokens() {
|
||||
return (delete(refreshTokens)
|
||||
..where((rt) => rt.expiresAt.isSmallerThanValue(DateTime.now()))
|
||||
..where((rt) => rt.expiresAt.isSmallerThanValue(PgDateTime(DateTime.now())))
|
||||
).go();
|
||||
}
|
||||
|
||||
|
|
@ -282,14 +284,14 @@ class UserDao extends DatabaseAccessor<AppDatabase> with _$UserDaoMixin {
|
|||
return (update(db.telegramAuthCodes)..where((ac) => ac.code.equals(code)))
|
||||
.write(TelegramAuthCodesCompanion(
|
||||
isUsed: const Value(true),
|
||||
usedAt: Value(DateTime.now()),
|
||||
usedAt: Value(PgDateTime(DateTime.now())),
|
||||
));
|
||||
}
|
||||
|
||||
/// Удалить истекшие коды
|
||||
Future<int> deleteExpiredAuthCodes() {
|
||||
return (delete(db.telegramAuthCodes)
|
||||
..where((ac) => ac.expiresAt.isSmallerThanValue(DateTime.now()))
|
||||
..where((ac) => ac.expiresAt.isSmallerThanValue(PgDateTime(DateTime.now())))
|
||||
).go();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import 'daos/promo_code_dao.dart';
|
|||
import 'daos/discount_dao.dart';
|
||||
import 'daos/statistics_dao.dart';
|
||||
import 'daos/achievement_dao.dart';
|
||||
import 'postgres_constants.dart';
|
||||
|
||||
// Сгенерированный код будет здесь
|
||||
part 'database.g.dart';
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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');
|
||||
|
|
@ -1,25 +1,31 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import '../postgres_constants.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'users.dart';
|
||||
|
||||
/// Таблица UserAchievements - достижения пользователей
|
||||
class UserAchievements extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get userId => text()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
// Achievement ID (from AchievementDefinitions)
|
||||
TextColumn get achievementId => text()();
|
||||
|
||||
// When achievement was unlocked
|
||||
DateTimeColumn get unlockedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get unlockedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
// Progress (0.0 to 1.0)
|
||||
RealColumn get progress => real().withDefault(const Constant(1.0))();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'users.dart';
|
||||
import '../converters.dart' show generateUuid;
|
||||
import '../postgres_constants.dart';
|
||||
|
||||
/// Таблица Tokens - токены авторизации пользователей
|
||||
class Tokens extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get userId => text()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get token => text().unique()();
|
||||
TextColumn get externalUserId => text()();
|
||||
|
||||
DateTimeColumn get created => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get expires => dateTime()();
|
||||
Column<PgDateTime> get created => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get expires => customType(PgTypes.timestampWithTimezone)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -25,17 +29,21 @@ class Tokens extends Table {
|
|||
|
||||
/// Таблица RefreshTokens - refresh токены для JWT
|
||||
class RefreshTokens extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get userId => text()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get jti => text().unique()(); // JWT ID
|
||||
|
||||
BoolColumn get isBlacklisted => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get expiresAt => dateTime()();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get expiresAt => customType(PgTypes.timestampWithTimezone)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -43,7 +51,9 @@ class RefreshTokens extends Table {
|
|||
|
||||
/// Таблица TelegramAuthCodes - коды для авторизации через Telegram
|
||||
class TelegramAuthCodes extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
TextColumn get code => text().unique()();
|
||||
TextColumn get telegramUserId => text()();
|
||||
|
|
@ -53,11 +63,13 @@ class TelegramAuthCodes extends Table {
|
|||
|
||||
BoolColumn get isUsed => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
DateTimeColumn get usedAt => dateTime().nullable()();
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
Column<PgDateTime> get usedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.nullable()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get expiresAt => dateTime()();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get expiresAt => customType(PgTypes.timestampWithTimezone)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../converters.dart' show generateUuid, StringListConverter, JsonListConverter;
|
||||
import '../postgres_constants.dart';
|
||||
import 'users.dart';
|
||||
|
||||
/// Таблица DiscountCampaigns - кампании скидок
|
||||
class DiscountCampaigns extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
TextColumn get name => text().nullable()();
|
||||
|
||||
DateTimeColumn get start => dateTime()();
|
||||
DateTimeColumn get finish => dateTime()();
|
||||
Column<PgDateTime> get start => customType(PgTypes.timestampWithTimezone)();
|
||||
Column<PgDateTime> get finish => customType(PgTypes.timestampWithTimezone)();
|
||||
|
||||
// Статус (enum as string)
|
||||
TextColumn get status => text()(); // created, active, expired, disabled
|
||||
|
|
@ -21,11 +23,13 @@ class DiscountCampaigns extends Table {
|
|||
.map(const StringListConverter())();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
BoolColumn get isDeleted => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -33,8 +37,11 @@ class DiscountCampaigns extends Table {
|
|||
|
||||
/// Таблица Discounts - скидки
|
||||
class Discounts extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get campaignId => text().references(DiscountCampaigns, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get campaignId => text()
|
||||
.references(DiscountCampaigns, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
// Процент скидки (0-100)
|
||||
RealColumn get discountPercent => real()();
|
||||
|
|
@ -45,11 +52,13 @@ class Discounts extends Table {
|
|||
.map(const JsonListConverter())();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
BoolColumn get isDeleted => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -57,10 +66,13 @@ class Discounts extends Table {
|
|||
|
||||
/// Junction table для связи Discounts ↔ UserDatas (many-to-many)
|
||||
class DiscountUserDatas extends Table {
|
||||
TextColumn get discountId => text().references(Discounts, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get discountId => text()
|
||||
.references(Discounts, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get userId => text()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
DateTimeColumn get grantedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get grantedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {discountId, userId};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../converters.dart' show generateUuid, IntListConverter, StringListConverter;
|
||||
import '../postgres_constants.dart';
|
||||
|
||||
/// Таблица CardPacks - наборы карточек
|
||||
class CardPacks extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
// Основная информация
|
||||
TextColumn get title => text()();
|
||||
|
|
@ -21,7 +23,7 @@ class CardPacks extends Table {
|
|||
IntColumn get order => integer().withDefault(const Constant(0))();
|
||||
BoolColumn get enabled => boolean()
|
||||
.withDefault(const Constant(true))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
// Порядок карточек (JSON array of IDs)
|
||||
TextColumn get cardsOrder => text()
|
||||
|
|
@ -38,11 +40,13 @@ class CardPacks extends Table {
|
|||
TextColumn get currency => text().withDefault(const Constant('RUB'))();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
BoolColumn get isDeleted => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -50,8 +54,11 @@ class CardPacks extends Table {
|
|||
|
||||
/// Таблица GameCards - карточки для изучения
|
||||
class GameCards extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get packId => text()
|
||||
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
// Основной контент
|
||||
TextColumn get original => text()(); // слово на иностранном языке
|
||||
|
|
@ -70,11 +77,13 @@ class GameCards extends Table {
|
|||
TextColumn get back => text().nullable()(); // дополнительный текст
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
BoolColumn get isDeleted => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -82,13 +91,17 @@ class GameCards extends Table {
|
|||
|
||||
/// Таблица VoiceModels - голосовые файлы для карточек
|
||||
class VoiceModels extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get cardId => text()
|
||||
.references(GameCards, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get voiceUrl => text()(); // URL аудиофайла
|
||||
TextColumn get language => text()(); // язык озвучки
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import '../converters.dart' show generateUuid, JsonListConverter, StringListConverter;
|
||||
import '../postgres_constants.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../converters.dart' show JsonListConverter, StringListConverter;
|
||||
import 'users.dart';
|
||||
|
||||
/// Таблица Payments - платежи
|
||||
class Payments extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get userId => text()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get amount => text()();
|
||||
TextColumn get currency => text()();
|
||||
|
|
@ -18,7 +21,8 @@ class Payments extends Table {
|
|||
TextColumn get externalToken => text().nullable()();
|
||||
TextColumn get meta => text().nullable()();
|
||||
|
||||
DateTimeColumn get date => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get date => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
// Продукты (JSON array)
|
||||
TextColumn get products => text()
|
||||
|
|
@ -31,11 +35,13 @@ class Payments extends Table {
|
|||
.map(const StringListConverter())();
|
||||
BoolColumn get subscription => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../converters.dart' show generateUuid, JsonListConverter, StringListConverter;
|
||||
import '../postgres_constants.dart';
|
||||
import 'users.dart';
|
||||
|
||||
/// Таблица PromoCodesCampaigns - кампании промокодов
|
||||
class PromoCodesCampaigns extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
TextColumn get template => text()();
|
||||
TextColumn get name => text().nullable()();
|
||||
|
|
@ -19,8 +21,8 @@ class PromoCodesCampaigns extends Table {
|
|||
IntColumn get activationsPerUser => integer()();
|
||||
IntColumn get generationSize => integer()();
|
||||
|
||||
DateTimeColumn get start => dateTime()();
|
||||
DateTimeColumn get finish => dateTime()();
|
||||
Column<PgDateTime> get start => customType(PgTypes.timestampWithTimezone)();
|
||||
Column<PgDateTime> get finish => customType(PgTypes.timestampWithTimezone)();
|
||||
|
||||
// Статус (enum as string)
|
||||
TextColumn get status => text()(); // created, preparing, ready, active, disabled
|
||||
|
|
@ -31,11 +33,13 @@ class PromoCodesCampaigns extends Table {
|
|||
.map(const StringListConverter())();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
BoolColumn get isDeleted => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -43,18 +47,25 @@ class PromoCodesCampaigns extends Table {
|
|||
|
||||
/// Таблица PromoCodes - промокоды
|
||||
class PromoCodes extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get campaignId => text().references(PromoCodesCampaigns, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get campaignId => text()
|
||||
.references(PromoCodesCampaigns, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get code => text().unique()();
|
||||
IntColumn get activations => integer().withDefault(const Constant(0))();
|
||||
|
||||
// Индивидуальный промокод (связан с пользователем)
|
||||
TextColumn get userId => text().nullable().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get userId => text()
|
||||
.nullable()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import '../postgres_constants.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'users.dart';
|
||||
import 'packs.dart' show CardPacks, GameCards, VoiceModels;
|
||||
|
||||
/// Junction table для связи Users ↔ CardPacks (многие ко многим)
|
||||
/// Хранит информацию о том, какие паки куплены пользователем
|
||||
class UserPacks extends Table {
|
||||
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get userId => text()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get packId => text()
|
||||
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
// Когда пользователь получил доступ к паку
|
||||
DateTimeColumn get grantedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get grantedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
// Как пользователь получил пак (purchase, promo, free, admin)
|
||||
TextColumn get grantType => text().withDefault(const Constant('purchase'))();
|
||||
|
|
@ -22,8 +25,10 @@ class UserPacks extends Table {
|
|||
/// Таблица PreviewCards - связь CardPacks с preview карточками
|
||||
/// Хранит какие карточки показывать в превью пака
|
||||
class PreviewCards extends Table {
|
||||
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get packId => text()
|
||||
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get cardId => text()
|
||||
.references(GameCards, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get order => integer().withDefault(const Constant(0))();
|
||||
|
||||
|
|
@ -34,8 +39,10 @@ class PreviewCards extends Table {
|
|||
/// Таблица CardPackCards - связь CardPacks с GameCards (many-to-many)
|
||||
/// Хранит какие карточки принадлежат какому паку
|
||||
class CardPackCards extends Table {
|
||||
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get packId => text()
|
||||
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get cardId => text()
|
||||
.references(GameCards, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get order => integer().withDefault(const Constant(0))();
|
||||
|
||||
|
|
@ -46,8 +53,10 @@ class CardPackCards extends Table {
|
|||
/// Таблица CardVoices - связь GameCards с VoiceModels (many-to-many)
|
||||
/// Хранит какие голосовые файлы принадлежат какой карточке
|
||||
class CardVoices extends Table {
|
||||
TextColumn get cardId => text().references(GameCards, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get voiceId => text().references(VoiceModels, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get cardId => text()
|
||||
.references(GameCards, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get voiceId => text()
|
||||
.references(VoiceModels, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {cardId, voiceId};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'users.dart';
|
||||
import '../converters.dart' show generateUuid;
|
||||
import '../postgres_constants.dart';
|
||||
|
||||
/// Таблица StudySessions - сессии изучения
|
||||
class StudySessions extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get userId => text()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
TextColumn get sessionId => text().nullable().unique()();
|
||||
|
||||
DateTimeColumn get startTime => dateTime()();
|
||||
DateTimeColumn get endTime => dateTime().nullable()();
|
||||
Column<PgDateTime> get startTime => customType(PgTypes.timestampWithTimezone)();
|
||||
Column<PgDateTime> get endTime => customType(PgTypes.timestampWithTimezone)
|
||||
.nullable()();
|
||||
|
||||
IntColumn get wordsLearned => integer().withDefault(const Constant(0))();
|
||||
IntColumn get testsCompleted => integer().withDefault(const Constant(0))();
|
||||
|
|
@ -21,8 +25,10 @@ class StudySessions extends Table {
|
|||
TextColumn get testId => text().nullable()();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../converters.dart' show JsonMapConverter, JsonListConverter;
|
||||
import '../postgres_constants.dart';
|
||||
import 'users.dart';
|
||||
|
||||
/// Таблица SubscriptionPlans - планы подписки
|
||||
class SubscriptionPlans extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
// UI информация (JSON)
|
||||
TextColumn get ui => text().nullable().map(const JsonMapConverter())();
|
||||
|
|
@ -26,11 +27,13 @@ class SubscriptionPlans extends Table {
|
|||
TextColumn get paymentSystem => text()(); // enum as string
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
BoolColumn get isDeleted => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -38,11 +41,15 @@ class SubscriptionPlans extends Table {
|
|||
|
||||
/// Таблица UserSubscriptions - подписки пользователей
|
||||
class UserSubscriptions extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get userId => text().unique().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get userId => text()
|
||||
.unique()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
DateTimeColumn get start => dateTime()();
|
||||
DateTimeColumn get finish => dateTime()();
|
||||
Column<PgDateTime> get start => customType(PgTypes.timestampWithTimezone)();
|
||||
Column<PgDateTime> get finish => customType(PgTypes.timestampWithTimezone)();
|
||||
|
||||
// Функции подписки (JSON array)
|
||||
TextColumn get features => text()
|
||||
|
|
@ -50,8 +57,10 @@ class UserSubscriptions extends Table {
|
|||
.map(const JsonListConverter())();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../converters.dart' show generateUuid, JsonListConverter, JsonMapConverter, StringListConverter;
|
||||
import '../postgres_constants.dart';
|
||||
import 'users.dart';
|
||||
|
||||
/// Таблица Tasks - задачи системы
|
||||
class Tasks extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
TextColumn get name => text()();
|
||||
IntColumn get minCycleMillis => integer()();
|
||||
|
|
@ -15,11 +17,14 @@ class Tasks extends Table {
|
|||
|
||||
TextColumn get status => text().nullable()();
|
||||
TextColumn get description => text().nullable()();
|
||||
DateTimeColumn get lastExecution => dateTime().nullable()();
|
||||
Column<PgDateTime> get lastExecution => customType(PgTypes.timestampWithTimezone)
|
||||
.nullable()();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -27,7 +32,9 @@ class Tasks extends Table {
|
|||
|
||||
/// Таблица UserTasks - задачи пользователей
|
||||
class UserTasks extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
TextColumn get title => text()();
|
||||
TextColumn get description => text()();
|
||||
|
|
@ -40,9 +47,11 @@ class UserTasks extends Table {
|
|||
.withDefault(const Constant('[]'))
|
||||
.map(const JsonListConverter())();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get expiresAt => dateTime()();
|
||||
DateTimeColumn get completedAt => dateTime().nullable()();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get expiresAt => customType(PgTypes.timestampWithTimezone)();
|
||||
Column<PgDateTime> get completedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.nullable()();
|
||||
|
||||
TextColumn get proofUrl => text().nullable()();
|
||||
TextColumn get instructions => text().nullable()();
|
||||
|
|
@ -55,7 +64,8 @@ class UserTasks extends Table {
|
|||
TextColumn get imageUrl => text().nullable()();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -63,8 +73,11 @@ class UserTasks extends Table {
|
|||
|
||||
/// Таблица UserTaskProgresses - прогресс выполнения задач пользователями
|
||||
class UserTaskProgresses extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get userId => text()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get taskId => text()(); // Reference to UserTasks, but not FK to avoid circular deps
|
||||
|
||||
// Прогресс (JSON)
|
||||
|
|
@ -72,8 +85,10 @@ class UserTaskProgresses extends Table {
|
|||
.withDefault(const Constant('{}'))
|
||||
.map(const JsonMapConverter())();
|
||||
|
||||
DateTimeColumn get startedAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get startedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -81,8 +96,11 @@ class UserTaskProgresses extends Table {
|
|||
|
||||
/// Таблица UserTaskResults - результаты выполнения задач
|
||||
class UserTaskResults extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get userId => text().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get userId => text()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get taskId => text()(); // Reference to UserTasks
|
||||
|
||||
// Результаты (JSON)
|
||||
|
|
@ -90,10 +108,12 @@ class UserTaskResults extends Table {
|
|||
.withDefault(const Constant('{}'))
|
||||
.map(const JsonMapConverter())();
|
||||
|
||||
DateTimeColumn get completedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get completedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,21 +1,27 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../converters.dart' show generateUuid;
|
||||
import '../postgres_constants.dart';
|
||||
|
||||
/// Таблица ShareRequests - запросы на шаринг через Telegram
|
||||
class ShareRequests extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
TextColumn get telegramUserId => text()();
|
||||
TextColumn get telegramUsername => text().nullable()();
|
||||
|
||||
TextColumn get sharedCardId => text().nullable()();
|
||||
TextColumn get sharedCardId => text()
|
||||
.nullable()();
|
||||
|
||||
DateTimeColumn get requestedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get requestedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../converters.dart' show generateUuid, JsonMapConverter;
|
||||
import '../postgres_constants.dart';
|
||||
import 'packs.dart';
|
||||
|
||||
/// Таблица Tests - тесты
|
||||
class Tests extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
|
||||
TextColumn get name => text()();
|
||||
TextColumn get color => text().nullable()();
|
||||
|
|
@ -15,11 +17,13 @@ class Tests extends Table {
|
|||
TextColumn get timeSubtitle => text().nullable()();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
BoolColumn get isDeleted => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -27,16 +31,21 @@ class Tests extends Table {
|
|||
|
||||
/// Таблица TestQuestions - вопросы тестов
|
||||
class TestQuestions extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get testId => text().references(Tests, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get testId => text()
|
||||
.references(Tests, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
// Тип вопроса (enum as string)
|
||||
TextColumn get questionType => text()();
|
||||
TextColumn get body => text()();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -44,8 +53,10 @@ class TestQuestions extends Table {
|
|||
|
||||
/// Таблица TestPackRelations - связь Tests с CardPacks (many-to-many)
|
||||
class TestPackRelations extends Table {
|
||||
TextColumn get testId => text().references(Tests, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get packId => text().references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get testId => text()
|
||||
.references(Tests, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get packId => text()
|
||||
.references(CardPacks, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {testId, packId};
|
||||
|
|
@ -53,20 +64,26 @@ class TestPackRelations extends Table {
|
|||
|
||||
/// Таблица TestStatistics - статистика прохождения тестов
|
||||
class TestStatistics extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get userId => text()(); // Reference to Users, but not FK to avoid circular deps
|
||||
TextColumn get testId => text().references(Tests, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get testId => text()
|
||||
.references(Tests, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
// Результаты (JSON)
|
||||
TextColumn get results => text()
|
||||
.withDefault(const Constant('{}'))
|
||||
.map(const JsonMapConverter())();
|
||||
|
||||
DateTimeColumn get completedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get completedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import '../converters.dart';
|
||||
import '../postgres_constants.dart';
|
||||
|
||||
/// Таблица Users - основная информация о пользователях
|
||||
class Users extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get externalUserId => text().unique()();
|
||||
TextColumn get name => text().nullable()();
|
||||
TextColumn get email => text().nullable()();
|
||||
BoolColumn get admin => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
// UserSettings (JSON)
|
||||
TextColumn get userSettings => text().nullable()();
|
||||
|
|
@ -21,11 +23,13 @@ class Users extends Table {
|
|||
.map(const StringListConverter())();
|
||||
|
||||
// Audit fields
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
BoolColumn get isDeleted => boolean()
|
||||
.withDefault(const Constant(false))
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN, не нужен CHECK
|
||||
.customConstraint('')(); // PostgreSQL использует нативный BOOLEAN
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
@ -38,8 +42,12 @@ class Users extends Table {
|
|||
|
||||
/// Таблица UserDatas - расширенная информация о пользователе
|
||||
class UserDatas extends Table {
|
||||
TextColumn get id => text().withDefault(const Constant('gen_random_uuid()'))();
|
||||
TextColumn get userId => text().unique().references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
// ID как String, но фактически UUID (генерируется PostgreSQL на стороне БД)
|
||||
TextColumn get id => text()
|
||||
.withDefault(const CustomExpression('gen_random_uuid()::text'))();
|
||||
TextColumn get userId => text()
|
||||
.unique()
|
||||
.references(Users, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
// Статистика
|
||||
IntColumn get totalStudyTimeMinutes => integer().withDefault(const Constant(0))();
|
||||
|
|
@ -49,8 +57,10 @@ class UserDatas extends Table {
|
|||
IntColumn get totalTests => integer().withDefault(const Constant(0))();
|
||||
|
||||
// Временные метки
|
||||
DateTimeColumn get lastTimeOnline => dateTime().nullable()();
|
||||
DateTimeColumn get registrationDate => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get lastTimeOnline => customType(PgTypes.timestampWithTimezone)
|
||||
.nullable()();
|
||||
Column<PgDateTime> get registrationDate => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
TextColumn get lastTestSessionToken => text().nullable()();
|
||||
|
||||
// Сложные структуры (JSON)
|
||||
|
|
@ -79,8 +89,10 @@ class UserDatas extends Table {
|
|||
.map(const JsonMapConverter())();
|
||||
|
||||
// Audit
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentTimestamp)();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentTimestamp)();
|
||||
Column<PgDateTime> get createdAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
Column<PgDateTime> get updatedAt => customType(PgTypes.timestampWithTimezone)
|
||||
.withDefault(now())();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:mnemo_cards_backend/database/daos/discount_dao.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
|
@ -30,8 +31,8 @@ extension DiscountCampaignToDto on DiscountCampaign {
|
|||
|
||||
return DiscountCampaignDto(
|
||||
id: id,
|
||||
start: start,
|
||||
finish: finish,
|
||||
start: start.dateTime,
|
||||
finish: finish.dateTime,
|
||||
status: statusEnum,
|
||||
name: name,
|
||||
tags: tags,
|
||||
|
|
@ -80,8 +81,8 @@ extension DiscountCampaignDtoToCompanion on DiscountCampaignDto {
|
|||
return DiscountCampaignsCompanion.insert(
|
||||
id: id != null ? drift.Value(id!) : const drift.Value.absent(),
|
||||
name: drift.Value(name),
|
||||
start: start,
|
||||
finish: finish,
|
||||
start: PgDateTime(start),
|
||||
finish: PgDateTime(finish),
|
||||
status: statusStr,
|
||||
tags: drift.Value(tags),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,12 +42,12 @@ class DiscountsManager {
|
|||
String? targetStatus;
|
||||
|
||||
if (campaign.status == 'created' &&
|
||||
now.isBetween(campaign.start, campaign.finish)) {
|
||||
now.isBetween(campaign.start.dateTime, campaign.finish.dateTime)) {
|
||||
targetStatus = 'active';
|
||||
} else if (campaign.status == 'active') {
|
||||
if (now.isBefore(campaign.start)) {
|
||||
if (now.isBefore(campaign.start.dateTime)) {
|
||||
targetStatus = 'created';
|
||||
} else if (now.isAfter(campaign.finish)) {
|
||||
} else if (now.isAfter(campaign.finish.dateTime)) {
|
||||
targetStatus = 'expired';
|
||||
}
|
||||
}
|
||||
|
|
@ -59,8 +59,8 @@ class DiscountsManager {
|
|||
|
||||
Future<DiscountError?> applyCampaign(DiscountCampaign campaign) async {
|
||||
final now = DateTime.now();
|
||||
if (!now.isBetween(campaign.start, campaign.finish)) {
|
||||
if (now.isBefore(campaign.start)) {
|
||||
if (!now.isBetween(campaign.start.dateTime, campaign.finish.dateTime)) {
|
||||
if (now.isBefore(campaign.start.dateTime)) {
|
||||
return DiscountError(DiscountErrorType.notStarted);
|
||||
}
|
||||
return DiscountError(DiscountErrorType.expired);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/api/purchase/payment_manager.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
|
@ -47,8 +48,8 @@ class PromoCodesManager {
|
|||
activationsPerCode: campaign.activationsPerCode,
|
||||
activationsPerUser: campaign.activationsPerUser,
|
||||
generationSize: campaign.generationSize,
|
||||
start: campaign.start,
|
||||
finish: campaign.finish,
|
||||
start: campaign.start.dateTime,
|
||||
finish: campaign.finish.dateTime,
|
||||
status: _parseCampaignStatus(campaign.status),
|
||||
tags: campaign.tags,
|
||||
promoCodes: promoCodes,
|
||||
|
|
@ -77,8 +78,8 @@ class PromoCodesManager {
|
|||
activationsPerCode: campaign.activationsPerCode,
|
||||
activationsPerUser: campaign.activationsPerUser,
|
||||
generationSize: campaign.generationSize,
|
||||
start: campaign.start,
|
||||
finish: campaign.finish,
|
||||
start: campaign.start.dateTime,
|
||||
finish: campaign.finish.dateTime,
|
||||
status: _parseCampaignStatus(campaign.status),
|
||||
tags: campaign.tags,
|
||||
promoCodes: promoCodes,
|
||||
|
|
@ -95,8 +96,8 @@ class PromoCodesManager {
|
|||
activationsPerCode: dto.activationsPerCode,
|
||||
activationsPerUser: dto.activationsPerUser,
|
||||
generationSize: dto.generationSize,
|
||||
start: dto.start,
|
||||
finish: dto.finish,
|
||||
start: PgDateTime(dto.start),
|
||||
finish: PgDateTime(dto.finish),
|
||||
status: dto.status.name,
|
||||
tags: drift.Value(dto.tags),
|
||||
);
|
||||
|
|
@ -119,11 +120,11 @@ class PromoCodesManager {
|
|||
activationsPerCode: dto.activationsPerCode,
|
||||
activationsPerUser: dto.activationsPerUser,
|
||||
generationSize: dto.generationSize,
|
||||
start: dto.start,
|
||||
finish: dto.finish,
|
||||
start: PgDateTime(dto.start),
|
||||
finish: PgDateTime(dto.finish),
|
||||
status: dto.status.name,
|
||||
tags: dto.tags,
|
||||
updatedAt: DateTime.now(),
|
||||
updatedAt: PgDateTime(DateTime.now()),
|
||||
);
|
||||
|
||||
await _db.promoCodeDao.updateCampaign(updated);
|
||||
|
|
@ -137,7 +138,7 @@ class PromoCodesManager {
|
|||
// Soft delete - mark as deleted
|
||||
await _db.promoCodeDao.updateCampaign(existing.copyWith(
|
||||
isDeleted: true,
|
||||
updatedAt: DateTime.now(),
|
||||
updatedAt: PgDateTime(DateTime.now()),
|
||||
));
|
||||
|
||||
return null; // No error
|
||||
|
|
@ -168,8 +169,8 @@ class PromoCodesManager {
|
|||
|
||||
final now = DateTime.now();
|
||||
if (campaign.status != 'active' ||
|
||||
campaign.start.isAfter(now) ||
|
||||
campaign.finish.isBefore(now)) {
|
||||
campaign.start.dateTime.isAfter(now) ||
|
||||
campaign.finish.dateTime.isBefore(now)) {
|
||||
return {'valid': false, 'message': 'Promo code expired'};
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +255,7 @@ class PromoCodesManager {
|
|||
await _db.promoCodeDao.updateCampaign(
|
||||
campaign.copyWith(
|
||||
status: 'active',
|
||||
updatedAt: DateTime.now(),
|
||||
updatedAt: PgDateTime(DateTime.now()),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
|
@ -57,7 +58,7 @@ class AchievementManager {
|
|||
final definition = AchievementDefinitions.getById(userAchievement.achievementId);
|
||||
if (definition != null) {
|
||||
achievements.add(definition.copyWith(
|
||||
unlockedAt: userAchievement.unlockedAt,
|
||||
unlockedAt: userAchievement.unlockedAt.dateTime,
|
||||
progress: userAchievement.progress,
|
||||
));
|
||||
}
|
||||
|
|
@ -90,7 +91,7 @@ class AchievementManager {
|
|||
final companion = UserAchievementsCompanion.insert(
|
||||
userId: userId,
|
||||
achievementId: achievementId,
|
||||
unlockedAt: Value(DateTime.now()),
|
||||
unlockedAt: Value(PgDateTime(DateTime.now())),
|
||||
progress: Value(1.0),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
|
|
@ -45,7 +46,7 @@ class SessionTracker {
|
|||
|
||||
// Create new session
|
||||
final sessionId = _generateSessionId();
|
||||
final now = DateTime.now();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
|
||||
await _db.statisticsDao.createSession(
|
||||
StudySessionsCompanion.insert(
|
||||
|
|
@ -116,7 +117,7 @@ class SessionTracker {
|
|||
wordsLearned: wordsLearned ?? session.wordsLearned,
|
||||
testsCompleted: testsCompleted ?? session.testsCompleted,
|
||||
accuracy: accuracy ?? session.accuracy,
|
||||
updatedAt: DateTime.now(),
|
||||
updatedAt: PgDateTime(DateTime.now()),
|
||||
);
|
||||
|
||||
await _db.statisticsDao.updateSession(updatedSession);
|
||||
|
|
@ -155,7 +156,7 @@ class SessionTracker {
|
|||
|
||||
if (session != null &&
|
||||
session.endTime == null &&
|
||||
now.difference(session.startTime).inMinutes > _sessionTimeout.inMinutes) {
|
||||
now.difference(session.startTime.dateTime).inMinutes > _sessionTimeout.inMinutes) {
|
||||
expiredSessions.add(sessionId);
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +191,7 @@ class SessionTracker {
|
|||
final session = await _db.statisticsDao.getSessionBySessionId(sessionId);
|
||||
if (session != null) {
|
||||
await _db.statisticsDao.updateSession(
|
||||
session.copyWith(updatedAt: DateTime.now()),
|
||||
session.copyWith(updatedAt: PgDateTime(DateTime.now())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
|
@ -64,7 +65,7 @@ class TaskManager {
|
|||
}
|
||||
|
||||
// Обновить статус задачи
|
||||
final now = DateTime.now();
|
||||
final now = PgDateTime(DateTime.now());
|
||||
await _db.taskDao.updateUserTask(
|
||||
task.copyWith(
|
||||
status: 'completed',
|
||||
|
|
@ -77,7 +78,7 @@ class TaskManager {
|
|||
UserTaskResultsCompanion.insert(
|
||||
userId: userId,
|
||||
taskId: taskId,
|
||||
results: drift.Value({'completed': true, 'completedAt': now.toIso8601String()}),
|
||||
results: drift.Value({'completed': true, 'completedAt': now.dateTime.toIso8601String()}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
|
@ -48,7 +49,7 @@ class UserManager {
|
|||
final now = DateTime.now();
|
||||
final token = await _db.userDao.getTokenByUserId(user.id!);
|
||||
if (token != null) {
|
||||
if (token.expires.isAfter(now)) {
|
||||
if (token.expires.dateTime.isAfter(now)) {
|
||||
return token.token;
|
||||
}
|
||||
await _db.userDao.deleteToken(token.id);
|
||||
|
|
@ -59,7 +60,7 @@ class UserManager {
|
|||
token: userToken,
|
||||
externalUserId: externalId,
|
||||
userId: user.id!,
|
||||
expires: now.add(const Duration(days: 360)),
|
||||
expires: PgDateTime(now.add(const Duration(days: 360))),
|
||||
),
|
||||
);
|
||||
return userToken;
|
||||
|
|
@ -71,7 +72,7 @@ class UserManager {
|
|||
return null;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
if (token.expires.isBefore(now)) {
|
||||
if (token.expires.dateTime.isBefore(now)) {
|
||||
await _db.userDao.deleteToken(token.id);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -98,8 +99,8 @@ class UserManager {
|
|||
await _db.userDao.updateUserDataPartial(
|
||||
UserDatasCompanion(
|
||||
userId: drift.Value(userId),
|
||||
lastTimeOnline: drift.Value(lastOnline),
|
||||
updatedAt: drift.Value(DateTime.now()),
|
||||
lastTimeOnline: drift.Value(PgDateTime(lastOnline)),
|
||||
updatedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -137,7 +138,7 @@ class UserManager {
|
|||
await _db.userDao.createUserData(
|
||||
UserDatasCompanion.insert(
|
||||
userId: userId,
|
||||
registrationDate: drift.Value(DateTime.now()),
|
||||
registrationDate: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -166,7 +167,7 @@ class UserManager {
|
|||
UsersCompanion(
|
||||
id: drift.Value(user.id!),
|
||||
userSettings: drift.Value(jsonEncode(settings.toJson())),
|
||||
updatedAt: drift.Value(DateTime.now()),
|
||||
updatedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -219,7 +220,7 @@ class UserManager {
|
|||
// Обновить существующую статистику
|
||||
final updatedStat = existingStat.copyWith(
|
||||
results: drift.Value(updatedResults),
|
||||
updatedAt: DateTime.now(),
|
||||
updatedAt: PgDateTime(DateTime.now()),
|
||||
);
|
||||
await _db.testDao.updateTestStatistics(updatedStat);
|
||||
} else {
|
||||
|
|
@ -229,7 +230,7 @@ class UserManager {
|
|||
userId: user.id!,
|
||||
testId: testStat.testId,
|
||||
results: drift.Value(updatedResults),
|
||||
completedAt: drift.Value(DateTime.now()),
|
||||
completedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -259,8 +260,8 @@ class UserManager {
|
|||
studyDates: drift.Value(studyDates),
|
||||
currentStreak: drift.Value(currentStreak),
|
||||
longestStreak: drift.Value(longestStreak),
|
||||
lastTimeOnline: drift.Value(now),
|
||||
updatedAt: drift.Value(now),
|
||||
lastTimeOnline: drift.Value(PgDateTime(now)),
|
||||
updatedAt: drift.Value(PgDateTime(now)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:mnemo_cards_backend/database/database.dart';
|
||||
|
|
@ -44,7 +45,7 @@ class UserManager {
|
|||
final now = DateTime.now();
|
||||
final token = await _db.userDao.getTokenByUserId(user.id!);
|
||||
if (token != null) {
|
||||
if (token.expires.isAfter(now)) {
|
||||
if (token.expires.dateTime.isAfter(now)) {
|
||||
return token.token;
|
||||
}
|
||||
await _db.userDao.deleteToken(token.id);
|
||||
|
|
@ -55,7 +56,7 @@ class UserManager {
|
|||
token: userToken,
|
||||
externalUserId: externalId,
|
||||
userId: user.id!,
|
||||
expires: now.add(const Duration(days: 360)),
|
||||
expires: PgDateTime(now.add(const Duration(days: 360))),
|
||||
),
|
||||
);
|
||||
return userToken;
|
||||
|
|
@ -67,7 +68,7 @@ class UserManager {
|
|||
return null;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
if (token.expires.isBefore(now)) {
|
||||
if (token.expires.dateTime.isBefore(now)) {
|
||||
await _db.userDao.deleteToken(token.id);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -94,8 +95,8 @@ class UserManager {
|
|||
await _db.userDao.updateUserDataPartial(
|
||||
UserDatasCompanion(
|
||||
userId: drift.Value(userId),
|
||||
lastTimeOnline: drift.Value(lastOnline),
|
||||
updatedAt: drift.Value(DateTime.now()),
|
||||
lastTimeOnline: drift.Value(PgDateTime(lastOnline)),
|
||||
updatedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -133,7 +134,7 @@ class UserManager {
|
|||
await _db.userDao.createUserData(
|
||||
UserDatasCompanion.insert(
|
||||
userId: userId,
|
||||
registrationDate: drift.Value(DateTime.now()),
|
||||
registrationDate: drift.Value(PgDateTime(DateTime.now())),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -53,10 +53,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.13.0"
|
||||
auth_header:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -229,10 +229,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.7"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -595,10 +595,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: postgres
|
||||
sha256: "83ba7afb0e778cc1f373e514c6110d323b81dfbaced774d3a7f4a0e21536eb45"
|
||||
sha256: fefbbfe749c6130e5096588b9c4459173684c695952cd7636ab19be76f255469
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.8"
|
||||
version: "3.5.9"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -639,22 +639,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sasl_scram:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sasl_scram
|
||||
sha256: a47207a436eb650f8fdcf54a2e2587b850dc3caef9973ce01f332b07a6fc9cb9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
saslprep:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: saslprep
|
||||
sha256: "3d421d10be9513bf4459c17c5e70e7b8bc718c9fc5ad4ba5eb4f5fd27396f740"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
shelf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -827,18 +811,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -895,14 +879,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
unorm_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: unorm_dart
|
||||
sha256: "0c69186b03ca6addab0774bcc0f4f17b88d4ce78d9d4d8f0619e30a99ead58e7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ dependencies:
|
|||
path: ../mnemo_cards_common_backend
|
||||
|
||||
# PostgreSQL + Drift
|
||||
drift: ^2.14.0
|
||||
drift_postgres: ^1.1.0
|
||||
postgres: ^3.0.4
|
||||
drift: ^2.30.0
|
||||
drift_postgres: ^1.3.1
|
||||
postgres: ^3.5.9
|
||||
|
||||
image: ^4.1.7
|
||||
http: ^1.1.0
|
||||
|
|
@ -61,7 +61,7 @@ dev_dependencies:
|
|||
copy_with_extension_gen: ^11.0.0
|
||||
|
||||
# Drift code generation
|
||||
drift_dev: ^2.14.0
|
||||
drift_dev: ^2.30.0
|
||||
|
||||
shelf_router_generator: ^1.1.3
|
||||
shelf_open_api_generator:
|
||||
|
|
|
|||
Loading…
Reference in a new issue