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
28 changes: 28 additions & 0 deletions docs/docs/guides/developer-guide/plugins/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,34 @@ If you have code that should only run either in the server context or worker con
you can inject the [ProcessContext provider](/reference/typescript-api/common/process-context/).
:::

A plugin can implement these hooks directly, rather than only on the providers it defines:

```ts title="src/plugins/my-plugin/my.plugin.ts"
import { OnApplicationBootstrap } from '@nestjs/common';
import { PluginCommonModule, ProcessContext, VendurePlugin } from '@vendure/core';

@VendurePlugin({
imports: [PluginCommonModule],
})
export class MyPlugin implements OnApplicationBootstrap {
constructor(private processContext: ProcessContext) {}

async onApplicationBootstrap() {
// Without this guard the work below would run twice: once in the
// server and once in the worker.
if (this.processContext.isWorker) {
return;
}
// Do something on the server only
}
}
```

If that startup work needs to call Vendure's services, it will need a `RequestContext`, which
outside of the request-response cycle you create yourself — see
[Creating a RequestContext](/guides/developer-guide/stand-alone-scripts/#creating-a-requestcontext),
including how to give it a user so that permission checks pass.

Comment on lines +137 to +141

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for existing links to the stand-alone-scripts guide to verify the correct URL path.

rg -ho '\[.*?\]\(/[^)]+stand-alone-scripts[^)]*\)' docs/docs/

Repository: vendurehq/vendure

Length of output: 8046


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Relevant links ==\n'
rg -n --no-heading --glob 'docs/docs/**/*.mdx' --glob 'docs/docs/**/*.md' 'stand-alone-scripts|Creating a RequestContext|/guides/developer-guide/|/developer-guide/' docs/docs || true

printf '\n== Target file excerpt ==\n'
sed -n '120,150p' docs/docs/guides/developer-guide/plugins/index.mdx

printf '\n== Docs config / routing hints ==\n'
rg -n --no-heading 'slug|routeBasePath|guides/developer-guide|developer-guide' docs/docs docs -g '!**/node_modules/**' || true

Repository: vendurehq/vendure

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Targeted file list ==\n'
git ls-files 'docs/docs/**' | sed -n '1,200p'

printf '\n== Direct references to the target page ==\n'
rg -n --no-heading 'stand-alone-scripts/index\.mdx|Creating a RequestContext|API Layer' docs/docs || true

Repository: vendurehq/vendure

Length of output: 14150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

root = Path("docs/docs")
for p in root.rglob("*.mdx"):
    text = p.read_text(encoding="utf-8")
    for m in re.finditer(r'\[[^\]]+\]\((/[^)]+stand-alone-scripts[^)]*)\)', text):
        print(f"{p}:{m.start(1)}:{m.group(1)}")
PY

Repository: vendurehq/vendure

Length of output: 589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and the referenced guide file paths.
printf '\n== Target excerpt ==\n'
sed -n '130,145p' docs/docs/guides/developer-guide/plugins/index.mdx

printf '\n== stand-alone-scripts file location(s) ==\n'
fd -a 'index.mdx' docs/docs | rg 'stand-alone-scripts|developer-guide'

printf '\n== Surrounding frontmatter / slugs ==\n'
for f in $(fd -a 'index.mdx' docs/docs | rg 'stand-alone-scripts|developer-guide'); do
  echo "--- $f ---"
  sed -n '1,40p' "$f"
done

Repository: vendurehq/vendure

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "FILES:"
git ls-files 'docs/docs/guides/developer-guide/**/index.mdx' | sed -n '1,120p'

Repository: vendurehq/vendure

Length of output: 2341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Docusaurus / docs config files ==\n'
fd -a 'docusaurus.config.*|sidebars.*|docs.*config*' . | sed -n '1,80p'

printf '\n== Route base path references ==\n'
rg -n --no-heading 'routeBasePath|slug:|path:|docs:|guides' docusaurus.config.* docs -g '!**/node_modules/**' | sed -n '1,200p' || true

printf '\n== Slug/frontmatter for target and related guides ==\n'
for f in \
  docs/docs/guides/developer-guide/plugins/index.mdx \
  docs/docs/guides/developer-guide/stand-alone-scripts/index.mdx \
  docs/docs/guides/developer-guide/ai-assisted-development/index.mdx
do
  echo "--- $f ---"
  sed -n '1,20p' "$f"
done

Repository: vendurehq/vendure

Length of output: 22971


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== developer-guide manifest entries ==\n'
rg -n --no-heading -A4 -B2 "developer-guide|stand-alone-scripts|plugins" docs/src/manifest.ts | sed -n '1,220p'

printf '\n== plugin page links ==\n'
rg -n --no-heading '/guides/developer-guide/|/developer-guide/' docs/docs/guides/developer-guide/plugins/index.mdx docs/docs/guides/developer-guide/stand-alone-scripts/index.mdx | sed -n '1,120p'

Repository: vendurehq/vendure

Length of output: 11670


Fix the internal docs link

The RequestContext link should use /developer-guide/stand-alone-scripts/#creating-a-requestcontext to match the Developer Guide routes. Also drop the extra “of” in outside of the.

📝 Proposed fixes
  If that startup work needs to call Vendure's services, it will need a `RequestContext`, which
- outside of the request-response cycle you create yourself — see
- [Creating a RequestContext](/guides/developer-guide/stand-alone-scripts/#creating-a-requestcontext),
+ outside the request-response cycle you create yourself — see
+ [Creating a RequestContext](/developer-guide/stand-alone-scripts/#creating-a-requestcontext),
  including how to give it a user so that permission checks pass.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
If that startup work needs to call Vendure's services, it will need a `RequestContext`, which
outside of the request-response cycle you create yourself — see
[Creating a RequestContext](/guides/developer-guide/stand-alone-scripts/#creating-a-requestcontext),
including how to give it a user so that permission checks pass.
If that startup work needs to call Vendure's services, it will need a `RequestContext`, which
outside the request-response cycle you create yourself — see
[Creating a RequestContext](/developer-guide/stand-alone-scripts/#creating-a-requestcontext),
including how to give it a user so that permission checks pass.
🧰 Tools
🪛 LanguageTool

[style] ~137-~137: This phrase is redundant. Consider using “outside”.
Context: ... it will need a RequestContext, which outside of the request-response cycle you create y...

(OUTSIDE_OF)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/guides/developer-guide/plugins/index.mdx` around lines 137 - 141,
Update the prose around the RequestContext guidance to say “outside the
request-response cycle,” and change the “Creating a RequestContext” link target
to /developer-guide/stand-alone-scripts/#creating-a-requestcontext while
preserving the surrounding explanation.

Source: Linters/SAST tools

### Configure

Another hook that is not strictly a lifecycle hook, but which can be useful to know is the [`configure` method](https://docs.nestjs.com/middleware#applying-middleware) which is
Expand Down
46 changes: 46 additions & 0 deletions docs/docs/guides/developer-guide/stand-alone-scripts/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,49 @@ async function getProductCount() {
const { totalItems } = await productService.findAll(ctx, {take: 0});
}
```

### Acting as a specific user

The context above is anonymous — it carries no user, and therefore no permissions. That is fine for
services which do not check them, but services such as the [`RoleService`](/reference/typescript-api/services/role-service/)
will reject it.

To act as a particular user, pass that user in the `user` option. The example below loads the
superadmin, which is usually what you want for an administrative script:

```ts title="src/get-product-count.ts"
// ...
import { ConfigService, RequestContextService, TransactionalConnection, User } from '@vendure/core';

async function getProductCount() {
const { app } = await bootstrapWorker(config);

// Load the user the script should act as. Any User will do — the superadmin is used here
// because an administrative script generally needs unrestricted permissions.
const { superadminCredentials } = app.get(ConfigService).authOptions; // [!code highlight]
const superAdminUser = await app // [!code highlight]
.get(TransactionalConnection) // [!code highlight]
.rawConnection.getRepository(User) // [!code highlight]
.findOneOrFail({ // [!code highlight]
where: { identifier: superadminCredentials.identifier }, // [!code highlight]
// The roles (and their channels) must be loaded, or the resulting context // [!code highlight]
// has the user's id but no permissions. // [!code highlight]
relations: { roles: { channels: true } }, // [!code highlight]
}); // [!code highlight]

const ctx = await app.get(RequestContextService).create({
apiType: 'admin',
user: superAdminUser, // [!code highlight]
});

// `ctx` now carries the superadmin's permissions, so permission-checking
// services will accept it.
}
```

:::note
Loading the `roles` relation (and its `channels`) is what gives the context its permissions. Omit
it and the context still has the user's id — enough for services that reload the user by id, but
its `channelPermissions` are empty, so any check that reads them directly (for example the entity
access-control checks behind `connection.getRepository(ctx, Entity)`) will deny access.
:::
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ class RequestContextService {
Creates a RequestContext based on the config provided. This can be useful when interacting
with services outside the request-response cycle, for example in stand-alone scripts or in
worker jobs.

Without a `user`, the resulting context is anonymous and carries no permissions, which
services that perform permission checks will reject. Pass the `User` the context should act
as — commonly the superadmin for administrative scripts:

```ts
const { superadminCredentials } = this.configService.authOptions;
const superAdminUser = await this.connection.rawConnection.getRepository(User).findOneOrFail({
where: { identifier: superadminCredentials.identifier },
// The roles (and their channels) must be loaded, or the resulting context
// has the user's id but no permissions.
relations: { roles: { channels: true } },
});

const ctx = await this.requestContextService.create({
apiType: 'admin',
user: superAdminUser,
});
```
### fromRequest

<MemberInfo kind="method" type={`(req: Request, info?: GraphQLResolveInfo, requiredPermissions?: <a href='/reference/typescript-api/common/permission#permission'>Permission</a>[], session?: <a href='/reference/typescript-api/auth/session-cache-strategy#cachedsession'>CachedSession</a>) => Promise<<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>>`} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ export class RequestContextService {
* with services outside the request-response cycle, for example in stand-alone scripts or in
* worker jobs.
*
* Without a `user`, the resulting context is anonymous and carries no permissions, which
* services that perform permission checks will reject. Pass the `User` the context should act
* as — commonly the superadmin for administrative scripts:
*
* ```ts
* const { superadminCredentials } = this.configService.authOptions;
* const superAdminUser = await this.connection.rawConnection.getRepository(User).findOneOrFail({
* where: { identifier: superadminCredentials.identifier },
* // The roles (and their channels) must be loaded, or the resulting context
* // has the user's id but no permissions.
* relations: { roles: { channels: true } },
* });
*
* const ctx = await this.requestContextService.create({
* apiType: 'admin',
* user: superAdminUser,
* });
* ```
*
* @since 1.5.0
*/
async create(config: {
Expand Down
Loading