From 7c1bcc2787bced600c724a8f77014685d770ed3a Mon Sep 17 00:00:00 2001 From: necessarylion Date: Mon, 2 Nov 2026 06:51:03 +0700 Subject: [PATCH 1/2] Add dox queue:table It writes a migration for the jobs and failed_jobs tables into db/migration. It is the first package command: dox_queue names lib/commands.dart in the dox: commands: key of pubspec.yaml. --- CHANGELOG.md | 2 + README.md | 15 +++++-- docs/queue.md | 15 +++++-- lib/commands.dart | 12 +++++ lib/src/commands/queue_table_command.dart | 53 +++++++++++++++++++++++ lib/src/queue_tables.dart | 2 +- pubspec.yaml | 4 ++ test/queue_table_command_test.dart | 38 ++++++++++++++++ 8 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 lib/commands.dart create mode 100644 lib/src/commands/queue_table_command.dart create mode 100644 test/queue_table_command_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 6594af4..29bf8ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## Unreleased - Fix: with `Mail.queue(message, encrypt: true)`, a send error could name the recipient (eg. an SMTP reply `550 : mailbox unavailable`). The worker wrote that error to its log and to the failed jobs in plain text. Now the encrypted mail job throws a `RedactedMailError`. Its text has only the error type and the SMTP or HTTP code. The full error text, with the answer of the server, is encrypted with the app key (at most the first 64K characters, counted in UTF-16 code units). Read it with `RedactedMailError.decrypt(job.error)`. When the error can not be encrypted, only the type and the code are kept. Mail jobs without `encrypt` keep the full error, as before. +- New: `dox queue:table` writes a migration for the `jobs` and `failed_jobs` tables in `db/migration`. It calls `QueueTables.up` and `QueueTables.down`. It does not overwrite: when a `create_queue_tables` migration is already there, it prints so and exits with code 1. It is a package command (`dox: commands:` in pubspec.yaml), so it needs a `dox` CLI that finds package commands. +- Needs dox_core with `package:dox_core/command.dart`. - New: a built-in job for `Mail.queue()` and `Mail.later()` of dox_mailer. `Queue.configure()` turns it on. Apps do not add it to `lib/generated/jobs.g.dart`: every `JobRegistry` finds the built-in jobs (`JobRegistry.builtIn`) after its own jobs. With `encrypt: true`, the mail is saved like a `ShouldBeEncrypted` job. - Now depends on dox_mailer. - Deploy the queue worker together with the web app. An old worker does not know the new mail job, so it fails the queued emails. diff --git a/README.md b/README.md index 71feab2..b068656 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,18 @@ The worker starts the providers of your app (database, cache, queue). It does no ### 4. Tables (database driver only) -Add a migration in `db/migration`. `QueueTables` makes the `jobs` and `failed_jobs` tables. +Run `dox queue:table`. It writes a migration in `db/migration` that makes the `jobs` and `failed_jobs` tables. Then run `dox migrate`. -`db/migration/2026_10_25_000000000_create_queue_tables.dart`: +```bash +dox queue:table +dox migrate +``` + +`queue:table` is a [package command](https://dartondox.dev/digging-deeper/package-commands/) of `dox_queue`. It does not overwrite: when a `create_queue_tables` migration is already there, it prints so and stops. + +The migration it writes looks like this. You can also write it by hand. `QueueTables` makes the two tables. + +`db/migration/2026_10_25_000000_create_queue_tables.dart`: ```dart // ignore_for_file: file_names @@ -121,8 +130,6 @@ class CreateQueueTables extends Migration { } ``` -Then run `dox migrate`. - ## Write a job `lib/app/jobs/send_welcome_email.job.dart`: diff --git a/docs/queue.md b/docs/queue.md index 31e874b..f30fcf0 100644 --- a/docs/queue.md +++ b/docs/queue.md @@ -90,9 +90,18 @@ The worker starts the providers of your app (database, cache, queue). It does no ### 4. Tables (database driver only) -Add a migration in `db/migration`. `QueueTables` makes the `jobs` and `failed_jobs` tables. +Run `dox queue:table`. It writes a migration in `db/migration` that makes the `jobs` and `failed_jobs` tables. Then run `dox migrate`. -```dart title="db/migration/2026_10_25_000000000_create_queue_tables.dart" +```bash +dox queue:table +dox migrate +``` + +`queue:table` is a [package command](https://dartondox.dev/digging-deeper/package-commands/) of `dox_queue`. It does not overwrite: when a `create_queue_tables` migration is already there, it prints so and stops. + +The migration it writes looks like this. You can also write it by hand. `QueueTables` makes the two tables. + +```dart title="db/migration/2026_10_25_000000_create_queue_tables.dart" // ignore_for_file: file_names import 'package:dox_migration/dox_migration.dart'; import 'package:dox_queue/dox_queue.dart'; @@ -111,8 +120,6 @@ class CreateQueueTables extends Migration { } ``` -Then run `dox migrate`. - ## Write a job ```dart title="lib/app/jobs/send_welcome_email.job.dart" diff --git a/lib/commands.dart b/lib/commands.dart new file mode 100644 index 0000000..b3fc286 --- /dev/null +++ b/lib/commands.dart @@ -0,0 +1,12 @@ +/// The `dox` commands of dox_queue. The `dox` CLI finds this library from +/// the `dox: commands:` key in pubspec.yaml. +library; + +import 'package:dox_core/command.dart'; + +import 'src/commands/queue_table_command.dart'; + +export 'src/commands/queue_table_command.dart'; + +/// every command of this package +final List commands = [QueueTableCommand()]; diff --git a/lib/src/commands/queue_table_command.dart b/lib/src/commands/queue_table_command.dart new file mode 100644 index 0000000..7304f7d --- /dev/null +++ b/lib/src/commands/queue_table_command.dart @@ -0,0 +1,53 @@ +import 'dart:io'; + +import 'package:dox_core/command.dart'; + +/// `dox queue:table`: write a migration that makes the `jobs` and +/// `failed_jobs` tables of the database driver (`QueueTables`). +class QueueTableCommand extends Command { + /// [migrations] is `MigrationFile()` (`db/migration`) by default + QueueTableCommand({MigrationFile? migrations}) + : _migrations = migrations ?? MigrationFile(); + + final MigrationFile _migrations; + + @override + String get name => 'queue:table'; + + @override + String get description => + 'Create a migration for the jobs and failed_jobs tables'; + + @override + Future run(List args) async { + String? path = _migrations.create('create_queue_tables', source); + if (path == null) { + print('A create_queue_tables migration is already in db/migration.'); + exitCode = 1; + return; + } + String file = path.split(Platform.pathSeparator).last; + print('\x1B[32m$file migration created successfully.\x1B[0m'); + print('Run `dox migrate` to make the tables.'); + } + + /// the migration file + static const String source = ''' +// ignore_for_file: file_names +import 'package:dox_migration/dox_migration.dart'; +import 'package:dox_queue/dox_queue.dart'; + +/// The jobs and failed_jobs tables of dox_queue. +class CreateQueueTables extends Migration { + @override + Future up(Schema schema) async { + await QueueTables.up(schema); + } + + @override + Future down(Schema schema) async { + await QueueTables.down(schema); + } +} +'''; +} diff --git a/lib/src/queue_tables.dart b/lib/src/queue_tables.dart index f553695..219ae9e 100644 --- a/lib/src/queue_tables.dart +++ b/lib/src/queue_tables.dart @@ -3,7 +3,7 @@ import 'package:dox_orm/dox_orm.dart'; import 'drivers/database_queue_driver.dart'; /// The `jobs` and `failed_jobs` tables of the `database` driver. -/// Add a migration in `db/migration` that calls it: +/// `dox queue:table` writes a migration in `db/migration` that calls it: /// ``` /// class CreateQueueTables extends Migration { /// @override diff --git a/pubspec.yaml b/pubspec.yaml index 9d2f776..c3aa45c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,3 +21,7 @@ dev_dependencies: build_test: ^3.5.23 lints: ^6.1.0 test: ^1.33.0 + +# The `dox` CLI finds the commands of this package here (`dox queue:table`). +dox: + commands: package:dox_queue/commands.dart diff --git a/test/queue_table_command_test.dart b/test/queue_table_command_test.dart new file mode 100644 index 0000000..4ff68a3 --- /dev/null +++ b/test/queue_table_command_test.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +import 'package:dox_core/command.dart'; +import 'package:dox_queue/commands.dart'; +import 'package:test/test.dart'; + +void main() { + late Directory folder; + + setUp(() { + folder = Directory.systemTemp.createTempSync('dox_queue_table_'); + }); + + tearDown(() { + exitCode = 0; + folder.deleteSync(recursive: true); + }); + + test('commands has queue:table', () { + expect(commands.map((Command c) => c.name), contains('queue:table')); + }); + + test('writes the migration once', () async { + QueueTableCommand command = QueueTableCommand( + migrations: MigrationFile(folder: folder), + ); + await command.run([]); + List files = folder.listSync().whereType().toList(); + expect(files, hasLength(1)); + expect(files.first.path, endsWith('_create_queue_tables.dart')); + expect(files.first.readAsStringSync(), contains('QueueTables.up(schema)')); + expect(exitCode, 0); + + await command.run([]); + expect(folder.listSync(), hasLength(1)); + expect(exitCode, 1); + }); +} From 6fe71ae97edf0d0cc5dda131cdd173efc91c1e67 Mon Sep 17 00:00:00 2001 From: necessarylion Date: Mon, 2 Nov 2026 07:08:48 +0700 Subject: [PATCH 2/2] Print the migration file name the same way on Windows Use File(path).uri.pathSegments.last. Add a test with the template's 9-digit migration name. --- lib/src/commands/queue_table_command.dart | 2 +- test/queue_table_command_test.dart | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/src/commands/queue_table_command.dart b/lib/src/commands/queue_table_command.dart index 7304f7d..f5f6289 100644 --- a/lib/src/commands/queue_table_command.dart +++ b/lib/src/commands/queue_table_command.dart @@ -26,7 +26,7 @@ class QueueTableCommand extends Command { exitCode = 1; return; } - String file = path.split(Platform.pathSeparator).last; + String file = File(path).uri.pathSegments.last; print('\x1B[32m$file migration created successfully.\x1B[0m'); print('Run `dox migrate` to make the tables.'); } diff --git a/test/queue_table_command_test.dart b/test/queue_table_command_test.dart index 4ff68a3..9de1ffa 100644 --- a/test/queue_table_command_test.dart +++ b/test/queue_table_command_test.dart @@ -20,6 +20,16 @@ void main() { expect(commands.map((Command c) => c.name), contains('queue:table')); }); + test('does not add a second one to an app from the template', () async { + /// the template has a 9-digit time in this file name + File('${folder.path}/2026_10_25_000000000_create_queue_tables.dart') + .writeAsStringSync('// template'); + await QueueTableCommand(migrations: MigrationFile(folder: folder)) + .run([]); + expect(folder.listSync(), hasLength(1)); + expect(exitCode, 1); + }); + test('writes the migration once', () async { QueueTableCommand command = QueueTableCommand( migrations: MigrationFile(folder: folder),