- 后端
【免费下载链接】keystone
The superpowered headless CMS for Node.js — built with GraphQL and React
导读
multiselect是 Keystone 内置的“多选”字段类型,用于让一条记录从你预先定义的一组options中选择一个值的集合(数组)。它非常适合标签(Tags)、多分类、多权限标记等场景,与单选的select字段互补。本文将基于 Keystone 源码与官方文档,完整讲解 multiselect 的配置项、三种值类型(string / enum / integer)、数据库与 GraphQL 层的存储行为、Admin UI 的两种交互模式,并结合仓库内的实现与测试用例给出可复制的实战配置。
Multiselect 是什么
根据 官方字段参考文档,一个multiselect字段表示从定义的options中选取一组值。与单选的select字段不同:
- 值的类型可以是字符串(string)、整数(integer)或枚举(enum),由
type选项决定; type决定的是该字段在GraphQL API 中的数据类型;- 与
select不同,type不会改变数据库类型——multiselect字段在数据库中始终以 JSON 数组存储(Prisma 的Json标量)。
这一点可以在仓库中找到直接证据:在 测试沙箱的 schema.prisma 中,multiselect 字段被生成为multiselect Json @default("[]");而在 测试沙箱的 schema.graphql 中,对应的 GraphQL 输出类型为[String!](可空数组、元素非空)。
配置选项总览
以下是 multiselect 支持的全部配置项(源自官方文档,并结合源码注释补充细节):
| 配置项 | 默认值 | 说明 |
|---|---|---|
type | 'string' | 字段值的类型,必须是['string', 'enum', 'integer']之一 |
options | 必填 | 一个{ label, value }数组;label是 Admin UI 中显示的文本,value用于 GraphQL API 并存储到数据库 |
defaultValue | [] | 创建条目时未显式传值时使用的默认值 |
db.map | 无 | 为字段添加 Prisma@map属性,改变数据库中的列名 |
graphql.read.isNonNull | false | 在无读访问控制时可将输出字段设为非空 |
graphql.create.isNonNull | false | 在无创建访问控制时可将 create 输入字段设为非空并带默认值 |
ui.displayMode | 'select' | Admin UI 交互模式:'checkboxes'(复选框组)或'select'(组合框 + 标签) |
db.isNullable | false | 数据库层面是否可空 |
db.extendPrismaSchema | 无 | 用于扩展生成的 Prisma schema 字段定义 |
说明:
ui.displayMode、db.isNullable与db.extendPrismaSchema在官方参考文档正文中没有展开,但明确存在于 multiselect 字段源码类型定义 中,本文一并纳入讲解。
关于options的两种写法
从 MultiselectFieldConfig 类型定义 可以看到,options的写法取决于type:
type为'string'或'enum'时,options可以是{ label: string; value: string }对象数组,也可以是纯字符串数组。当传入纯字符串时,源码会调用humanize(option)自动生成展示用的 label(configToOptionsAndGraphQLType);type为'integer'时,options必须是{ label: string; value: number }数组,且defaultValue相应地为数字数组。
完整配置示例
官方文档给出的最小可用配置如下,它展示了type、options、defaultValue与db.map的组合用法:
import { config, list } from '@keystone-6/core'; import { multiselect } from '@keystone-6/core/fields'; export default config({ lists: { SomeListName: list({ fields: { someFieldName: multiselect({ type: 'enum', options: [ { label: '...', value: '...' }, /* ... */ ], defaultValue: ['...'], db: { map: 'my_multiselect' }, }), /* ... */ }, }), /* ... */ }, /* ... */ });一个更贴近实战的完整例子
以“文章标签”为例,把三种type的典型场景都展示出来,便于直接复制修改:
import { config, list } from '@keystone-6/core'; import { multiselect } from '@keystone-6/core/fields'; export default config({ lists: { Post: list({ fields: { // 枚举类型:value 只能是预定义枚举值,GraphQL 层会生成独立的枚举类型 tags: multiselect({ type: 'enum', options: [ { label: 'News', value: 'news' }, { label: 'Tutorial', value: 'tutorial' }, { label: 'Release', value: 'release' }, ], defaultValue: ['news'], ui: { displayMode: 'checkboxes' }, }), // 字符串类型:value 是自由字符串,GraphQL 层是 [String!] categories: multiselect({ type: 'string', options: [ { label: '前端', value: 'frontend' }, { label: '后端', value: 'backend' }, ], }), // 整数类型:value 是 32 位有符号整数,GraphQL 层是 [Int!] priorityLevels: multiselect({ type: 'integer', options: [ { label: 'Level 1', value: 1 }, { label: 'Level 2', value: 2 }, { label: 'Level 3', value: 3 }, ], defaultValue: [1], db: { map: 'priority_levels' }, }), }, }), }, });options的便捷简写
如果你不关心 Admin UI 中显示的自定义 label,可以直接传字符串数组,源码会自动将字符串 humanize 为 label(实现位置):
multiselect({ type: 'string', options: ['red', 'green', 'blue'], })三种值类型与 GraphQL 数据形态
type决定 GraphQL 层的数据类型,这在 configToOptionsAndGraphQLType 中实现:
type: 'string':GraphQL 类型为[String!]。value是任意字符串,如'a string';type: 'integer':GraphQL 类型为[Int!]。value必须是 32 位有符号整数范围内的数字(-2147483648到2147483647)。源码在 index.ts 中对此做了显式校验:一旦发现value不是整数或超出范围,会在启动时抛出TypeError;type: 'enum':GraphQL 层会为字段自动生成一个命名枚举类型,其名称格式为${listKey}${FieldName}Type(例如列表Post的字段tags会生成PostTagsType),枚举值即各 option 的value(生成逻辑)。
无论哪种type,字段在 GraphQL 输出层都是元素非空的列表:g.list(g.nonNull(...))(index.ts)。在数据库中则统一以 JSON 数组存储,且默认值为[](sandbox schema.prisma)。
与select字段的关键差异
从实现对比可以更清楚地理解 multiselect 的定位:
select字段在 Prisma 层使用String或Int标量(见 select 字段源码),是单值列;multiselect字段在 Prisma 层固定使用Json标量(index.ts),存储的是整个数组。
因此两者的type选项含义不同:对select而言type影响数据库列类型,而对multiselect而言type只影响 GraphQL 层的类型呈现。
校验规则与源码级约束
multiselect 内置了严格的运行时校验,均在 字段实现 中可见:
- 选项去重:所有 option 的
value必须唯一,否则启动时报错has duplicate options, this is not allowed(index.ts); - 值必须在白名单内:create/update 时,传入数组中的每个值都必须是已定义 option 的
value,否则校验钩子抛出'<value>' is not an accepted option(index.ts); - 不允许重复选择:同一字段的取值数组内不能有重复值,否则抛出
non-unique set of options selected(index.ts); - 不支持唯一索引:
isIndexed: 'unique'不被支持,配置后会直接抛出TypeError(index.ts)。
这些约束同样体现在测试矩阵中:在 multiselect 测试夹具 中,supportsUnique = false、supportsNullInput = false、nonNullableDefault = true、supportsDbMap = true,意味着该字段不可唯一索引、不可传 null、默认非空、支持db.map。
默认值(defaultValue)的两种语义
defaultValue默认是空数组[]。需要注意它的作用范围与平台差异:
- 创建条目时:若没有显式传值,字段会使用
defaultValue。这由 create 输入解析器resolveCreate保证:当传入值为undefined时回退到defaultValue(index.ts); - Prisma 默认值:在 PostgreSQL 等数据库上,源码会给字段生成字面量默认值
JSON.stringify(defaultValue ?? null);而在SQLite 上则不生成 Prisma 默认值(index.ts),因为 SQLite 的 Json 默认值由 create 输入逻辑管理; db.isNullable会影响defaultValue的解析:当字段不可空时,defaultValue缺省为[](index.ts)。
测试夹具也印证了“未显式赋值时默认存储为空数组”的行为:storedValues中未提供company字段的记录最终存储为company: [](test-fixtures.ts)。
graphql.isNonNull 的使用前提
graphql.read.isNonNull与graphql.create.isNonNull两个选项有严格的前提条件——只能在对应方向没有访问控制时启用:
graphql.read.isNonNull:仅当你没有读访问控制且不打算未来添加时才可设为true。原因在官方文档中解释得很清楚:一旦存在访问控制,被拒绝访问时字段会返回null,而非空字段收到null会引发错误并向上传播,直到遇到可空字段为止——这会导致整个条目不可读,甚至在items查询中所有条目都不可读。源码中 assertReadIsNonNullAllowed 也会在“字段未设置validation.isRequired/db.isNullable: false却开启非空读”时直接抛错;graphql.create.isNonNull:仅当你没有创建访问控制时才能设为true,此时 create 输入字段在 GraphQL 层变为非空并带有默认值。如果存在创建访问控制,用户无权创建该字段时条目会始终通过不了访问控制校验。
简单归纳:这两个选项是“性能/类型严谨性”优化,前提是确认对应方向永远放行,否则会破坏字段与条目的可读/可写性。
Admin UI 的两种展示模式
在 multiselect 的 Admin UI 视图 中,字段支持两种ui.displayMode:
'select'(默认):渲染为一个可过滤的组合框(Combobox)+ 已选标签组(TagGroup)。用户键入文字过滤候选,点选后以标签形式展示,标签可逐个移除,最多展示两行(maxRows={2})(SelectModeField 实现);'checkboxes':渲染为一个复选框组(CheckboxGroup),所有 option 平铺为复选框,勾选即选中(CheckboxesModeField 实现)。
列表页的单元格展示也有优化:当选中项超过 3 个时,只显示第一个标签并追加N more(例如 “News, 4 more”),避免表格过宽(Cell 组件)。
Admin UI 内部统一把 option 的value转为字符串处理(valuesToOptionsWithStringValues),序列化回 API 时再按字段type用parseInt还原为整数(controller 实现)。这也是为什么type: 'integer'的 option value 必须落在 32 位有符号整数范围内。
测试验证与延伸阅读
仓库为 multiselect 提供了完整的 API 测试矩阵(enum / string / integer 三种类型全覆盖),见 multiselect 测试夹具:它定义了初始数据、期望存储值、exampleValue等,可用于理解字段在各种操作(创建、查询、过滤、更新)下的期望行为。
如果你正在对比选择字段类型,建议同时阅读 select 字段参考文档 与 字段总览文档;深入理解字段底层机制可阅读 multiselect 源码 以及 非空 GraphQL 校验逻辑。
小结
multiselect是 Keystone 处理“一对多的值集合”场景的首选字段:它把类型约束(string / enum / integer)、选项白名单校验、JSON 数组存储与两套 Admin UI 交互模式封装在一个配置对象中。使用时牢记三个要点:options的 value 必须唯一且(整数类型下)在 32 位有符号整数范围内;defaultValue默认[],在 SQLite 上不生成 Prisma 层默认值;graphql.*.isNonNull仅在确认无访问控制时才启用。理解这些细节后,你就能在真实项目中安全、高效地使用 multiselect 建模标签、分类与多选属性。
- 后端
【免费下载链接】keystone
The superpowered headless CMS for Node.js — built with GraphQL and React
相关推荐
Keystone 字段类型(Field Types)完全指南:数据模型、通用选项与动态字段机制
Keystone 字段类型(Field Types)完全指南:数据模型、通用选项与动态字段机制 Keystone 的字段类型(Field Types)是构建数据
后端Keystone与GraphQL Code Generator集成:类型安全开发实践
Keystone与GraphQL Code Generator集成:类型安全开发实践 在现代Web开发中,类型安全正成为提升代码质量和开发效率的关键实践。当使用
后端Keystone Classic Boolean 字段类型完整指南:存储、更新规则、校验与过滤
Keystone Classic Boolean 字段类型完整指南:存储、更新规则、校验与过滤 Boolean 是 Keystone Classic(Node.
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考