Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## Unreleased

- Fix: with `Mail.queue(message, encrypt: true)`, a send error could name the recipient (eg. an SMTP reply `550 <ann@example.com>: 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.
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -121,8 +130,6 @@ class CreateQueueTables extends Migration {
}
```

Then run `dox migrate`.

## Write a job

`lib/app/jobs/send_welcome_email.job.dart`:
Expand Down
15 changes: 11 additions & 4 deletions docs/queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions lib/commands.dart
Original file line number Diff line number Diff line change
@@ -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<Command> commands = <Command>[QueueTableCommand()];
53 changes: 53 additions & 0 deletions lib/src/commands/queue_table_command.dart
Original file line number Diff line number Diff line change
@@ -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<void> run(List<String> 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<void> up(Schema schema) async {
await QueueTables.up(schema);
}

@override
Future<void> down(Schema schema) async {
await QueueTables.down(schema);
}
}
''';
}
2 changes: 1 addition & 1 deletion lib/src/queue_tables.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
48 changes: 48 additions & 0 deletions test/queue_table_command_test.dart
Original file line number Diff line number Diff line change
@@ -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(<String>[]);
expect(folder.listSync(), hasLength(1));
expect(exitCode, 1);
});

test('writes the migration once', () async {
QueueTableCommand command = QueueTableCommand(
migrations: MigrationFile(folder: folder),
);
await command.run(<String>[]);
List<File> files = folder.listSync().whereType<File>().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(<String>[]);
expect(folder.listSync(), hasLength(1));
expect(exitCode, 1);
});
}
Loading