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..f5f6289 --- /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 = File(path).uri.pathSegments.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..9de1ffa --- /dev/null +++ b/test/queue_table_command_test.dart @@ -0,0 +1,48 @@ +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('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), + ); + 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); + }); +}