Dagger 模块自定义标量类型 ScalarTypeDef:TypeScript 客户端 API 完全指南
【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger
导读
ScalarTypeDef是 Dagger 引擎中描述“模块内自定义标量类型(custom scalar)”的核心类型定义对象。在 Dagger 模块化体系中,模块可以对外暴露自定义数据类型(如自定义 ID、枚举之外的简单值类型),而ScalarTypeDef就是这些标量在 GraphQL/DAG 执行引擎中的统一载体。本文以 docs/versioned_docs/version-0.19/reference/typescript/api/client.gen/classes/ScalarTypeDef.md 为骨架,结合仓库源码(sdk/typescript/src/api/client.gen.ts、core/typedef.go、core/schema/module.go),完整讲解该类的构造约束、四个查询方法的语义、底层实现原理与真实调用链路,帮助读者在 TypeScript 模块开发中正确理解和使用标量类型定义。
类概述:什么是 ScalarTypeDef
根据 API 文档,ScalarTypeDef的官方定义是:
A definition of a custom scalar defined in a Module.
即:在一个 Module 中定义的自定义标量的类型定义。它是 Dagger 类型系统(TypeDef家族)中用于描述标量(scalar)的一等公民。
从源码结构看,Dagger 的 TypeDef 家族包含多种“子类型定义”,其中标量对应的正是ScalarTypeDef。在 core/typedef.go 中,其 Go 侧核心结构如下:
type ScalarTypeDef struct { Name string `field:"true" doc:"The name of the scalar." doNotCache:"simple field selection"` Description string `field:"true" doc:"A doc string for the scalar, if any." doNotCache:"simple field selection"` OriginalName string // SourceModuleName is currently only set when returning the TypeDef from the Scalars field on Module SourceModuleName string `field:"true" doc:"If this ScalarTypeDef is associated with a Module, the name of the module. Unset otherwise." doNotCache:"simple field selection"` }可以看到,Go 侧字段与 TypeScript 客户端公开的四个属性一一对应:
| 字段 | 含义 |
|---|---|
Name | 标量的名称(GraphQL 名称,由NewScalarTypeDef构造时经strcase.ToCamel规范化) |
Description | 标量的文档字符串,可空 |
SourceModuleName | 若该标量与某个 Module 关联,则为该模块名;否则为空 |
OriginalName | 构造时传入的原始名称(仅存在于 Go 引擎侧,不暴露给 GraphQL API) |
它在 TypeDef 类型体系中的位置
ScalarTypeDef并非孤立存在,它是TypeDef这个总类型(kind 判别联合)的一个分支。在 core/typedef.go 中,引擎通过以下方式把标量挂接到通用TypeDef上:
func (typeDef *TypeDef) WithScalar(scalar dagql.ObjectResult[*ScalarTypeDef]) *TypeDef { typeDef = typeDef.WithKind(TypeDefKindScalar) typeDef.AsScalar = dagql.NonNull(scalar) return typeDef.syncName() } func (typeDef *TypeDef) WithScalarTypeDef(scalar dagql.ObjectResult[*ScalarTypeDef]) *TypeDef { typeDef = typeDef.Clone() typeDef.Kind = TypeDefKindScalar typeDef.AsScalar = dagql.NonNull(scalar) return typeDef.syncName() }其中TypeDefKindScalar的注册描述为 “A scalar value of any basic kind.”(见 core/typedef.go)。这意味着:当你看到TypeDef.Kind == SCALAR_KIND时,其AsScalar字段就是一个ScalarTypeDef。
构造函数:仅供内部使用,禁止手动创建
new ScalarTypeDef( ctx?: Context, _id?: ScalarTypeDefID, _description?: string, _name?: string, _sourceModuleName?: string ): ScalarTypeDef文档明确指出:
Constructor is used for internal usage only, do not create object from it.
该构造函数参数如下:
| 参数 | 类型 | 说明 |
|---|---|---|
ctx? | Context | GraphQL 执行上下文(由BaseClient维护) |
_id? | ScalarTypeDefID | 持久化标识符,对应 GraphQL 的ScalarTypeDefID类型别名 |
_description? | string | 标量文档字符串 |
_name? | string | 标量名称 |
_sourceModuleName? | string | 关联模块名称 |
在 TypeScript 实现中(sdk/typescript/src/api/client.gen.ts),构造函数只做两件事:调用super(ctx)继承BaseClient,并把四个可选值原样保存到私有只读字段(_id、_description、_name、_sourceModuleName)。这些私有字段会在对应方法中被“短路”使用——见下文各方法实现。
实际上,客户端代码不会直接new一个ScalarTypeDef,而是通过两种途径获得实例:
- GraphQL 响应反序列化:在 sdk/typescript/src/api/client.gen.ts 中,
TypeDef.asScalar()方法把查询结果节点封装为ScalarTypeDef实例:
asScalar = async (): Promise<ScalarTypeDef | null> => { const ctx = this._ctx.select("asScalar", ...) const response: Awaited<ScalarTypeDef | null> = await ctx.execute() return new ScalarTypeDef(ctx.copy().selectNode(response, "ScalarTypeDef")) }- 模块 SDK 的
withScalar构造链路:TypeScript 模块运行时通过dag.typeDef().withScalar(name)构造,见下文调用链分析。
实例方法详解
ScalarTypeDef共暴露四个异步方法,全部返回Promise,与文档一一对应。以下逐个结合实现说明。
id():获取唯一标识符
id = async (): Promise<ScalarTypeDefID> => { if (this._id) { return this._id } const ctx = this._ctx.select("id") const response: Awaited<ScalarTypeDefID> = await ctx.execute() return response }- 若构造时已携带
_id(例如从反序列化结果中取得),直接返回缓存值,不发起网络请求; - 否则向 GraphQL 服务器查询
id字段并返回ScalarTypeDefID。
ScalarTypeDefID是一个类型别名(type alias),它代表 Dagger 引擎生成的标量类型定义 ID。该 ID 在引擎侧对应持久化机制:ScalarTypeDef实现了EncodePersistedObject/DecodePersistedObject(见 core/typedef.go),可将自身编码为persistedScalarTypeDefJSON 载荷,通过 ID 在会话间恢复对象,这正是 Dagger“一切皆 ID、懒执行”的核心设计。
description():标量文档字符串
description = async (): Promise<string> => { if (this._description) { return this._description } const ctx = this._ctx.select("description") const response: Awaited<string> = await ctx.execute() return response }文档语义为 “A doc string for the scalar, if any.”——即标量的文档字符串,可以为空。同样遵循“先查缓存、再发查询”的模式。
name():标量名称
name = async (): Promise<string> => { if (this._name) { return this._name } const ctx = this._ctx.select("name") const response: Awaited<string> = await ctx.execute() return response }返回标量的名称(The name of the scalar)。注意引擎侧的NewScalarTypeDef(core/typedef.go)会将传入名称通过strcase.ToCamel规范化为最终 GraphQL 名称,原始名称保留在OriginalName中;而WithName(core/typedef.go)则用于在重命名场景下直接写入已规范化的名称(与ObjectTypeDef.WithName的处理方式一致,避免二次规范化)。
sourceModuleName():来源模块名
sourceModuleName = async (): Promise<string> => { if (this._sourceModuleName) { return this._sourceModuleName } const ctx = this._ctx.select("sourceModuleName") const response: Awaited<string> = await ctx.execute() return response }文档语义为:
If this ScalarTypeDef is associated with a Module, the name of the module. Unset otherwise.
即:若该标量类型定义与某个 Module 关联,返回该模块名;否则为空字符串。
从引擎注释(core/typedef.go)可知:SourceModuleName目前只在通过Module上的Scalars字段返回TypeDef时才会被设置。而创建标量的入口scalarTypeDef(core/schema/module.go)也支持可选的SourceModuleName内部参数:
func (s *moduleSchema) scalarTypeDef(ctx context.Context, _ *core.Query, args struct { Name string Description string `default:""` SourceModuleName dagql.Optional[dagql.String] `internal:"true"` }) (*core.ScalarTypeDef, error) { scalar := core.NewScalarTypeDef(args.Name, args.Description) if args.SourceModuleName.Valid { scalar.SourceModuleName = string(args.SourceModuleName.Value) } return scalar, nil }统一的“短路”执行模式
四个方法共享同一实现模式:若构造时已注入对应值则直接返回,否则向引擎发起字段查询。这在 Dagger 的 TypeScript 客户端(client.gen.ts)中是一种通用优化——SDK 在反序列化时会把引擎已返回的字段缓存到实例上,避免重复的 GraphQL 往返。所有方法均通过this._ctx.select(...)选择字段并execute(),遵循BaseClient的懒加载(lazy)执行模型:方法调用只记录查询意图,真正执行发生在最终await时。
引擎侧完整解析链路
1. GraphQL Schema 注册
ScalarTypeDef在引擎侧通过 dagql 注册为 GraphQL 对象类型。在 core/schema/module.go 附近可以看到dagql.Fields[*core.ScalarTypeDef]{...}的字段注册,且模块 Schema 暴露了构造标量定义的顶层入口scalarTypeDef以及__withScalarTypeDef等内部工具函数(core/schema/module.go)。
2. TypeDef 的 asScalar 分支
当对TypeDef查询asScalar字段时,引擎会取出TypeDef.AsScalar并返回其ScalarTypeDef实例(对应 TypeScript 客户端 sdk/typescript/src/api/client.gen.ts 的asScalar()方法)。在 core/typedef.go 附近可以看到引擎侧对该分支的类型断言处理:attached.(dagql.ObjectResult[*ScalarTypeDef])。
3. 模块 SDK 的 withScalar 注册链路
在 TypeScript 模块运行时中,当模块开发者把自定义类型注册进模块时,addTypeDef函数(sdk/typescript/src/module/entrypoint/register.ts)会按 kind 分发:
case TypeDefKind.ScalarKind: return dag.typeDef().withScalar((type as ScalarTypeDef).name)即:对SCALAR_KIND类型的 typedef,仅取其.name调用dag.typeDef().withScalar(name),由引擎侧typeDefWithScalar(core/schema/module.go)完成校验与构造。该处理器要求名称非空:
if args.Name == "" { return nil, fmt.Errorf("scalar type def must have a name") }这印证了文档中name()是必填语义字段,而description()是可选字段。
4. 类型内省(Introspection)与标量
在 TypeScript 模块的 introspection 工具中,ScalarTypeDef被建模为“基础 typedef 的扩展”(sdk/typescript/src/module/introspector/typedef.ts):
Extends the base typedef if it's a scalar to add its name and real type.
即:当内省到 kind 为scalar时,基础TypeDef会被扩展为ScalarTypeDef,补上 name 与真实类型信息。这说明ScalarTypeDef在“引擎类型系统 ↔ 模块 SDK 类型系统”之间承担着标准化的桥梁作用。
真实调用场景示例
在 Dagger 模块中,标量类型最常见的出现场景是:模块定义自定义函数、参数或返回值时,引擎需要为它们建立类型描述。例如一个返回自定义标量的模块函数,其类型描述最终会以TypeDef{Kind: SCALAR_KIND, AsScalar: ScalarTypeDef{Name: "MyScalar", Description: "..."}}的形式被引擎持久化并暴露给客户端。
在 TypeScript 客户端侧,一个典型的查询片段如下(示意,对应四个方法):
import { connect } from "@dagger.io/dagger" connect(async (client) => { // 通过模块的类型信息获得 TypeDef 后,取标量分支 const typeDef = await client.module().scalars().name("MyScalar").typeDef() const scalar = await typeDef.asScalar() const id = await scalar?.id() // 唯一标识符 const name = await scalar?.name() // 标量名 const desc = await scalar?.description() // 文档字符串 const mod = await scalar?.sourceModuleName() // 来源模块名(未关联则为空) })注意:实际获取
ScalarTypeDef的方式取决于你的查询入口(如通过module.scalars()、typeDef().asScalar()或自定义模块的构造链路),上述代码用于展示方法语义,具体字段路径请以你使用的 SDK 版本生成的 client 为准。
测试与验证依据
仓库中多处集成测试数据验证了标量类型定义的实际行为:
- core/typedef_test.go 与 core/typedef_test_helpers_test.go:覆盖
TypeDef系列(含标量分支)的构造、序列化与往返; - core/schema/testdata/base_schema.graphqls:Schema 基准测试中包含
ScalarTypeDef的 GraphQL 类型定义; - 大量生成代码(如 core/integration/testdata/modules/go/ifaces/internal/dagger/dagger.gen.go)展示了各语言 SDK 生成的
ScalarTypeDef相关客户端代码,可作为跨语言对照。
关键要点总结
ScalarTypeDef是 Dagger 模块类型系统中描述自定义标量的标准对象,与ObjectTypeDef、EnumTypeDef、ListTypeDef等并列,通过TypeDef.AsScalar挂载在统一的TypeDef之上(Kind 为SCALAR_KIND)。- 构造函数仅供 SDK 内部使用,业务代码不应手动
new;实例应通过 GraphQL 查询结果(如typeDef.asScalar())或模块运行时构造链路获得。 - 四个方法语义明确:
id()返回持久化标识符、name()返回标量名、description()返回可选文档字符串、sourceModuleName()返回关联模块名(未关联时为空)。 - 全部方法采用“缓存短路 + 懒查询”模式:SDK 反序列化时已缓存的字段直接返回,未缓存的字段才会触发一次 GraphQL 查询。
- 名称规范化发生在引擎侧:
NewScalarTypeDef会经strcase.ToCamel生成最终 GraphQL 名称,且名称不可为空(引擎会显式报错)。 SourceModuleName仅在通过Module的Scalars字段返回TypeDef时设置,这是判断标量“归属模块”的关键信号。
通过本文,读者应能准确理解 Dagger TypeScript 客户端中ScalarTypeDef的完整语义、实现原理与调用场景,从而在自定义 Dagger 模块的类型设计中正确使用这一 API。
【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考