Directus Sandbox 的 sandboxes() 多沙箱并行 API:从函数签名到多数据库 e2e 实践
@directus/sandbox 是 Directus 仓库内用于「快速拉起/销毁一套可直接访问的 Directus 实例」的测试工具集(源码位于 tests/sandbox),其入口 API 除单个实例的 sandbox() 外,还包括本文核心的 sandboxes()——一次并行启动多套、可跨不同数据库的 Directus 沙箱。读完本文,你将掌握 sandboxes() 的函数签名与参数语义、Sandboxes 返回值结构、其底层 docker→bootstrap→schema→api 的启动流水线,并能直接参考 tests/e2e/setup/global-setup-all.ts 写出「一套代码跑遍 7 种数据库」的端到端测试基建。
一、什么是 sandboxes():与单实例 sandbox() 的分工
sandbox.ts 通过 tests/sandbox/src/index.ts 对外导出全部类型与函数,其中提供了两个 JS API:
sandbox(database, options?):针对单一数据库启动一组 Directus 实例;sandboxes(sandboxOptions, options?):针对一组数据库/多组配置并行启动多套沙箱,适合矩阵式测试与横向对比。
而 sandboxes() 的独立类型参考页位于 tests/sandbox/docs/_media/sandboxes.md,包内总览见 tests/sandbox/readme.md。
二、函数签名逐参数解析
sandboxes() 的类型声明(来自 sandboxes.md)为:
sandboxes(
sandboxes: SandboxesOptions,
options?: Partial<Pick<Options, 'watch' | 'build' | 'dev'>>,
): Promise<Sandboxes>
参数 1:sandboxes —— 每套沙箱的规格清单
类型为 SandboxesOptions,即一组 { database, options } 对象的数组:
type SandboxesOptions = {
database: Database; // 该套沙箱使用的数据库
options: DeepPartial<Omit<Options, 'build' | 'dev' | 'watch' | 'export'>>; // 该套沙箱各自的启动选项
}[];
其中 Database 由源码 sandbox.ts:158-166 中 databases 常量限定为 'maria' | 'cockroachdb' | 'mssql' | 'mysql' | 'oracle' | 'postgres' | 'sqlite'。注意:build、dev、watch、export 被排除在单套选项之外——它们是「全局级」行为,必须通过第二参数统一控制,避免在多套沙箱间出现状态不一致。传入的数据库若不在白名单内,源码会在启动前直接抛错:
if (!sandboxOptions.every((sandbox) => databases.includes(sandbox.database)))
throw new Error('Invalid database provided'); // sandbox.ts:177-178
参数 2:options? —— 所有沙箱共享的运行模式
Partial<Pick<Options, 'watch' | 'build' | 'dev'>> 表示只允许覆写三个全局开关,含义(见 Options 类型 与 CLI 帮助文本):
| 选项 | 类型 | 说明 |
|---|---|---|
build |
boolean |
每次启动前从源码重新构建 Directus(对应 CLI -b, --build) |
dev |
boolean |
以开发者模式启动 API;与 build 互斥(对应 -d, --dev) |
watch |
boolean |
文件变更时自动重启 API,用于快速迭代(对应 -w, --watch) |
源码 getOptions() 会将默认值与用户传入做深合并,未提供时各默认值为:build/dev/watch=false、端口取 options.port ?? process.env['PORT'] ?? 8055、instances='1'、app=false、schema=undefined、全部 extras(redis/maildev/minio/saml/license)为 false、cache=false、knex=false。若传入 schema: true,会被归一化为默认快照文件 snapshot.json(仓库根已提供 tests/sandbox/snapshot.json)。
三、返回值:Sandboxes 结构与能力
Promise<Sandboxes> 的具体结构见 Sandboxes 类型,对应源码 sandbox.ts:92-101:
type Sandboxes = {
sandboxes: {
apis: [Api, ...Api[]]; // 至少一个已启动的 API 进程
env: Env; // 该套沙箱生效的完整环境变量(含自动分配的端口)
logger: Logger;
knex?: Knex; // 仅当 options.knex=true 时存在,可直接访问数据库
}[];
restartApis(): Promise<void>; // 重启所有套下的所有 API 进程
stop(): Promise<void>; // 统一销毁构建进程、knex、API 与 license server
};
关键语义:
sandboxes数组顺序与传入的SandboxesOptions一一对应,可用下标建立「数据库 → env/apis」映射;- 每套里的
apis是元组[Api, ...Api[]],配合Options.instances可实现单库多实例水平扩容(源码注释见 sandbox.ts:52); env是调试与请求时最常用的对象——sb.env.PUBLIC_URL即该实例的对外地址,README 示例即用fetch(sb.env.PUBLIC_URL + '/items/articles')访问 REST 接口。
四、启动流水线:每套沙箱背后发生了什么
包主文档 readme.md 的 "Inner workings" 一节将单套沙箱的生命周期归纳为若干可裁剪步骤,而 sandboxes() 对每一套沙箱都会执行相同管线(见 sandbox.ts:207-228):
- (可选)重建 Directus:全局
options.build && !options.dev成立时先执行buildApi(源码在 tests/sandbox/src/steps 下); - 拉起 docker 容器:
dockerUp(database, opts, env, logger)按数据库启动容器并等待其 healthy。若同名容器仍在运行则直接复用而非重建——这正是并发跑多套沙箱时能显著提速的关键;容器编排模板位于 tests/sandbox/src/docker(如postgres.yml、mysql.yml、redis.yml); - 引导数据库:
bootstrap(...)确保系统表齐全;未初始化则自动建表; - 加载 schema 快照:若该套
options.schema有值,则loadSchema(...)在 API 启动前把快照灌入库(同一快照文件可共享给不同数据库,用于验证跨库一致性); - 可选建 knex:
options.knex=true时用createDatabase(env, logger)打开直连句柄,随后会触发options.hooks.beforeApi?.({ env, logger, knex })生命周期钩子(在 API 启动前做数据准备); - 启动 API:
startApi(...)以正确环境变量拉起一个或多个 API 进程; - 若开启了
extras.license,还会并行拉起 tests/sandbox/src/steps/license.ts 对应的 mock license server,并在stop()时一并回收。
多套沙箱彼此以 Promise.all 并行启动,相互独立;日志支持 prefix 前缀区分(源码 sandbox.ts:211),当你在终端同时观察多个数据库沙箱时,前缀能让你一眼分清输出来源。
restartApis 与 stop 的语义
restartApis()(sandbox.ts:234-240)会先kill所有既有 API 进程,再基于每套各自保存的opts/env重新startApi,因此它保留的是各套沙箱的原配置,适合「改完代码/权限后热重启全部实例」;stop()(sandbox.ts:242-253)负责完整清理:kill 构建进程 → 逐套knex.destroy()→ kill 各 API → kill license server。测试 suite 结束时务必调用它,否则子进程与 docker 容器会残留。
五、仓库内的真实用法:e2e 多数据库矩阵
sandboxes() 在仓库里最典型的落地场景是端到端测试的全局 setup。以 tests/e2e/setup/global-setup-all.ts 为例:
import { type Database, databases, sandboxes, type Sandboxes } from '@directus/sandbox';
const dbs = databases.map((database, index) => {
const port = 8000 + index * 100; // 每库错开端口段
return {
database,
options: {
prefix: database, // 日志前缀便于区分
port,
env: { CACHE_SCHEMA: 'false', LICENSE_KEY: 'D0000-...' },
docker: { port: port + 10, keep: true }, // docker 端口也错开且停止后保留
extras: { license: true },
killPorts: true,
},
};
});
sb = await sandboxes(dbs); // 7 种数据库并行拉起
// 将每套 env 按数据库名提供给测试项目
project.provide('envs', Object.fromEntries(
sb.sandboxes.map((sandbox, index) => [dbs[index]!.database, sandbox.env]),
));
// 对应 teardown 里:await sb.stop();
这段代码展示了 sandboxes() 的完整协作模式:
- 用源码导出的
databases常量(见 databases 变量文档)自动生成 7 库矩阵,未来新增数据库无需手改测试文件; - 每库用
prefix、port、docker.port隔离,互不干扰; docker.keep: true让容器在stop()后依然存活,下一次运行直接复用容器,只重建 API 进程,把重复测试的冷启动成本降到最低;killPorts会强制清理目标端口的占用进程(对应 CLI--killPorts);- setup 完成后通过
sb.sandboxes[i].env把各自PUBLIC_URL等交给具体用例。
六、CLI 对照:命令行形态的 sandboxes
虽然 sandboxes() 是纯 JS 多实例 API,但若你只需要「以某数据库快速起一套」,可使用同包 CLI(bin.sandbox 指向 dist/cli.js,包清单见 tests/sandbox/package.json):
Usage: sandbox [options] <database>
# 例:以 postgres 起开发者沙箱并开启文件监听
pnpm sandbox postgres --dev --watch
CLI 的 database 参数同样限定为 maria/cockroachdb/mssql/mysql/oracle/postgres/sqlite;-i, --instances 对应水平扩容、-e, --extras <extras> 对应 redis/maildev/saml/minio/license 等附加服务、-x/--export 会每 2 秒导出一次 schema 快照。CLI 顶层选项中的多数(如 build/dev/watch/export)即映射为 sandboxes() 第二参数可覆写的全局行为。
七、使用建议与注意事项
build与dev互斥:源码中构建仅在opts.build && !opts.dev时发生(sandbox.ts:203),日常迭代优先dev/watch,只有需要验证产物时才用build;- 并行度权衡:
sandboxes()同时拉起多套数据库 + 多个 API 进程,对内存与 docker 资源消耗显著,请按机器能力裁剪SandboxesOptions数组长度(例如仅测试自己关心的['postgres', 'sqlite']); - 端口规划:多套沙箱必须显式分配不同
port与docker.port,否则会互相抢占;也可留给getPort自动探测空闲端口(底层使用get-port包); - 善用
docker.keep:容器复用让「再次运行同一批测试」几乎只花 API 启动的时间,代价是stop()不会清理容器,需要时可用--docker.keep=false或手动回收; - 清理务必成对:在 vitest
teardown或afterAll中调用await sb.stop(),避免遗留子进程与 license server。
sandboxes() 的价值在于把「多数据库 × 多实例」这套复杂编排压缩成一个 Promise 调用:容器编排、数据库引导、快照灌入、API 启动与统一回收全部交给 tests/sandbox/src/sandbox.ts 的固定管线处理,让测试作者只需关心 { database, options } 的声明式清单——这正是 Directus 仓库能对其 API 兼容的 7 种数据库做矩阵式端到端验证的工程基础。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00