Directus 多沙箱并行编排指南:深入解读 @directus/sandbox 的 sandboxes() 函数
Directus 多沙箱并行编排指南深入解读 directus/sandbox 的 sandboxes() 函数【免费下载链接】directusThe flexible backend for all your projects Turn your DB into a headless CMS, admin panels, or apps with a custom UI, instant APIs, auth more.项目地址: https://gitcode.com/GitHub_Trending/di/directussandboxes()是 Directus 仓库内测试沙箱包directus/sandbox提供的多实例编排入口用于在同一进程中同时拉起多套、可基于不同数据库的 Directus API 实例并统一管理其启动、重启与回收。它主要服务于仓库中 tests/e2e 这类需要全数据库矩阵覆盖的端到端测试也可供本地开发者在多种数据库环境下并行验证同一份代码行为。读完本文你将掌握sandboxes()的完整签名、参数契约、并行生命周期以及如何用它替代逐个手动启动的繁琐流程。本文以 tests/sandbox/docs/functions/sandboxes.md 的 API 参考为骨架辅以 tests/sandbox/src/sandbox.ts 及其依赖模块的源码进行展开。注意仓库中的 typedoc 文档与源码可能存在修订版本差异例如文档标注的定义行号与当前源码不符文中的实现细节一律以当前源码为准。一、函数签名与核心契约sandboxes()的函数签名如下摘录自 sandboxes.md类型定义见 SandboxesOptions、Sandboxessandboxes( sandboxes: SandboxesOptions, options?: PartialPickOptions, watch | build | dev, ): PromiseSandboxes对照当前源码tests/sandbox/src/sandbox.ts:173-256该函数入参一sandboxes描述要启动的一组沙箱配置的数组入参二options?可选的全局开关只能覆盖Options中watch、build、dev三个字段返回值一个PromiseSandboxes聚合句柄可对整个沙箱集合统一执行restartApis()与stop()。三者之间全局 vs 单沙箱的职责划分非常明确与编译方式相关的三项开关是全局的一旦开启就作用于所有沙箱而端口、数据库、Docker、环境变量等则属于每个沙箱各自的配置。二、参数详解2.1 第一参数 SandboxesOptions一份沙箱清单SandboxesOptions的完整类型为对象数组源码 tests/sandbox/src/sandbox.ts:168-171type SandboxesOptions { database: Database; options: DeepPartialOmitOptions, build | dev | watch | export; }[];每个元素由两部分组成字段类型含义databaseDatabase沙箱要连接的数据库客户端类型optionsDeepPartialOmitOptions, build\|dev\|watch\|export该沙箱的深度部分配置且明确排除build/dev/watch/export四项这里有几层值得注意的约束database的可选值有严格白名单。源码中维护了一个常量数组tests/sandbox/src/sandbox.ts:158-166export const databases: Database[] [ maria, cockroachdb, mssql, mysql, oracle, postgres, sqlite, ] as const;sandboxes()在启动前会逐项校验sandbox.ts:177-178传入白名单之外的数据库名会直接抛出Invalid database provided且不会启动任何子进程。每个沙箱的options使用深度合并deep merge。入口先调用getOptions()sandbox.ts:112-152将传入的深度部分配置与内置默认值合并从而允许你只覆盖关心的字段// getOptions 内部merge(defaults, options) // 默认值包括 // build: false, dev: false, watch: false, app: false, // port: 8055默认来自 PORT 环境变量instances: 1, // inspect: true, silent: false, export: false, cache: false, // skipSetup: false, knex: false, // docker: { keep: false, port: undefined, name: undefined, suffix: }, // extras: { redis: false, maildev: false, minio: false, saml: false, license: false },build/dev/watch被逐沙箱禁止它们属于重新编译 Directus 源码这一类全局行为只可能来自第二个参数。逐沙箱配置会被忽略避免多个沙箱各自触发互相冲突的编译。DeepPartial意味着你可以只写{ port: 8055 }而不必补齐docker、extras等所有嵌套字段lodash的merge会负责逐层合并。此外getOptions里有一个便捷分支当options.schema true时会被自动改写为snapshot.jsonsandbox.ts:113方便直接加载仓库默认快照。2.2 第二参数全局的 build / dev / watchoptions?: PartialPickOptions, watch | build | dev对照 Options 类型三个字段的语义为字段默认说明buildfalse每次启动沙箱前先从源码重新编译 Directus见下文重新构建小节devfalse以开发模式启动 Directus通过tsx运行源码而非编译产物与build互斥——源码中二者的判断是if (opts.build !opts.dev)watchfalse监听源码变化并自动重启 API适合快速迭代改动2.3 返回值 Sandboxes一套集合句柄返回值类型源码 sandbox.ts:92-101type Sandboxes { sandboxes: { apis: [Api, ...Api[]]; // 每个沙箱至少一台 APIinstances1 时多台 env: Env; // 该沙箱最终生效的环境变量 logger: Logger; knex?: Knex | undefined; // 仅在开启 knex 选项时存在 }[]; restartApis(): Promisevoid; stop(): Promisevoid; };方法语义restartApis()先kill掉所有沙箱的全部 API 进程再基于各沙箱各自保存的opts/env/logger重新调用startApi()拉起sandbox.ts:234-240。注意它重启的是API 层而非数据库也不会重新加载 schema 快照。stop()完整清理包含顺序为——kill 构建进程 → 逐个knex.destroy()销毁直接数据库连接 → kill 各沙箱的所有 API 进程 → kill 许可服务sandbox.ts:242-253。sandboxes[i]内部的env是该沙箱已经解析完毕、API 进程实际运行时使用的完整环境变量而非你传入的原始配置因此用它去构造 REST/GraphQL 请求非常可靠PUBLIC_URL已被正确设置。三、并行编排生命周期五步流水线sandboxes()的编排核心是一个Promise.all驱动的并行流水线sandbox.ts:207-228对清单中的每个数据库并发执行以下步骤任一失败都会先调用stop()做整体清理再抛出异常。对应 tests/sandbox/docs/README.md 的 Inner workings 说明完整生命周期可拆为1. 可选启动 Mock 许可服务license extra如果任一沙箱配置了extras.license会先通过startLicenseServer()拉起仓库内的 mock 许可服务器tests/sandbox/src/steps/license.ts。此时环境变量会被补上NODE_ENVdevelopment与指向该服务的LICENSE_API_URL见 config.ts:171。2. 可选重新构建 Directus全局 build当opts.build !opts.dev时先在apiFolder下执行pnpm tsc ... --project tsconfig.prod.json完成编译tests/sandbox/src/steps/api.ts:9-60。在watch模式下构建进程会持续监听一旦检测到Watching for file changes.输出就调用restartApis()回调刷新沙箱。3. 启动 Docker 依赖dockerUp根据database类型解析出对应的 compose 文件并docker compose up随后等待容器进入健康状态。仓库内为每种数据库和 extras 都预置了 compose 定义tests/sandbox/src/docker 下的postgres.yml、mysql.yml、redis.yml、minio.yml、saml.yml等。若同名容器仍在运行则直接复用而不是重复创建——这正是文档中所说的如果 docker 容器仍在运行会复用它们的机制。docker配置项中的name、suffix用于控制 Docker 项目名portCLI 中写作--docker.port/--docker.basePort用于指定容器端口范围下限。4. 引导数据库bootstrap通过pnpm tsx .../src/cli/run.ts bootstrapdev 模式或node .../dist/cli/run.js bootstrap生产模式执行 Directus 的 bootstrap 命令确保必要的系统表已创建tests/sandbox/src/steps/api.ts:62-95。若数据库已 bootstrap 过则跳过。5. 加载 schema 快照可选若该沙箱设置了schema选项如默认快照snapshot.json则在启动 API 前把快照应用到数据库。此处使用了tests/sandbox/snapshot.json这类预置快照文件。6. 建立直接数据库连接与前置钩子可选opts.knex为真时通过createDatabase()建立一个 Knex 连接暴露在sandbox.knex上供测试直连数据库做断言opts.hooks.beforeApi若存在会在bootstrap schema 加载完成、但 API 尚未监听端口的间隙执行回调拿到{ env, logger, knex }上下文sandbox.ts:221适合预置测试数据或调整配置。7. 启动 APIstartApi最后按该沙箱的instances数量并行拉起一个或多个 API 子进程steps/api.ts:103-117。多实例时端口按固定步长递增由于默认inspect: true每个进程附带--inspect调试端口相邻两个 API 端口相差 2一个业务端口 一个 inspector 端口关闭 inspect 后步长为 1。启动过程会持续监听 stdout直到出现Server started at http://...才认为就绪并 resolve若进程异常退出或 60 秒内未就绪会 rejectsteps/api.ts:140-184。启动完成后如果配置了管理员账号日志还会打印可直接使用的adminexample.com / pw / admin凭据。四、Env 是如何为每个数据库生成的理解sandboxes()的返回值还需知道它背后基于 tests/sandbox/src/config.ts 的getEnv()config.ts:148-201为每个沙箱动态计算环境变量每种数据库预置了一套连接参数模板例如 PostgreSQL 使用DB_CLIENT: pg、SQLite 使用DB_CLIENT: sqlite3DB_FILENAME: ./test.dbOracle 使用secretsysuser等config.ts:30-101所有沙箱共享一组通用基线TELEMETRY: false、RATE_LIMITER_ENABLED: false、PRESSURE_LIMITER_ENABLED: false、LOG_LEVEL: info、SERVE_APP: true、WEBSOCKETS_ENABLED: true等config.ts:6-28未设置skipSetup时自动注入ADMIN_EMAIL: adminexample.com、ADMIN_PASSWORD: pw、ADMIN_TOKEN: admin等初始化账号信息config.ts:157-164通过正则\$PORT(?:_[A-Z])*识别环境变量值中的端口占位符$PORT、$PORT_LICENSE、$PORT_MINIO等每个占位符通过getPort()解析为真实空闲端口后回写保证数据库端口、Redis 端口等互不冲突config.ts:183-198最终的PORT与PUBLIC_URL以解析后的opts.port为准config.ts:172-177覆盖外层process.env确保测试访问的地址与实际监听端口一致。合并优先级从低到高大致为数据库基线 → extrasminio/saml/maildev/license→opts.env用户自定义 → 外部process.env→ 解析后的PORT/PUBLIC_URL权威值。因此在SandboxesOptions的元素中通过options.env注入测试专用变量例如关闭 schema 缓存、设置许可证密钥非常常见。五、实战示例一条命令铺满全部数据库sandboxes()最典型的用法来自 e2e 测试的全局初始化 tests/e2e/setup/global-setup-all.ts当环境变量ALLtrue时把databases白名单里的 7 种数据库全部并行拉起并分配给每个 Vitest 项目import { type Database, databases, type Env, type Options, sandboxes, type Sandboxes } from directus/sandbox; const dbs databases.map((database, index) { const port 8000 index * 100; // 每个沙箱一个独立的 API 端口区间 return { database, options: { prefix: database, // 日志前缀便于区分多沙箱输出 port, env: { CACHE_SCHEMA: false, LICENSE_KEY: D0000-00000-00000-00000-0000K, MCP_ENABLED: true, // 需要额外打开的模块开关 // ... }, docker: { port: port 10, // Docker 容器端口从 API 端口错开 keep: true, // 测试结束后保留容器以便复用 }, extras: { license: true }, // 挂载 mock 许可服务 killPorts: true, } as DeepPartialOptions, }; }); const sb await sandboxes(dbs); // 每个数据库一份已生效的 Env交给对应 Vitest 项目使用 project.provide( envs, Object.fromEntries(sb.sandboxes.map((sandbox, index) [dbs[index]!.database, sandbox.env])), );测试结束后在 teardown 中统一回收export async function teardown(_project: TestProject) { if (sb) await sb.stop(); }这个例子里包含的几个实用技巧prefix为每个沙箱的 logger 加上数据库名前缀源码中通过createLogger(env, opts, opts.prefix)实现并发输出日志时一眼可辨docker.keep: true沙箱停止后保留容器后续运行直接复用大幅缩短冷启动时间端口规划API 端口8000 index*100与 Docker 容器端口10错开人工分区避免动态分配的不确定性。单实例场景的最小示例如果你的目标是单库、单次开发验证仓库同时提供单实例版sandbox()tests/sandbox/docs/functions/sandbox.md实现见 sandbox.ts:258-347。它返回{ restartApi, stop, env, apis, logger, knex }的独立句柄sandboxes()本质上可视为sandbox()的并行集合版。需要说明的是tests/sandbox/readme.md的示例代码中写的是sb.close()而当前源码返回的方法名是stop()/restartApi()旧版文档中的示例调用名并未随源码同步更新实际使用请以stop()为准。最小示例import { sandbox } from directus/sandbox; const sb await sandbox(postgres, { dev: true }); // 通过沙箱暴露的 PUBLIC_URL 以 REST / GQL / WebSocket 交互 const result await fetch(sb.env.PUBLIC_URL /items/articles); console.log(await result.json()); await sb.stop();重启时restartApi源码还会处理一个细节被 kill 的 API 端口可能短暂处于TIME_WAIT因此重启前会重新getPort()若原端口不可用则回退到空闲端口并同步更新opts.port、env.PORT与env.PUBLIC_URL保证句柄使用者拿到的地址始终一致sandbox.ts:296-316。这一逻辑同样解释了为何文档要求你读取返回值中的env而不是硬编码端口。六、CLI 入口与编程式入口的分工directus/sandbox同时暴露 CLI 与 JS API 两种交互方式tests/sandbox/docs/README.mdUsage: sandbox [options] database Arguments: database What database to start the api with (choices: maria, cockroachdb, mssql, mysql, oracle, postgres, sqlite) Options: -b, --build Rebuild directus from source -d, --dev Start directus in developer mode. Not compatible with build -w, --watch Restart the api when changes are made -p, --port port Port to start the api on -i, --instances instances Horizontally scale directus (default: 1) -a, --app [port] Spin up the app in dev mode -x, --export Export the schema to a file every 2 seconds -s, --schema [schema] Load an additional schema snapshot on startup -e, --extras extras Enable redis, maildev, saml or other extras --docker.keep Keep containers running when stopping the sandbox --docker.name name Overwrite the name of the docker project --env env... Add env vars the api should start with (KEYVALUE) --silent Silence all logs except for errorsCLI 与sandboxes()的适用边界是CLI 面向单个数据库的交互式开发一次只能指定一个database参数而sandboxes()是纯编程接口专为需要在同一测试进程中编排多数据库沙箱的场景设计——这也是 e2e 全量测试选择它的原因。若日常开发只想快速体验直接运行pnpm sandbox postgres -d -w即可获得一个跟随源码热重启的实例要得到一键覆盖所有数据库的矩阵环境则编写类似上文global-setup-all.ts的sandboxes()调用更合适。七、小结sandboxes() 的设计要点速查白名单强校验database必须是maria / cockroachdb / mssql / mysql / oracle / postgres / sqlite之一非法值在启动前即抛错配置分层编译开关build/dev/watch全局统一其余选项逐沙箱深度合并均以DeepPartial形式传入确定性端口默认inspect开启导致多实例端口步长为 2重启时自动规避TIME_WAIT并回填env幂等的环境依赖运行中的 Docker 容器会被复用Docker 端口与 API 端口通过$PORT占位符自动分配集合级生命周期restartApis()只重启 API 进程stop()负责销毁 Knex 连接、Docker 侧按keep选项决定去留、并回收全部子进程可扩展点hooks.beforeApi与knex选项为测试预留了启动前注入 直连数据库能力。延伸阅读仓库内API 文档sandboxes()tests/sandbox/docs/functions/sandboxes.md与单实例版 sandbox()类型说明Sandboxes、SandboxesOptions、Options、Database、Env包总览tests/sandbox/docs/README.md、tests/sandbox/readme.md核心实现tests/sandbox/src/sandbox.ts、tests/sandbox/src/config.ts、tests/sandbox/src/steps/api.ts数据库/依赖 compose 定义tests/sandbox/src/docker真实调用范例tests/e2e/setup/global-setup-all.ts【免费下载链接】directusThe flexible backend for all your projects Turn your DB into a headless CMS, admin panels, or apps with a custom UI, instant APIs, auth more.项目地址: https://gitcode.com/GitHub_Trending/di/directus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考