@ai- matrx/ 记录
AI Matrx唱片店的无头客户端: Table / Field / Rule / 记录 / 关系 / 家庭类型、 关闭词汇和拒绝代码 从商店ISTELF, 打出的电话 在商店的门,没有别的, 合并字段声明,一个只预测什么 商店本身的触发器 拒绝, 和一个结果封 从来没有扔.
包件装有"根"、"无头" /core 和反应绑定
/react。 。 。 . @ai-matrx/records-ui 做它; 没有别的谈话 商店.
这个套装的规矩是: 客户镜像商店的合同 永远不要重新执行 商店执行的规则。 商店拒绝的地方,这个 软件包表面显示商店本身的句子——从不翻译.
import {
type Table, type Field, type StoredRecord, // the shapes
type ValueEnvelope, // ver · src · actor · on_behalf_of · at · absent · alternates · dated
PARITY_FIELD_TYPES, // the 13 field types, read from custom.parity_field_types()
ACTOR_VOCABULARY, // exactly: user, agent, system
ABSENCE_REASONS, // exactly: never asked, none, refused, conflicting
STORE_DOORS, STORE_REFUSAL_CODES, // GENERATED from pg_proc on the live store
} from "@ai-matrx/records";
快速启动——我们自己的系统内的记录库
公司需要从自己的屏幕上列出、创建、编辑、归档和观看所有记录,或 剧本,没有我们的。 一个包;每个段仅在导入时加载.
npm install @ai-matrx/records @supabase/supabase-js
import { createClient } from "@supabase/supabase-js";
import { createRecords, sessionAuth } from "@ai-matrx/records/setup";
import { ensureTable } from "@ai-matrx/records/core";
// 1. Sign in. The database address is https://db.matrxserver.com; the publishable key is on your
// AI Matrx account page. Any supabase-js sign-in works (password, magic link, SSO, server cookies).
const supabase = createClient("https://db.matrxserver.com", PUBLISHABLE_KEY);
await supabase.auth.signInWithPassword({ email, password });
// 2. One call. You choose the organization new records are saved in; reads span all of yours.
const matrx = await createRecords({ auth: sessionAuth(supabase), organizationId: (orgs) => orgs[0] ?? null });
// 3. A table (found by its slug, or made the first time), then records.
const table = await ensureTable(matrx.records, { name: "Service callbacks", slug: "service_callbacks",
fields: [{ key: "caller", label: "Caller", type: "text" }, { key: "phone", label: "Phone", type: "phone" }] });
if (!table.ok) throw new Error(table.error.message);
const tableId = table.data; // the table id
const created = await matrx.records.recordWrite({ table_id: tableId, data: { caller: "Marcus Delgado", phone: "+14155550178" } });
const page = await matrx.records.list({ table_id: tableId, limit: 50 }); // { rows, total }
const record_id = created.ok ? created.data : "";
const read = await matrx.records.recordRead({ record_id, withVersion: true }); // .data.document, .data.version
const expectedVersion = read.ok ? (read.data.version ?? undefined) : undefined;
const edited = await matrx.records.recordUpdate({ record_id, patch: { phone: "+14155550199" }, expectedVersion }); // answers the new version
// Always send expectedVersion: a colleague's newer edit is then refused (`stale_write`, field by field), never overwritten.
const archived = await matrx.records.recordDelete({ record_id }); // soft: recordRestore brings it back
// 4. Live: other people's changes to this table (this setup's own writes are not echoed back).
const stop = matrx.watch(tableId, { records: (ids) => console.log("changed", ids) });
// 5. Search across every table you can see; platform records (contacts…) through matrx.entities.
const hits = await matrx.search({ search: "Delgado" });
stop(); matrx.close(); // a script closes the live socket before it exits
每一个呼叫回答 { ok: true, data } | { ok: false, error } - 商店自己的句子,从来没有
掷出. 反应主机添加 @ai-matrx/records/react 或者说 @ai-matrx/records/data/react 同样的钩子
客户端; @ai-matrx/records-ui 是我们可选的一组屏幕。 组织 API 密钥插入 auth
作为第二种模式(计划);今天 auth 是一个签名的会话.
| 节次 | 什麽 |
|---|---|
records/setup | createRecords, (中文(简体) ), sessionAuth - 上面的电话 |
records/core | 每个门上输入的客户端(表格、记录、版本、视图、表格、总数) |
records/realtime | 用户端口设置 |
records/search | searchRecords, (中文(简体) ), listStoreRecordCandidates |
records/forms | 表格文档,嵌入,读取一个公共地址, typedAnswersFor |
records/versions | 版本规则 : useRecordVersions,版本携带的写出门, askOnce, (中文(简体) ), refusal |
records/entity, (中文(简体) ), records/entity/react | 平台记录(联系人、项目、任务) |
records/data, (中文(简体) ), records/data/react | 一个数据端口和自定义表格和实体上的 TanStack 钩子 |
records/react | 提供者 + 钩子在客户端之上 |
/core-门上的打字客户端
import { createClient } from "@supabase/supabase-js";
import { createRecordsClient, ensureTable, supabaseDataSource } from "@ai-matrx/records/core";
const supabase = createClient(url, publishableKey); // the client that carries this person's session
const records = createRecordsClient({
dataSource: supabaseDataSource(supabase), // binds the store's schema for you
actor: { actor: "user" }, // who is acting: "user", "agent" or "system"
organizationId: orgId, // where writes go; the store is keyed (organization_id, id)
onError: (e) => report(e), // OPTIONAL — the default screams to console
});
// Reads of tables and records span `organizationIds` when you pass them (every organization the
// person belongs to); typed tables read every one of them by default, without being told.
// (This block is compiled with the package: src/__tests__/readme-quick-start-compiles.tsx.)
// A page's own small table: found by its slug, or made the first time (one transaction).
const log = await ensureTable(records, { name: "Front desk callback log", slug: "front_desk_callback_log",
fields: [{ key: "caller_phone", label: "Phone", type: "phone", unique: true }, { key: "reason", label: "Reason", type: "long_text" }] });
const one = await records.tableFind({ slug: "front_desk_callback_log" }); // the Table, or null
const id = await records.recordUpsert({ table_id, key: "caller_phone", data: { caller_phone, reason } }); // write-or-update
// With expectedVersion it is an EDIT: no live record of that key → stale_write, never a new record.
const written = await records.recordWrite({ table_id, data: { title: "Acme", owner: personId } });
const rows = await records.list({ table_id, limit: 50 });
const read = await records.recordRead({ record_id, withComputed: true });
const moved = await records.recordUpdate({ record_id, patch: { title: "Acme Inc" }, expectedVersion: 3 });
if (!moved.ok && moved.error.code === "stale_write") {
// The store's own words, plus the conflict field by field.
moved.error.message; // "Someone else changed this record while you were working on it. …"
staleWriteDetail(moved.error); // { expected_version, current_version, contested_fields }
}
ensureTable 永远不要改变一栏,它说每个栏是如何不同的
已经存在的表格将每个列都完全保留在存储中(重排是转换一个人) (决定)。 要求的规格不同
const log = await ensureTable(records, spec);
// { ok: true, data: tableId, homeId, created: false,
// drift: [{ field: "caller_phone", aspect: "type", stored: "text", declared: "phone" },
// { field: "patient_name", aspect: "label", stored: "Patient", declared: "Patient full name" }] }
drift 实值 FieldDrift[] —— 说 { field, aspect, stored, declared }, (中文(简体) ), aspect 一个 type (比例类型,
其他行为, format, (中文(简体) ), label, (中文(简体) ), multi, (中文(简体) ), required, (中文(简体) ), unique,或 spec (该栏的规格可以
不读取; declared 是商店的句子。 没错 [] 当出生和无异时
null 只能从一个老店里拿来比较 申报的一面是商店存储的规格
这样 long_text 永远不是假的 type 漂来漂去。 这个形状是合同的放行时间漂移
检查读取( n)parseFieldDrift 键入原始答案).
应用程序保存的表格
const outputs = await ensureTable(records, {
name: "Exercise flashcard outputs", slug: "agent_output_exercise_flashcard",
platformOwned: true, keptFor: "agent_output", kind: "exercise_flashcard",
homeId: sharedOutputsHome, // a NEW table goes in this Home; none is made
fields: [{ key: "body_region", label: "Body region", type: "select", optionsTableId }],
});
keptFor 是地址的一部分: 带有它的光谱只找到一个为该词保留的表格, 而光谱
没有它就永远找不到一个应用的桌子——一个人的桌子和一个应用的一发子弹的桌子是不同的
图表. platformOwned, (中文(简体) ), keptFor, (中文(简体) ), kind, (中文(简体) ), draftsFor 财务报告和审定财务报表 draftsStamp 存储在桌子上
生产. homeId 必须是这个组织的活家 打电话者可能改变,或什么都不做.
optionsTableId 重新使用现有的选择列表; declaration 过任何其他 custom.field_declare
键.
当两个桌子共用一发子弹时, tableFind 财务报告和审定财务报表 ensureTable 可能有所不同
鼻涕并不是独一无二的(较老的组织持有一些重复. tableFind 回答最年长的
子弹的表 呼叫者可能会看到。 。 。 . ensureTable 取最古老的弹子表
总计 如果打电话者可能不知道, 拒绝用读取门本身的话
比做第二个桌子。 对于一个最古老的复制品, tableFind 返回更新表格时
ensureTable 拒绝;双方总是同意,当打电话者能看到最年长的.
这里什么也没有, 这里什么也没有发明一个答案。 店里的门
尚未答复 door_absent,命名门和欠车道
这.. fieldPropose, (中文(简体) ), tablePropose 财务报告和审定财务报表 mergeFieldResolve
今天拒绝,而不是返回一个空列表,它会读作“有”
没什么".
三道作一桌(每分一分下,同作一铺)
每一道都在同一地段结束: 一个普通的记录存储表,有商店自己的门 决定访问、历史和档案。 选一个和你站的地方匹配的.
1. 从代码——声明,它存在于先用上,行被打出. 无编码源 : RowOf 编号 :
InsertOf 来自于声明,所以一个错误的选择,缺少必要的一栏或拼写错误
列失败 tsc。 。 。 .
import { defineTypedTable, f } from "@ai-matrx/records/typed-table";
import { useTypedTable } from "@ai-matrx/records/react";
export const vendorContacts = defineTypedTable({
name: "Vendor contacts", slug: "vendor_contacts", scope: "organization", kept_for: "purchasing", key: "vendor",
fields: {
vendor: f.text({ required: true, unique: true }),
contact_name: f.text(),
category: f.select(["Medical supplies", "Lab services", "Facilities", "IT"] as const),
monthly_spend: f.currency("USD"),
},
});
// in a component under <RecordsProvider>: the table is made in the active organization on mount
const { rows, upsert } = useTypedTable(vendorContacts);
await upsert({ vendor: "Quest Diagnostics", category: "Lab services", monthly_spend: 1850 });
2. 从结构化的JSON——REST v1或AI Matrx MCP中 tables 工具 (单位:千美元)POST /api/v1/tables
与 X-Organization-Id; 磁共振 tables {action:"create_table", organization_id, name, columns}:
{ "name": "Vendor contacts", "columns": [
{ "name": "Vendor", "type": "text", "required": true, "unique": true },
{ "name": "Category", "type": "choice", "choices": ["Medical supplies", "Lab services"] },
{ "name": "Monthly spend", "type": "currency" } ] }
3. 来自SQL - POST /api/v1/sql 或 MCP 程序 sql 工具,一次一个声明,写成
邮政: CREATE TABLE vendor_contacts (vendor text not null unique, category text check (category in ('Medical supplies','Lab services')), monthly_spend numeric),则 INSERT …, (中文(简体) ),
SELECT … WHERE … ORDER BY … LIMIT, (中文(简体) ), UPDATE … WHERE, (中文(简体) ), DELETE … WHERE (档案), ALTER TABLE … ADD COLUMN子集和每次拒绝:援助性梦 aidream/services/table_api/FEATURE.md。 。 。 .
已经存在的表格 (在应用软件中制作,或由2或3方式制作)得到一个打入的定义,其中
pnpm tables:types <table id | slug> [--organization <id>] --out features/x/vendor-contacts.ts, (中文(简体) ),
发射 columns 地图加 …Row 编号 : …Insert 类型从活商店.
/typed-table- 月台保留自己的特性的表格
需要自己一个小表的特征,在代码中宣布一次;每个组织得到其 第一次使用的自有副本,行类型来自声明.
十分钟的路程
// features/decisions/decision-picks.typed-table.ts — imports only @ai-matrx/records/typed-table
import { defineTypedTable, f } from "@ai-matrx/records/typed-table";
export const decisionPicks = defineTypedTable({
name: "UI decision picks", slug: "ui_decision_picks", kept_for: "decisions",
scope: "person", owner: "lane:PLATFORM-APP-DATA", // owner is optional
key: "decision_id", // must be a unique field
fields: {
decision_id: f.text({ label: "Decision", unique: true, required: true }),
choice: f.select(["a", "b", "neither"] as const, { label: "Choice" }),
decided_at: f.datetime({ label: "Decided" }),
},
});
// in a component under <RecordsProvider>
const { rows, upsert, remove, loading, error, status } = useTypedTable(decisionPicks);
await upsert({ decision_id: "header-density", choice: "a" }); // choice: "maybe" does not compile
rows为:RowOf<typeof decisionPicks>[]个人所属的每个组织 客户端organizationIds当主机通过它们, 否则商店自己的列表(iam.my_orgs()(三); 呼叫者什么也没有通过.{ organizationId }缩到一(也是写作的地方). 没有它,写给活跃的组织(客户端organizationId) (中文(简体) ).total是商店的数 这个人可能看到的行;truncated当rows10 000起停止(APP_ROWS_CEILING)——读诵从来不默不作声.status实值"ok", (中文(简体) ),"drifted"(所储存的列与代码不同——报告,从未) 应用; 已存储的一列代码不再声明为漂移extra, (中文(简体) ),"archived"(商店) 拒绝再来一次:error说,命名桌子upsert同样的回答 句子——从已归档的表格中恢复)或null不明或拒绝.error这是 商店本身的句子; 在没有12号车道门的商店里door_absent。 。 。 .- 每个打入的表格都是由它的 sulter 和
kept_for: 一个人自己的同一颗子弹的桌子是 不同的桌子,从来不回答。 一个组织的每张打字台都住在一个家庭, "平面表格". - 密码取决于它:
ensureTypedTable标记每个副本code_depends: true(单位:千美元)app_table.declared_in从定义的declared_in) (中文(简体) ). 商店拒绝归档, 重命名( slug 或kept_for),移动或解开标记,穿过每个门archiveTypedTableDeliberately(client, { tableId, typedSlug, reason })- 一个人打出子弹, 拒绝代理. - 无头:
ensureTypedTable(client, def, { organizationId }), (中文(简体) ),checkTypedTable(client, def)(单位:千美元)status, (中文(简体) ),drift, (中文(简体) ),rowCount, (中文(简体) ),listAppRows, (中文(简体) ),upsertAppRow, (中文(简体) ),removeAppRow(商店的软 页:1 该商店拒绝在密钥下一个从未声明过的定义(23514名名名下)的值. - 毕业 (合同:共同文件)
projects/data-doctrine-adoption/v6/CONTRACT-PLATFORM-APP-DATA-GRADUATION.md: 定义添加graduatedTo: { token: "<schema>.<table>", map: { <field>: <column> | "custom_fields" | "association" } }一旦其标准实体表存在(通过platform.create_entity_table) (中文(简体) ). 每个申报 字段必须映射(与"association"; 光谱散列不移动.pnpm tables:graduate <module> <export> [--target clone|production] [--dry-run]复制每组活的和存档的行 组织的复制件(id, organization, 作者, 时间戳, 版本,存档状态,shown_to, (中文(简体) ),visibility保存;未申报的密钥custom_fields; 存储簿记+legacy_id→metadata· ; 关系值platform.associations),每个组织进行一次交易,ON CONFLICT (id) DO NOTHING,拒绝任何数 异议,然后档案 每一个现场副本通过custom.table_archive_deliberately("毕业为". 默认目标:克隆人. 纯洁 导出部件 :graduationProblems, (中文(简体) ),graduatedRow。 。 。 . - 构建器 :
text longText email phone url number integer decimal currency(unit) percent checkbox date datetime select multiSelect relation(otherDef) member attachment entityReference(kinds)财务报告和审定财务报表 仅读formula autonumber createdTime modifiedTime(缺席)InsertOf) (中文(简体) ).
大门 (Arman 2026-10-02号裁决,逐字记录
systems/data/custom-data/DECISIONS.md,"当平台可能将自己的数据保存在商店":
- 允许 : 实验,特征 很少人使用,未经证实的收养,短期中途停留,小数据 包括其行属于组织的平台功能(预订,录取名单).
- 禁止 : 大部分或所有的人使用 (tags), 一个明显的长期特征,数据 几乎不会改变形状, 或很多数据.
- 大小, 不是时间 : 没有记录理由或审查日期;表格为50 000行
* 包括所有组织,并每增加10 000人
checkTypedTable是因为rowCount输入 检查;确认步骤尚未建立).
范围,老实说: organization 排队由排队组织成员看到,每排
成员可以添加行(复制时与编辑级成员共享). person
排起"只有我":他们是 隐藏在别人的名单上 - 商店的上游清单
丢掉它们,所以另一个成员读到的只有她自己的一行——但是它们是 未锁定(单位:千美元)
有排行ID的成员仍然可以打开. 店面锁是店面的机密级别,
独立且需要所有者按表批准; 绝不放任何必须属于私人的东西
在一个 person 页:1. global 在平台组织中是一份,仅由服务器代码编写;
ensureTypedTable 拒绝,除非该组织被明确命名.
镜子——商店要说的话,半秒前说
import { predictWriteRefusals } from "@ai-matrx/records/core";
const problems = predictWriteRefusals({ fields, document, recordType });
// [{ field_key: "amount", sqlstate: "23514", predicted_by: "custom.validate_values",
// message: "Amount has to be at least 1" }]
这不是验证——商店验证,在写出门内,不能 绕行。 镜子上只写着店里要说的话,在店里 准确的句子,上面写着什么是看不见的(选择成员, 关系目标,任何需要查询的内容)。 沉默的意思是"问店".
/反应-绑住一次,然后钩子
import {
RecordsProvider, useTables, useFields, useRecords, useRecord, useRecordMutation,
} from "@ai-matrx/records/react";
<RecordsProvider config={{ dataSource: supabaseDataSource(supabase), actor: { actor: "user" }, organizationId: orgId }}>
{children}
</RecordsProvider>;
// A config without `actor` is refused by name ("RecordsProvider needs config.actor …").
const { tables } = useTables();
const { fields } = useFields(table_id);
const { rows, total, live, liveOffReason } = useRecords({ table_id, pageSize: 50 });
const { write, update, remove, conflict } = useRecordMutation();
live 绝不是妄言的;当实时不受约束的时候, liveOffReason 说
为什么在句子中屏幕可以显示.
/实体——平台记录(联系人,项目,任务)通过同一端口
通过相同的平台记录(联系人,项目,任务.) DataPort 小程序读取自定义
与表齐. 一个在商店实体门上打字的瘦客户端和一个主机 DataPort 实体
化名, 和符号第一反应钩 在记录的一个数据引擎上(@ai-matrx/records/data/react) (中文(简体) ).
读与写. create 保存到活动组织中的主机名( 无拒绝 )
organization_required, (中文(简体) ), update 发送所看见的版本(与
商店的句子, archive 编号 : restore 移动一行到垃圾堆和后面. 名单是实时的 当主机经过时 realtime: createEntityRealtime({ manager })
(和 organization: 其他人的改变是 rows:<alias> 命名标识的事件.
import { createEntityDataPort } from "@ai-matrx/records/entity";
import { DataPortProvider, hostEvents, useDataRows } from "@ai-matrx/records/data/react";
const port = createEntityDataPort({
dataSource: supabase, // the signed-in person's supabase-js client
aliases: { contacts: { kind: "entity", token: "party", organizationId } },
});
// in React:
// <DataPortProvider port={port} events={hostEvents(port)} portKey="applet-1" queryClient={qc}>
// const { rows, hasMore, loadMore, error } = useDataRows("contacts", { org: organizationId, sort: [{ column: "display_name" }] });
在 React app中,请命名类型,让引擎来做剩下的:
import { EntityDataProvider, useEntityList, useEntityWrite } from "@ai-matrx/records/entity/react";
// once, high: <EntityDataProvider dataSource={supabase} activeOrganizationId={activeOrg}>…
const contacts = useEntityList("party", { where: [{ column: "last_name", op: "eq", value: "Ellis" }] });
const write = useEntityWrite("party");
await write.update(contacts.rows[0]._id, { first_name: "Ada" }); // drawn at once, rolled back on refusal
条目
| 条目 | 什麽 |
|---|---|
@ai-matrx/records/entity | createEntityClient({ dataSource }) → list, (中文(简体) ), get, (中文(简体) ), columns, (中文(简体) ), insert, (中文(简体) ), update, (中文(简体) ), archive, (中文(简体) ), unarchive· ; toWire· ; mapEntityError· ; createEntityDataPort({ client | dataSource, aliases, activeOrganizationId }) → HostDataPort + 键 describe· ; createEntityRealtime |
@ai-matrx/records/entity/react | EntityDataProvider, (中文(简体) ), useEntityList, (中文(简体) ), useEntityColumns, (中文(简体) ), useEntityType, (中文(简体) ), useEntityRow, (中文(简体) ), useEntityWrite |
值得了解的行为
- 车道默认为该型号自有起降道,由商店决定; 2;
org是一个过滤器,从不锁. - Page 是密钥集: 光标是商店的
next_after。 。 。 . - 商店尚未提供的过滤器或排序器被名字与商店的句子拒绝,从未在浏览器中被模仿.
- 自定义字段读取自
row._custom过滤为_custom.<key>; 个人可能无法读取的字段到达null。 。 。 . - 商店的倒数封顶上停放的总数报告为:
null(未知),绝不是错误的号码.
当地力学: src/entity/FEATURE.md。前称软件包 @ai-matrx/entity-data (折旧;相同的API,更名进口: /core 财务报告和审定财务报表 /port → records/entity, (中文(简体) ), /react → records/entity/react) (中文(简体) ).
钻倒——宣布的定义
服务器演练( A)platform.drill_describe 编号 : platform.drill_ask)回答一个标准表格
登记册,或 已声明定义(单位:千美元) *.drill.ts 文件( 编号declareDrillDefinition({...}), (中文(简体) ),
形状在 scripts/drill-definitions/ai_usage.drill.ts汇编成每个定义的一个函数,
platform.drill_def__<key>()维度可能携带 level (爆发,显示,属性,
记录);UI方面是 useDrill 内 @ai-matrx/records-ui 和表格在
@ai-matrx/design-system (它的README § drill - down).
| 命令( 在本包中) | 对 |
|---|---|
pnpm drill:sync --write --lane <LANE> | 写一个活动文件( + 反向) , 用于每个活体不同的声明 |
pnpm drill:sync --write --lane <LANE> --only <key,key> | 同样的,只有你的定义, 从来不是同行的半干 |
pnpm drill:sync --check | 人口普查:每一份声明都健全,与其活体相当(另1个除外) |
pnpm drill:sync --check --only <key,key> | 定义的普查 |
... --target clone | 读取夜间拷贝而不是制作 |
--only 命名一个密钥, 没有宣布文件失败, 所以一个打字从不作为“ 无所事事 ” .
生成的一半
src/store.generated.ts 作者为 pnpm records:generate 从现场
商店自己的目录——来自商店词汇功能的词汇表,
字段类型 custom.parity_field_types()门从 pg_proc, (中文(简体) ),
来自内核行的内核id,来自 platform.entity_types。 。 。 .
pnpm check:store-registry 此软件包和存储时退出非零
不同的是,另一道门是怎样加成红色支票的
而不是一个惊喜.
那个套房
pnpm test 跑向现场排练分支。 每个案子都是一个
总是在 rollback里面的套房打开
产品开关,只满足自己的一半需求,并设置
request.jwt.claims 参加运动的测试[email protected])——所以每个
电话通过真正的门,真正的触发器和真正的守卫,作为一个
真实身份,然后分支是同字节(最后一个测试)
从新的连接中重新读取并证明它). 红色一半是相同的
电话没有这种状态, 由商店的信息短信断言.
图标
| 命令 | 它会做什么 |
|---|---|
pnpm records:generate | 重写 src/store.generated.ts 从现场商店 |
pnpm check:store-registry | 当软件包和商店有分歧时失败 |
pnpm test | 真正的门套房 |
pnpm build | {\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}你来干什么 dist (sm + cjs + d.ts),然后是客户端边界印章 |
pnpm check:package | · 出口协议 |