集合
集合
工作区中的一切都指向集合。模型(src/data/model/+<m>.model.ts)是存储:字段、类型、约束与状态。它的集合(src/data/collection/<m>/+collection.ts)是公开的部分:读取哪些字段、写入哪些输入、它唯一的 transform、它的查询与操作。策略、读取、实时视图、搜索、Agent、自动化与种子数据都指向集合,从不指向模型;没有集合的模型只能通过某个已公开集合所公开的关系访问。
模型
// src/data/model/+bookings.model.ts (templates/on-demand-services)
import { model } from '@norbital-ai/bolt';
import { AREAS, REPEATS } from '../../lib/matching.ts';
/**
* One customer order: a service at an address, once or recurring, with or without preferred helpers. Each occurrence is
* a visit; the booking holds what they share.
*/
export default model({
description:
'A booking: customer, service, address, preference and repeat. Its visits are the occurrences.',
icon: 'lucide:calendar-check',
label: 'number',
fields: {
number: { kind: 'seq', pattern: 'BK-{yyyy}-{0000}' },
address: { kind: 'text' },
location: { kind: 'point', optional: true },
area: { kind: 'enum', values: AREAS, optional: true },
preference: { kind: 'enum', values: ['any', 'preferred'] },
repeat: { kind: 'enum', values: REPEATS },
status: {
kind: 'state',
initial: 'active',
states: { active: { to: ['cancelled'] }, cancelled: { edit: 'none' } }
},
notes: { kind: 'text', optional: true },
visit_count: { kind: 'count', of: 'visits' }
},
index: ['status']
}); model 字面量接收 description、icon、label(记录标题)与 fields,以及约束 key(upsert、导入与集成使用的自然键)、unique、index、check、noOverlap、computed 与 search。模型从不引用另一个模型:引用即关系。历史记录始终开启。
字段类型
每个字段都是一个带类型的字面量。optional: true 使其可为空;其余字段都是必填的。同一种语言既描述存储字段,也描述所有输入。
text(format可为email、phone、url或zone;max;many)、int、decimal(scale)、money、currency与booldate、instant、time、duration,以及period(of: 'date'或of: 'instant')enum(values、many)、state(initial,以及每个状态的to与edit),以及seq——如'QT-{yyyy}-{0000}'这样永不重复使用的编号序列- 基于关系的
sum与count汇总(of: 'quote_lines.line_total'),由内核在写入语句中维护 json(带类型的shape)、file(accept、最大 20 MiB 的max、multiple)、point、vector(dim与metric,始终建立索引)与custom(工作区自定义字段)- 输入(查询、操作、自动化与集成记录的输入)另外增加
number、id、list、object、union与record。
状态与锁定
state 字段是唯一的锁。to 列出离开某个状态的边,edit 列出行处于该状态时可以更改的内容(默认 'all',也可以是 'none',或具名字段与所属关系)。二者在每条写入路径上都生效,包括管理员与自动化。审批从来不是一种状态:它是对写入的挂起,在授权上声明。
关系
// src/data/+relationship.ts (templates/on-demand-services, excerpt)
import { relationship } from '@norbital-ai/bolt';
export default relationship({
'helpers.user': { to: 'sys_user', optional: true, onDelete: 'setNull' },
'bookings.customer': { to: 'customers', inverse: 'bookings' },
'bookings.service': { to: 'services', inverse: 'bookings' },
'booking_helpers.booking': { to: 'bookings', inverse: 'preferred_helpers', owned: true },
'booking_helpers.helper': { to: 'helpers', inverse: 'preferred_by' },
'visits.booking': { to: 'bookings', inverse: 'visits', owned: true },
'visits.helper': { to: 'helpers', inverse: 'visits', optional: true, onDelete: 'setNull' },
'booking_requests.service': { to: 'services', where: { active: { eq: true } } }
// …
}); 每个引用都位于 src/data/+relationship.ts 中,以 '<model>.<field>' 为键:该键指明关系为模型添加的外键字段。to 是一个模型(数组表示互斥弧),inverse 为汇总、筛选与视图命名另一侧,onDelete 可为 restrict(默认)、cascade 或 setNull。
owned: true 使子项成为父项的一部分:随父项一起删除,永不移到另一个父项,并且只在父项状态允许编辑时才能写入——预订的各次上门服务(visits)就是这样跟随预订的。指向人员的引用使用身份集合 sys_user。
集合
// src/data/collection/customers/+collection.ts (templates/on-demand-services)
import { collection } from '@norbital-ai/bolt';
const columns = ['name', 'phone', 'email', 'address', 'location', 'area', 'notes'] as const;
export default collection('customers', {
read: { fields: 'all', relations: 'all' },
create: { input: { columns } },
update: { input: { columns } },
delete: {}
}); read.fields 限定了所有界面:授权、视图、Agent 与自动化都看不到其外的内容;read.relations 指明筛选或视图可以跨越的关系。create 与 update 是输入白名单(columns,以及 with 下的关系操作);delete: {} 公开删除。只有 read 的集合是只读的。
集合的 transform、查询、操作与通知构成它的写入契约。可选的 +pipeline.ts 与它放在一起(流水线);集成定义位于 src/integration/<name>/+integration.ts(集成)。
搜索与相似度
模型上的 search.text 为每个视图开启搜索框;search.semantic 为指定的文本与文件字段维护嵌入向量。由集合自行填充的 vector 字段可以拥有具名相似度搜索:字面量声明带类型的输入,c.similarity 附加探测向量以及可选的精确重排。
// src/data/model/+photo_evidence.model.ts (templates/field-operations, excerpt)
perceptual_embedding: { kind: 'vector', dim: 256, metric: 'l2', hidden: true },
scene_embedding: { kind: 'vector', dim: 256, metric: 'cosine', optional: true, hidden: true },
// src/data/collection/photo_evidence/+collection.ts (excerpt)
const photo_evidence = collection('photo_evidence', {
read: { fields: 'all' },
// …
similarity: {
pdq: {
description: 'Photos nearest a PDQ vector: near-duplicates of the same pixels.',
input: { vector: { kind: 'list', of: { kind: 'number' } } }
}
}
});
export default photo_evidence;
photo_evidence.similarity('pdq', {
probe: (input) => ({ field: 'perceptual_embedding', vector: input.vector })
}); 自定义字段
自定义字段是工作区自有的存储类型。src/data/custom_field/<f>/+definition.ts 用输入语言声明其 shape,并可附加一个 validate;旁边的 +renderer.svelte 负责显示与编辑。模型以 { kind: 'custom', of: '<f>' } 使用它。
// src/data/custom_field/photo_source/+definition.ts (templates/field-operations)
import { customField } from '@norbital-ai/bolt';
const photo_source = customField({
description:
'Where a photo came from: either a workspace upload, or the messaging conversation, message, attachment and sender that delivered it.',
shape: {
kind: 'union',
by: 'kind',
arms: {
workspace_upload: {},
message: {
integration: { kind: 'text' },
conversation_id: { kind: 'text' },
message_id: { kind: 'text' },
attachment_id: { kind: 'text' },
sender_id: { kind: 'text' },
sent_at: { kind: 'instant', optional: true }
}
}
}
});
export default photo_source;
photo_source.validate((value) =>
value.kind === 'message' &&
[value.integration, value.conversation_id, value.message_id, value.attachment_id, value.sender_id].some(
(part) => part.trim() === ''
)
? 'A message photo source names its integration, conversation, message, attachment and sender.'
: undefined
);
// used in src/data/model/+photo_evidence.model.ts as: source: { kind: 'custom', of: 'photo_source' } 系统集合
身份与运行记录位于引擎拥有的系统集合中,例如 sys_user、sys_team 与 sys_run。关系可以指向它们(to: 'sys_user'),策略可以授予对它们的读取;它们的写入是生成的操作,从不由作者编写。