This commit is contained in:
Dmitry 2026-01-24 19:07:29 +03:00
parent f665c75eeb
commit 63524c2575
10 changed files with 109 additions and 28 deletions

View file

@ -22,6 +22,9 @@ RUN dart pub get
WORKDIR /app/mnemo_cards_backend WORKDIR /app/mnemo_cards_backend
RUN dart pub get RUN dart pub get
# Generate code (drift, json_serializable, etc.)
RUN dart run build_runner build --delete-conflicting-outputs
# Compile to native executable # Compile to native executable
RUN dart compile exe lib/main.dart -o /app/server RUN dart compile exe lib/main.dart -o /app/server

View file

@ -631,7 +631,7 @@ class AdminTestsApiV2 {
final timeSubtitle = bodyJson['timeSubtitle'] as String?; final timeSubtitle = bodyJson['timeSubtitle'] as String?;
final expiresAtStr = bodyJson['expiresAt'] as String?; final expiresAtStr = bodyJson['expiresAt'] as String?;
final questions = bodyJson['questions'] as List<dynamic>?; final questions = bodyJson['questions'] as List<dynamic>?;
// Parse expiresAt if provided // Parse expiresAt if provided
PgDateTime? expiresAt; PgDateTime? expiresAt;
if (expiresAtStr != null && expiresAtStr.isNotEmpty) { if (expiresAtStr != null && expiresAtStr.isNotEmpty) {
@ -642,7 +642,8 @@ class AdminTestsApiV2 {
'error': 'Invalid expiresAt format', 'error': 'Invalid expiresAt format',
'message': 'expiresAt must be a valid ISO 8601 datetime string', 'message': 'expiresAt must be a valid ISO 8601 datetime string',
'field': 'expiresAt', 'field': 'expiresAt',
'details': 'Please provide expiresAt in ISO 8601 format (e.g., "2025-01-24T12:00:00Z").', 'details':
'Please provide expiresAt in ISO 8601 format (e.g., "2025-01-24T12:00:00Z").',
}, statusCode: 400); }, statusCode: 400);
} }
} }
@ -799,7 +800,8 @@ class AdminTestsApiV2 {
'version': updatedTest.version, 'version': updatedTest.version,
'time': updatedTest.time, 'time': updatedTest.time,
'timeSubtitle': updatedTest.timeSubtitle, 'timeSubtitle': updatedTest.timeSubtitle,
'expiresAt': updatedTest.expiresAt?.dateTime.toIso8601String(), 'expiresAt': updatedTest.expiresAt?.dateTime
.toIso8601String(),
} }
: { : {
'id': testId, 'id': testId,
@ -933,7 +935,8 @@ class AdminTestsApiV2 {
'version': createdTest.version, 'version': createdTest.version,
'time': createdTest.time, 'time': createdTest.time,
'timeSubtitle': createdTest.timeSubtitle, 'timeSubtitle': createdTest.timeSubtitle,
'expiresAt': createdTest.expiresAt?.dateTime.toIso8601String(), 'expiresAt': createdTest.expiresAt?.dateTime
.toIso8601String(),
} }
: { : {
'id': newTest, 'id': newTest,

View file

@ -125,12 +125,10 @@ class MediaApiV2 {
} }
/// Parses multipart/form-data request and extracts file and optional objectId /// Parses multipart/form-data request and extracts file and optional objectId
Future<({ Future<
Uint8List bytes, ({Uint8List bytes, String contentType, String? filename, String? objectId})?
String contentType, >
String? filename, _parseMultipartFileWithObjectId(Request request) async {
String? objectId,
})?> _parseMultipartFileWithObjectId(Request request) async {
try { try {
final multipart = request.multipart(); final multipart = request.multipart();
if (multipart == null) { if (multipart == null) {
@ -144,7 +142,7 @@ class MediaApiV2 {
await for (final part in multipart.parts) { await for (final part in multipart.parts) {
final contentDisposition = part.headers['content-disposition'] ?? ''; final contentDisposition = part.headers['content-disposition'] ?? '';
if (contentDisposition.contains('name="file"')) { if (contentDisposition.contains('name="file"')) {
// Read file bytes // Read file bytes
final chunks = <List<int>>[]; final chunks = <List<int>>[];
@ -375,7 +373,7 @@ class MediaApiV2 {
r'^[0-9a-f]{32}$', r'^[0-9a-f]{32}$',
caseSensitive: false, caseSensitive: false,
).hasMatch(objectId); ).hasMatch(objectId);
if (!isValidUuid && !isValidHash) { if (!isValidUuid && !isValidHash) {
return _badRequest( return _badRequest(
'Invalid objectId format. Must be UUID or MD5 hash (32 hex characters)', 'Invalid objectId format. Must be UUID or MD5 hash (32 hex characters)',

View file

@ -13,5 +13,6 @@ Router _$MediaApiV2Router(MediaApiV2 service) {
router.add('POST', r'/media/upload/voice', service.uploadVoice); router.add('POST', r'/media/upload/voice', service.uploadVoice);
router.add('GET', r'/media/<bucket>/<objectId>/url', service.getPresignedUrl); router.add('GET', r'/media/<bucket>/<objectId>/url', service.getPresignedUrl);
router.add('DELETE', r'/media/<bucket>/<objectId>', service.deleteFile); router.add('DELETE', r'/media/<bucket>/<objectId>', service.deleteFile);
router.add('POST', r'/media/generate-tts', service.generateTTS);
return router; return router;
} }

View file

@ -52,7 +52,7 @@ class TestGeneratorTask with cron_task.Task {
} }
} }
print('Generated tests for $ok packs'); print('Generated tests for $ok packs');
// Soft delete tests that have expired (expiresAt <= now) // Soft delete tests that have expired (expiresAt <= now)
print('Soft deleting expired tests...'); print('Soft deleting expired tests...');
try { try {
@ -61,7 +61,7 @@ class TestGeneratorTask with cron_task.Task {
} catch (e) { } catch (e) {
print('failed to soft delete expired tests: $e'); print('failed to soft delete expired tests: $e');
} }
// Hard delete orphan generated tests (not linked to any pack) // Hard delete orphan generated tests (not linked to any pack)
print('Purging orphan generated tests...'); print('Purging orphan generated tests...');
try { try {

View file

@ -14,7 +14,7 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
// ==================== Tests ==================== // ==================== Tests ====================
/// Получить тест по ID (включая soft-deleted для доступа по прямой ссылке) /// Получить тест по ID (включая soft-deleted для доступа по прямой ссылке)
/// ///
/// Примечание: soft-deleted тесты не показываются в списке пака (getTestsByPackId), /// Примечание: soft-deleted тесты не показываются в списке пака (getTestsByPackId),
/// но доступны по прямой ссылке для завершения начатых тестов. /// но доступны по прямой ссылке для завершения начатых тестов.
Future<Test?> getTestById(String id) { Future<Test?> getTestById(String id) {
@ -62,17 +62,18 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
} }
/// Soft delete tests that have expired (expiresAt <= now) /// Soft delete tests that have expired (expiresAt <= now)
/// ///
/// When expiresAt is reached, test becomes soft-deleted (available only by direct link) /// When expiresAt is reached, test becomes soft-deleted (available only by direct link)
Future<int> softDeleteExpiredTests() async { Future<int> softDeleteExpiredTests() async {
final now = DateTime.now(); final now = DateTime.now();
final expiredTests = await (select(tests)..where( final expiredTests =
(t) => await (select(tests)..where(
t.expiresAt.isSmallerOrEqualValue(PgDateTime(now)) & (t) =>
t.isDeleted.equals(false), t.expiresAt.isSmallerOrEqualValue(PgDateTime(now)) &
)) t.isDeleted.equals(false),
.get(); ))
.get();
var deleted = 0; var deleted = 0;
for (final test in expiredTests) { for (final test in expiredTests) {
@ -87,7 +88,7 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
/// This is important because we use soft delete for regular operations, /// This is important because we use soft delete for regular operations,
/// but generated tests are ephemeral and otherwise will accumulate in DB /// but generated tests are ephemeral and otherwise will accumulate in DB
/// (along with their questions/stats). Hard delete triggers FK cascades. /// (along with their questions/stats). Hard delete triggers FK cascades.
/// ///
/// Hard deletes tests that were soft-deleted more than [olderThan] ago. /// Hard deletes tests that were soft-deleted more than [olderThan] ago.
Future<int> hardDeleteOldSoftDeletedGeneratedTests({ Future<int> hardDeleteOldSoftDeletedGeneratedTests({
required Duration olderThan, required Duration olderThan,

View file

@ -8548,6 +8548,18 @@ class $TestsTable extends Tests with TableInfo<$TestsTable, Test> {
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
); );
static const VerificationMeta _expiresAtMeta = const VerificationMeta(
'expiresAt',
);
@override
late final GeneratedColumn<PgDateTime> expiresAt =
GeneratedColumn<PgDateTime>(
'expires_at',
aliasedName,
true,
type: PgTypes.timestampWithTimezone,
requiredDuringInsert: false,
);
static const VerificationMeta _createdAtMeta = const VerificationMeta( static const VerificationMeta _createdAtMeta = const VerificationMeta(
'createdAt', 'createdAt',
); );
@ -8608,6 +8620,7 @@ class $TestsTable extends Tests with TableInfo<$TestsTable, Test> {
version, version,
time, time,
timeSubtitle, timeSubtitle,
expiresAt,
createdAt, createdAt,
updatedAt, updatedAt,
isDeleted, isDeleted,
@ -8669,6 +8682,12 @@ class $TestsTable extends Tests with TableInfo<$TestsTable, Test> {
), ),
); );
} }
if (data.containsKey('expires_at')) {
context.handle(
_expiresAtMeta,
expiresAt.isAcceptableOrUnknown(data['expires_at']!, _expiresAtMeta),
);
}
if (data.containsKey('created_at')) { if (data.containsKey('created_at')) {
context.handle( context.handle(
_createdAtMeta, _createdAtMeta,
@ -8730,6 +8749,10 @@ class $TestsTable extends Tests with TableInfo<$TestsTable, Test> {
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}time_subtitle'], data['${effectivePrefix}time_subtitle'],
), ),
expiresAt: attachedDatabase.typeMapping.read(
PgTypes.timestampWithTimezone,
data['${effectivePrefix}expires_at'],
),
createdAt: attachedDatabase.typeMapping.read( createdAt: attachedDatabase.typeMapping.read(
PgTypes.timestampWithTimezone, PgTypes.timestampWithTimezone,
data['${effectivePrefix}created_at'], data['${effectivePrefix}created_at'],
@ -8763,6 +8786,7 @@ class Test extends DataClass implements Insertable<Test> {
final String? version; final String? version;
final String? time; final String? time;
final String? timeSubtitle; final String? timeSubtitle;
final PgDateTime? expiresAt;
final PgDateTime createdAt; final PgDateTime createdAt;
final PgDateTime updatedAt; final PgDateTime updatedAt;
final bool isDeleted; final bool isDeleted;
@ -8775,6 +8799,7 @@ class Test extends DataClass implements Insertable<Test> {
this.version, this.version,
this.time, this.time,
this.timeSubtitle, this.timeSubtitle,
this.expiresAt,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
required this.isDeleted, required this.isDeleted,
@ -8800,6 +8825,12 @@ class Test extends DataClass implements Insertable<Test> {
if (!nullToAbsent || timeSubtitle != null) { if (!nullToAbsent || timeSubtitle != null) {
map['time_subtitle'] = Variable<String>(timeSubtitle); map['time_subtitle'] = Variable<String>(timeSubtitle);
} }
if (!nullToAbsent || expiresAt != null) {
map['expires_at'] = Variable<PgDateTime>(
expiresAt,
PgTypes.timestampWithTimezone,
);
}
map['created_at'] = Variable<PgDateTime>( map['created_at'] = Variable<PgDateTime>(
createdAt, createdAt,
PgTypes.timestampWithTimezone, PgTypes.timestampWithTimezone,
@ -8835,6 +8866,9 @@ class Test extends DataClass implements Insertable<Test> {
timeSubtitle: timeSubtitle == null && nullToAbsent timeSubtitle: timeSubtitle == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(timeSubtitle), : Value(timeSubtitle),
expiresAt: expiresAt == null && nullToAbsent
? const Value.absent()
: Value(expiresAt),
createdAt: Value(createdAt), createdAt: Value(createdAt),
updatedAt: Value(updatedAt), updatedAt: Value(updatedAt),
isDeleted: Value(isDeleted), isDeleted: Value(isDeleted),
@ -8857,6 +8891,7 @@ class Test extends DataClass implements Insertable<Test> {
version: serializer.fromJson<String?>(json['version']), version: serializer.fromJson<String?>(json['version']),
time: serializer.fromJson<String?>(json['time']), time: serializer.fromJson<String?>(json['time']),
timeSubtitle: serializer.fromJson<String?>(json['timeSubtitle']), timeSubtitle: serializer.fromJson<String?>(json['timeSubtitle']),
expiresAt: serializer.fromJson<PgDateTime?>(json['expiresAt']),
createdAt: serializer.fromJson<PgDateTime>(json['createdAt']), createdAt: serializer.fromJson<PgDateTime>(json['createdAt']),
updatedAt: serializer.fromJson<PgDateTime>(json['updatedAt']), updatedAt: serializer.fromJson<PgDateTime>(json['updatedAt']),
isDeleted: serializer.fromJson<bool>(json['isDeleted']), isDeleted: serializer.fromJson<bool>(json['isDeleted']),
@ -8874,6 +8909,7 @@ class Test extends DataClass implements Insertable<Test> {
'version': serializer.toJson<String?>(version), 'version': serializer.toJson<String?>(version),
'time': serializer.toJson<String?>(time), 'time': serializer.toJson<String?>(time),
'timeSubtitle': serializer.toJson<String?>(timeSubtitle), 'timeSubtitle': serializer.toJson<String?>(timeSubtitle),
'expiresAt': serializer.toJson<PgDateTime?>(expiresAt),
'createdAt': serializer.toJson<PgDateTime>(createdAt), 'createdAt': serializer.toJson<PgDateTime>(createdAt),
'updatedAt': serializer.toJson<PgDateTime>(updatedAt), 'updatedAt': serializer.toJson<PgDateTime>(updatedAt),
'isDeleted': serializer.toJson<bool>(isDeleted), 'isDeleted': serializer.toJson<bool>(isDeleted),
@ -8889,6 +8925,7 @@ class Test extends DataClass implements Insertable<Test> {
Value<String?> version = const Value.absent(), Value<String?> version = const Value.absent(),
Value<String?> time = const Value.absent(), Value<String?> time = const Value.absent(),
Value<String?> timeSubtitle = const Value.absent(), Value<String?> timeSubtitle = const Value.absent(),
Value<PgDateTime?> expiresAt = const Value.absent(),
PgDateTime? createdAt, PgDateTime? createdAt,
PgDateTime? updatedAt, PgDateTime? updatedAt,
bool? isDeleted, bool? isDeleted,
@ -8901,6 +8938,7 @@ class Test extends DataClass implements Insertable<Test> {
version: version.present ? version.value : this.version, version: version.present ? version.value : this.version,
time: time.present ? time.value : this.time, time: time.present ? time.value : this.time,
timeSubtitle: timeSubtitle.present ? timeSubtitle.value : this.timeSubtitle, timeSubtitle: timeSubtitle.present ? timeSubtitle.value : this.timeSubtitle,
expiresAt: expiresAt.present ? expiresAt.value : this.expiresAt,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted, isDeleted: isDeleted ?? this.isDeleted,
@ -8917,6 +8955,7 @@ class Test extends DataClass implements Insertable<Test> {
timeSubtitle: data.timeSubtitle.present timeSubtitle: data.timeSubtitle.present
? data.timeSubtitle.value ? data.timeSubtitle.value
: this.timeSubtitle, : this.timeSubtitle,
expiresAt: data.expiresAt.present ? data.expiresAt.value : this.expiresAt,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted, isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted,
@ -8934,6 +8973,7 @@ class Test extends DataClass implements Insertable<Test> {
..write('version: $version, ') ..write('version: $version, ')
..write('time: $time, ') ..write('time: $time, ')
..write('timeSubtitle: $timeSubtitle, ') ..write('timeSubtitle: $timeSubtitle, ')
..write('expiresAt: $expiresAt, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ') ..write('updatedAt: $updatedAt, ')
..write('isDeleted: $isDeleted, ') ..write('isDeleted: $isDeleted, ')
@ -8951,6 +8991,7 @@ class Test extends DataClass implements Insertable<Test> {
version, version,
time, time,
timeSubtitle, timeSubtitle,
expiresAt,
createdAt, createdAt,
updatedAt, updatedAt,
isDeleted, isDeleted,
@ -8967,6 +9008,7 @@ class Test extends DataClass implements Insertable<Test> {
other.version == this.version && other.version == this.version &&
other.time == this.time && other.time == this.time &&
other.timeSubtitle == this.timeSubtitle && other.timeSubtitle == this.timeSubtitle &&
other.expiresAt == this.expiresAt &&
other.createdAt == this.createdAt && other.createdAt == this.createdAt &&
other.updatedAt == this.updatedAt && other.updatedAt == this.updatedAt &&
other.isDeleted == this.isDeleted && other.isDeleted == this.isDeleted &&
@ -8981,6 +9023,7 @@ class TestsCompanion extends UpdateCompanion<Test> {
final Value<String?> version; final Value<String?> version;
final Value<String?> time; final Value<String?> time;
final Value<String?> timeSubtitle; final Value<String?> timeSubtitle;
final Value<PgDateTime?> expiresAt;
final Value<PgDateTime> createdAt; final Value<PgDateTime> createdAt;
final Value<PgDateTime> updatedAt; final Value<PgDateTime> updatedAt;
final Value<bool> isDeleted; final Value<bool> isDeleted;
@ -8994,6 +9037,7 @@ class TestsCompanion extends UpdateCompanion<Test> {
this.version = const Value.absent(), this.version = const Value.absent(),
this.time = const Value.absent(), this.time = const Value.absent(),
this.timeSubtitle = const Value.absent(), this.timeSubtitle = const Value.absent(),
this.expiresAt = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.updatedAt = const Value.absent(), this.updatedAt = const Value.absent(),
this.isDeleted = const Value.absent(), this.isDeleted = const Value.absent(),
@ -9008,6 +9052,7 @@ class TestsCompanion extends UpdateCompanion<Test> {
this.version = const Value.absent(), this.version = const Value.absent(),
this.time = const Value.absent(), this.time = const Value.absent(),
this.timeSubtitle = const Value.absent(), this.timeSubtitle = const Value.absent(),
this.expiresAt = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.updatedAt = const Value.absent(), this.updatedAt = const Value.absent(),
this.isDeleted = const Value.absent(), this.isDeleted = const Value.absent(),
@ -9022,6 +9067,7 @@ class TestsCompanion extends UpdateCompanion<Test> {
Expression<String>? version, Expression<String>? version,
Expression<String>? time, Expression<String>? time,
Expression<String>? timeSubtitle, Expression<String>? timeSubtitle,
Expression<PgDateTime>? expiresAt,
Expression<PgDateTime>? createdAt, Expression<PgDateTime>? createdAt,
Expression<PgDateTime>? updatedAt, Expression<PgDateTime>? updatedAt,
Expression<bool>? isDeleted, Expression<bool>? isDeleted,
@ -9036,6 +9082,7 @@ class TestsCompanion extends UpdateCompanion<Test> {
if (version != null) 'version': version, if (version != null) 'version': version,
if (time != null) 'time': time, if (time != null) 'time': time,
if (timeSubtitle != null) 'time_subtitle': timeSubtitle, if (timeSubtitle != null) 'time_subtitle': timeSubtitle,
if (expiresAt != null) 'expires_at': expiresAt,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (updatedAt != null) 'updated_at': updatedAt, if (updatedAt != null) 'updated_at': updatedAt,
if (isDeleted != null) 'is_deleted': isDeleted, if (isDeleted != null) 'is_deleted': isDeleted,
@ -9052,6 +9099,7 @@ class TestsCompanion extends UpdateCompanion<Test> {
Value<String?>? version, Value<String?>? version,
Value<String?>? time, Value<String?>? time,
Value<String?>? timeSubtitle, Value<String?>? timeSubtitle,
Value<PgDateTime?>? expiresAt,
Value<PgDateTime>? createdAt, Value<PgDateTime>? createdAt,
Value<PgDateTime>? updatedAt, Value<PgDateTime>? updatedAt,
Value<bool>? isDeleted, Value<bool>? isDeleted,
@ -9066,6 +9114,7 @@ class TestsCompanion extends UpdateCompanion<Test> {
version: version ?? this.version, version: version ?? this.version,
time: time ?? this.time, time: time ?? this.time,
timeSubtitle: timeSubtitle ?? this.timeSubtitle, timeSubtitle: timeSubtitle ?? this.timeSubtitle,
expiresAt: expiresAt ?? this.expiresAt,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
isDeleted: isDeleted ?? this.isDeleted, isDeleted: isDeleted ?? this.isDeleted,
@ -9098,6 +9147,12 @@ class TestsCompanion extends UpdateCompanion<Test> {
if (timeSubtitle.present) { if (timeSubtitle.present) {
map['time_subtitle'] = Variable<String>(timeSubtitle.value); map['time_subtitle'] = Variable<String>(timeSubtitle.value);
} }
if (expiresAt.present) {
map['expires_at'] = Variable<PgDateTime>(
expiresAt.value,
PgTypes.timestampWithTimezone,
);
}
if (createdAt.present) { if (createdAt.present) {
map['created_at'] = Variable<PgDateTime>( map['created_at'] = Variable<PgDateTime>(
createdAt.value, createdAt.value,
@ -9135,6 +9190,7 @@ class TestsCompanion extends UpdateCompanion<Test> {
..write('version: $version, ') ..write('version: $version, ')
..write('time: $time, ') ..write('time: $time, ')
..write('timeSubtitle: $timeSubtitle, ') ..write('timeSubtitle: $timeSubtitle, ')
..write('expiresAt: $expiresAt, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ') ..write('updatedAt: $updatedAt, ')
..write('isDeleted: $isDeleted, ') ..write('isDeleted: $isDeleted, ')
@ -27689,6 +27745,7 @@ typedef $$TestsTableCreateCompanionBuilder =
Value<String?> version, Value<String?> version,
Value<String?> time, Value<String?> time,
Value<String?> timeSubtitle, Value<String?> timeSubtitle,
Value<PgDateTime?> expiresAt,
Value<PgDateTime> createdAt, Value<PgDateTime> createdAt,
Value<PgDateTime> updatedAt, Value<PgDateTime> updatedAt,
Value<bool> isDeleted, Value<bool> isDeleted,
@ -27704,6 +27761,7 @@ typedef $$TestsTableUpdateCompanionBuilder =
Value<String?> version, Value<String?> version,
Value<String?> time, Value<String?> time,
Value<String?> timeSubtitle, Value<String?> timeSubtitle,
Value<PgDateTime?> expiresAt,
Value<PgDateTime> createdAt, Value<PgDateTime> createdAt,
Value<PgDateTime> updatedAt, Value<PgDateTime> updatedAt,
Value<bool> isDeleted, Value<bool> isDeleted,
@ -27819,6 +27877,11 @@ class $$TestsTableFilterComposer extends Composer<_$AppDatabase, $TestsTable> {
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<PgDateTime> get expiresAt => $composableBuilder(
column: $table.expiresAt,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<PgDateTime> get createdAt => $composableBuilder( ColumnFilters<PgDateTime> get createdAt => $composableBuilder(
column: $table.createdAt, column: $table.createdAt,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@ -27959,6 +28022,11 @@ class $$TestsTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<PgDateTime> get expiresAt => $composableBuilder(
column: $table.expiresAt,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<PgDateTime> get createdAt => $composableBuilder( ColumnOrderings<PgDateTime> get createdAt => $composableBuilder(
column: $table.createdAt, column: $table.createdAt,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -28012,6 +28080,9 @@ class $$TestsTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
GeneratedColumn<PgDateTime> get expiresAt =>
$composableBuilder(column: $table.expiresAt, builder: (column) => column);
GeneratedColumn<PgDateTime> get createdAt => GeneratedColumn<PgDateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column); $composableBuilder(column: $table.createdAt, builder: (column) => column);
@ -28140,6 +28211,7 @@ class $$TestsTableTableManager
Value<String?> version = const Value.absent(), Value<String?> version = const Value.absent(),
Value<String?> time = const Value.absent(), Value<String?> time = const Value.absent(),
Value<String?> timeSubtitle = const Value.absent(), Value<String?> timeSubtitle = const Value.absent(),
Value<PgDateTime?> expiresAt = const Value.absent(),
Value<PgDateTime> createdAt = const Value.absent(), Value<PgDateTime> createdAt = const Value.absent(),
Value<PgDateTime> updatedAt = const Value.absent(), Value<PgDateTime> updatedAt = const Value.absent(),
Value<bool> isDeleted = const Value.absent(), Value<bool> isDeleted = const Value.absent(),
@ -28153,6 +28225,7 @@ class $$TestsTableTableManager
version: version, version: version,
time: time, time: time,
timeSubtitle: timeSubtitle, timeSubtitle: timeSubtitle,
expiresAt: expiresAt,
createdAt: createdAt, createdAt: createdAt,
updatedAt: updatedAt, updatedAt: updatedAt,
isDeleted: isDeleted, isDeleted: isDeleted,
@ -28168,6 +28241,7 @@ class $$TestsTableTableManager
Value<String?> version = const Value.absent(), Value<String?> version = const Value.absent(),
Value<String?> time = const Value.absent(), Value<String?> time = const Value.absent(),
Value<String?> timeSubtitle = const Value.absent(), Value<String?> timeSubtitle = const Value.absent(),
Value<PgDateTime?> expiresAt = const Value.absent(),
Value<PgDateTime> createdAt = const Value.absent(), Value<PgDateTime> createdAt = const Value.absent(),
Value<PgDateTime> updatedAt = const Value.absent(), Value<PgDateTime> updatedAt = const Value.absent(),
Value<bool> isDeleted = const Value.absent(), Value<bool> isDeleted = const Value.absent(),
@ -28181,6 +28255,7 @@ class $$TestsTableTableManager
version: version, version: version,
time: time, time: time,
timeSubtitle: timeSubtitle, timeSubtitle: timeSubtitle,
expiresAt: expiresAt,
createdAt: createdAt, createdAt: createdAt,
updatedAt: updatedAt, updatedAt: updatedAt,
isDeleted: isDeleted, isDeleted: isDeleted,

View file

@ -15,7 +15,7 @@ class Tests extends Table {
TextColumn get version => text().nullable()(); TextColumn get version => text().nullable()();
TextColumn get time => text().nullable()(); TextColumn get time => text().nullable()();
TextColumn get timeSubtitle => text().nullable()(); TextColumn get timeSubtitle => text().nullable()();
// Время, когда тест перестанет быть доступен (soft limit) // Время, когда тест перестанет быть доступен (soft limit)
// Когда expiresAt наступает, тест soft-delete (доступен только по прямой ссылке) // Когда expiresAt наступает, тест soft-delete (доступен только по прямой ссылке)
Column<PgDateTime> get expiresAt => Column<PgDateTime> get expiresAt =>

View file

@ -76,8 +76,8 @@ class MinioService {
final cacheControl = contentType.startsWith('image/') final cacheControl = contentType.startsWith('image/')
? 'public, max-age=604800, immutable' // 7 дней для изображений ? 'public, max-age=604800, immutable' // 7 дней для изображений
: contentType.startsWith('audio/') : contentType.startsWith('audio/')
? 'public, max-age=604800' // 7 дней для аудио ? 'public, max-age=604800' // 7 дней для аудио
: 'public, max-age=86400'; // 1 день для других : 'public, max-age=86400'; // 1 день для других
await _client.putObject( await _client.putObject(
bucket, bucket,

View file

@ -874,7 +874,7 @@ class TestManager {
final expiresAt = testDto.version == 'generated' final expiresAt = testDto.version == 'generated'
? PgDateTime(DateTime.now().add(const Duration(hours: 1))) ? PgDateTime(DateTime.now().add(const Duration(hours: 1)))
: null; : null;
// Create test // Create test
final testCompanion = TestsCompanion.insert( final testCompanion = TestsCompanion.insert(
name: testDto.name, name: testDto.name,