Skip to content
Open
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
4 changes: 4 additions & 0 deletions docs/.vitepress/sidebarTutorials.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ const sidebarTutorials = [
{
text: 'Local Extension Development',
link: 'tutorials/local-extension-development.md'
},
{
text: 'Creating Command Line Tasks',
link: 'tutorials/creating-command-line-tasks.md'
}
]
},
Expand Down
325 changes: 313 additions & 12 deletions docs/reference/modules/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ extends: '@apostrophecms/module'

<AposRefExtends :module="$frontmatter.extends" />

This module allows support modules in creating command line tasks. It also provides utilities for generating request objects when one is not available and is needed.
This module provides the functionality needed to create and run command line tasks. It also provides utilities for generating request objects when one is not available and is needed.

Command line tasks are invoked with the general structure:

Expand All @@ -22,9 +22,14 @@ node app article:generate --total=20

Apostrophe is fully initialized before a task is run, except that it does not listen for connections. We may access all general Apostrophe features in a task.

::: info
**New to creating tasks?** See the [Creating Command-Line Tasks tutorial](/tutorials/creating-command-line-tasks.md) for a step-by-step guide with practical examples.
:::

## Related documentation

- [Module task configuration](/reference/module-api/module-overview.md#tasks-self)
- [Module task configuration](/reference/module-api/module-overview.md#tasks-self) - How to define tasks in your module
- [Creating Command-Line Tasks tutorial](/tutorials/creating-command-line-tasks.html) - Complete guide to building tasks

## Featured methods

Expand All @@ -49,12 +54,15 @@ If present, **the `args` argument should be an array of positional arguments** t

If present, **the `options` argument is an object that contains optional parameters** that would normally be hyphenated, i.e. at the command line you might write `--total=20`. This can be passed as the second argument if `args` is omitted.

```
# CLI equivalent: node app @apostrophecms/user:add 'alf' 'admin'
await self.apos.task.invoke('@apostrophecms/user:add', [ 'alf', 'admin' ])
```javascript
// CLI equivalent: node app @apostrophecms/user:add 'alf' 'admin'
await self.apos.task.invoke('@apostrophecms/user:add', [ 'alf', 'admin' ]);

// CLI equivalent: node app product:generate --total=20
await self.apos.task.invoke('product:generate', { total: 20 });

# CLI equivalent: node app product:generate --total=20
await self.apos.task.invoke('product:generate', { total: 20 })
// With both positional arguments and options
await self.apos.task.invoke('article:generate', [ 'technology' ], { total: 20 });
```

The `args` and `options` arguments may be completely omitted, though individual tasks should indicate whether arguments are required when they are run.
Expand Down Expand Up @@ -83,23 +91,316 @@ The methods below provide quick access to create request objects for each role.

Other properties of `options` are assigned as properties of the returned `req` object before any initialization tasks (such as computing `req.absoluteUrl`).

#### Example: Using getReq in a task

```javascript
tasks(self) {
return {
'list-articles': {
usage: 'List all articles in the database',
async task(argv) {
// Database queries require a request object
const req = self.apos.task.getReq();

// Now we can query the database with admin permissions
const articles = await self.find(req).toArray();

console.log(`Found ${articles.length} articles:`);
for (const article of articles) {
console.log(`- ${article.title} (${article.aposMode})`);
}
}
}
};
}
```

#### Example: Choosing the appropriate permission level

```javascript
tasks(self) {
return {
'compare-visibility': {
usage: 'Compare what admins vs anonymous users can see',
async task(argv) {
// Get all content with admin permissions
const adminReq = self.apos.task.getReq();
const allArticles = await self.find(adminReq).toArray();

// Get only published content (what anonymous users see)
const anonReq = self.apos.task.getAnonReq();
const publicArticles = await self.find(anonReq).toArray();

console.log(`Admin can see: ${allArticles.length} articles`);
console.log(`Public can see: ${publicArticles.length} articles`);
console.log(`Unpublished: ${allArticles.length - publicArticles.length} articles`);
}
}
};
}
```

#### Example: Specifying locale in a task

```javascript
tasks(self) {
return {
'list-french': {
usage: 'List articles in the French locale',
async task(argv) {
// Create a request object with a specific locale
const req = self.apos.task.getReq({
locale: 'fr',
mode: 'published'
});

const articles = await self.find(req).toArray();

console.log(`Found ${articles.length} French articles`);
}
}
};
}
```

### `getAnonReq(options)`

A convenience wrapper for `getReq`. This returns a request object simulating an anonymous site visitor, with no role and no `req.user`. See [`getReq`](#getreq-options) for information about the `options` argument.
A convenience wrapper for `getReq`. This returns a request object simulating an anonymous site visitor, with no role and no `req.user`.

**When to use:** Use `getAnonReq` when you need to see content exactly as unauthenticated visitors would see it - typically only published content that has public visibility.

See [`getReq`](#getreq-options) for information about the `options` argument.

#### Example: Testing public visibility

```javascript
tasks(self) {
return {
'test-public-access': {
usage: 'Test what anonymous users can access',
async task(argv) {
const req = self.apos.task.getAnonReq();

// This will only return published, public content
const articles = await self.find(req).toArray();

console.log(`Anonymous users can see ${articles.length} articles`);

// Test a specific article
const slug = argv.slug || 'test-article';
const article = await self.find(req, { slug }).toOne();

if (article) {
console.log(`✓ Article "${slug}" is publicly accessible`);
} else {
console.log(`✗ Article "${slug}" is NOT publicly accessible`);
}
}
}
};
}
```

### `getGuestReq(options)`

A convenience wrapper for `getReq`. This returns a request object simulating a user with the `guest` role. See [`getReq`](#getreq-options) for information about the `options` argument.
A convenience wrapper for `getReq`. This returns a request object simulating a user with the `guest` role.

**When to use:** Use this when you need to test how content appears to logged-in users with minimal permissions (guests can typically view more than anonymous users but cannot edit content).

See [`getReq`](#getreq-options) for information about the `options` argument.

### `getContributorReq(options)`

A convenience wrapper for `getReq`. This returns a request object simulating a user with the `contributor` role. See [`getReq`](#getreq-options) for information about the `options` argument.
A convenience wrapper for `getReq`. This returns a request object simulating a user with the `contributor` role.

**When to use:** Contributors can typically create and edit their own content but cannot publish it. Use this to test workflows or content visibility at the contributor permission level.

See [`getReq`](#getreq-options) for information about the `options` argument.

#### Example: Simulating contributor permissions

```javascript
tasks(self) {
return {
'test-contributor-workflow': {
usage: 'Test what contributors can do with content',
async task(argv) {
const req = self.apos.task.getContributorReq();

// Try to create a draft article
const article = {
title: 'Contributor Test Article',
aposMode: 'draft'
};

try {
await self.insert(req, article);
console.log('✓ Contributor can create draft articles');
} catch (error) {
console.log('✗ Contributor cannot create articles:', error.message);
}

// Try to publish (this should fail)
article.aposMode = 'published';
try {
await self.update(req, article);
console.log('✓ Contributor can publish articles');
} catch (error) {
console.log('✗ Contributor cannot publish articles (expected)');
}
}
}
};
}
```

### `getEditorReq(options)`

A convenience wrapper for `getReq`. This returns a request object simulating a user with the `editor` role. See [`getReq`](#getreq-options) for information about the `options` argument.
A convenience wrapper for `getReq`. This returns a request object simulating a user with the `editor` role.

**When to use:** Editors can create, edit, and publish content but typically cannot manage users or access admin-only features. Use this to test content management workflows at the editor permission level.

See [`getReq`](#getreq-options) for information about the `options` argument.

### `getAdminReq(options)`

A convenience wrapper for `getReq`. This returns a request object simulating a user with the `admin` role. This is the default behavior of `getReq()`. See [`getReq`](#getreq-options) for information about the `options` argument.
A convenience wrapper for `getReq`. This returns a request object simulating a user with the `admin` role. This is the default behavior of `getReq()`.

**When to use:** Use this (or simply `getReq()`) for most tasks where you need full access to all content, including unpublished drafts, archived content, and admin-only features. This is the most common choice for maintenance tasks, data generation, and migrations.

See [`getReq`](#getreq-options) for information about the `options` argument.

#### Example: Admin-level maintenance task

```javascript
tasks(self) {
return {
'cleanup-old-drafts': {
usage: 'Delete draft articles older than 90 days',
async task(argv) {
// Use admin permissions to access all drafts
const req = self.apos.task.getAdminReq();

const ninetyDaysAgo = new Date();
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);

// Find old drafts (admin can see drafts, others cannot)
const oldDrafts = await self.find(req, {
aposMode: 'draft',
updatedAt: { $lt: ninetyDaysAgo }
}).toArray();

console.log(`Found ${oldDrafts.length} drafts older than 90 days`);

if (!argv['dry-run']) {
for (const draft of oldDrafts) {
await self.delete(req, draft);
console.log(`✓ Deleted: ${draft.title}`);
}
} else {
console.log('DRY RUN - No articles were deleted');
}
}
}
};
}
```

## Common Patterns

### Pattern: Dry run option

Always include a `--dry-run` option for tasks that modify or delete data:

```javascript
tasks(self) {
return {
'dangerous-operation': {
usage: 'Perform a destructive operation. Use --dry-run to preview changes.',
async task(argv) {
const req = self.apos.task.getReq();
const dryRun = argv['dry-run'];

// Find items to modify
const items = await self.find(req, { /* criteria */ }).toArray();

if (dryRun) {
console.log(`DRY RUN: Would modify ${items.length} items`);
return;
}

// Perform actual modifications
for (const item of items) {
await self.update(req, item);
}
}
}
};
}
```

### Pattern: Progress reporting

For long-running tasks, report progress to keep users informed:

```javascript
tasks(self) {
return {
'process-many': {
usage: 'Process a large number of items',
async task(argv) {
const req = self.apos.task.getReq();
const items = await self.find(req).toArray();
const total = items.length;

console.log(`Processing ${total} items...`);

for (let i = 0; i < items.length; i++) {
await processItem(items[i]);

// Report progress every 10 items
if ((i + 1) % 10 === 0) {
console.log(`Progress: ${i + 1}/${total} (${Math.round((i + 1) / total * 100)}%)`);
}
}

console.log('✓ Complete!');
}
}
};
}
```

### Pattern: Validating required arguments

Always validate that required arguments are present:

```javascript
tasks(self) {
return {
'require-args': {
usage: 'Task that requires arguments.\nUsage: node app module:require-args <filename> --format=json',
async task(argv) {
// Check positional argument
const filename = argv._[1];
if (!filename) {
console.error('Error: filename argument is required');
console.log(this.usage);
process.exit(1);
}

// Check named option
const format = argv.format;
if (!format) {
console.error('Error: --format option is required');
console.log(this.usage);
process.exit(1);
}

// Proceed with task
console.log(`Processing ${filename} in ${format} format...`);
}
}
};
}
```
Loading