proofkit-fmodata

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ProofKit FMOData

ProofKit FMOData

Type-safe ORM for FileMaker's OData API with TypeScript code generation.
为FileMaker的OData API打造的类型安全ORM,支持TypeScript代码生成。

Up-to-Date Documentation

最新文档

For the latest docs, fetch from proofkit.dev:
  • FMOData:
    https://proofkit.dev/llms/fmodata
  • Typegen:
    https://proofkit.dev/llms/typegen
  • All packages:
    https://proofkit.dev/llms-full.txt
如需获取最新文档,请访问proofkit.dev:
  • FMOData
    https://proofkit.dev/llms/fmodata
  • Typegen
    https://proofkit.dev/llms/typegen
  • 所有包
    https://proofkit.dev/llms-full.txt

Quick Setup

快速设置

bash
undefined
bash
undefined

1. Install packages

1. 安装包

pnpm add @proofkit/fmodata@beta @proofkit/typegen
pnpm add @proofkit/fmodata@beta @proofkit/typegen

2. Create config (proofkit-typegen.config.jsonc)

2. 创建配置文件(proofkit-typegen.config.jsonc)

npx @proofkit/typegen init
npx @proofkit/typegen init

3. Set env vars

3. 设置环境变量

FM_SERVER=https://your-server.com FM_DATABASE=YourDatabase.fmp12 OTTO_API_KEY=your-api-key # or FM_USERNAME/FM_PASSWORD
FM_SERVER=https://your-server.com FM_DATABASE=YourDatabase.fmp12 OTTO_API_KEY=your-api-key # 或使用FM_USERNAME/FM_PASSWORD

4. Generate types

4. 生成类型

npx @proofkit/typegen generate
npx @proofkit/typegen generate

5. Or use interactive UI

5. 或使用交互式UI

npx @proofkit/typegen ui
undefined
npx @proofkit/typegen ui
undefined

Define Tables

定义表

typescript
import { fmTableOccurrence, textField, numberField, timestampField } from "@proofkit/fmodata";
import { z } from "zod";

export const Users = fmTableOccurrence("Users", {
  id: textField().primaryKey().entityId("FMFID:100001"),
  name: textField().notNull(),
  email: textField().notNull(),
  active: numberField()
    .readValidator(z.coerce.boolean())
    .writeValidator(z.boolean().transform(v => v ? 1 : 0)),
  createdAt: timestampField().readOnly(),
}, {
  entityId: "FMTID:1000001",
  navigationPaths: ["Contacts", "Orders"],
});
typescript
import { fmTableOccurrence, textField, numberField, timestampField } from "@proofkit/fmodata";
import { z } from "zod";

export const Users = fmTableOccurrence("Users", {
  id: textField().primaryKey().entityId("FMFID:100001"),
  name: textField().notNull(),
  email: textField().notNull(),
  active: numberField()
    .readValidator(z.coerce.boolean())
    .writeValidator(z.boolean().transform(v => v ? 1 : 0)),
  createdAt: timestampField().readOnly(),
}, {
  entityId: "FMTID:1000001",
  navigationPaths: ["Contacts", "Orders"],
});

Query Patterns

查询模式

typescript
import { FMServerConnection, eq, and, gt, asc, contains } from "@proofkit/fmodata";

const connection = new FMServerConnection({
  serverUrl: process.env.FM_SERVER,
  auth: { apiKey: process.env.OTTO_API_KEY }
});
const db = connection.database("MyDatabase.fmp12");

// List with filters
const result = await db.from(Users).list()
  .where(and(eq(Users.active, true), gt(Users.age, 18)))
  .orderBy(asc(Users.name))
  .top(10)
  .execute();

// Get single record
const user = await db.from(Users).get("user-123").execute();

// Select specific fields
const result = await db.from(Users).list()
  .select({ userId: Users.id, userName: Users.name })
  .execute();

// String filters
.where(contains(Users.email, "@example.com"))
.where(startsWith(Users.name, "John"))
typescript
import { FMServerConnection, eq, and, gt, asc, contains } from "@proofkit/fmodata";

const connection = new FMServerConnection({
  serverUrl: process.env.FM_SERVER,
  auth: { apiKey: process.env.OTTO_API_KEY }
});
const db = connection.database("MyDatabase.fmp12");

// 带筛选条件的列表查询
const result = await db.from(Users).list()
  .where(and(eq(Users.active, true), gt(Users.age, 18)))
  .orderBy(asc(Users.name))
  .top(10)
  .execute();

// 获取单条记录
const user = await db.from(Users).get("user-123").execute();

// 选择特定字段
const result = await db.from(Users).list()
  .select({ userId: Users.id, userName: Users.name })
  .execute();

// 字符串筛选器
.where(contains(Users.email, "@example.com"))
.where(startsWith(Users.name, "John"))

CRUD Operations

CRUD操作

typescript
// Insert
const result = await db.from(Users)
  .insert({ name: "John", email: "john@example.com" })
  .execute();

// Update
const result = await db.from(Users)
  .update({ name: "Jane" })
  .byId("user-123")
  .execute();

// Delete
const result = await db.from(Users)
  .delete()
  .byId("user-123")
  .execute();

// Batch operations (atomic)
const result = await db.batch([
  db.from(Users).list().top(10),
  db.from(Users).insert({ name: "Alice", email: "alice@example.com" }),
]).execute();
typescript
// 插入记录
const result = await db.from(Users)
  .insert({ name: "John", email: "john@example.com" })
  .execute();

// 更新记录
const result = await db.from(Users)
  .update({ name: "Jane" })
  .byId("user-123")
  .execute();

// 删除记录
const result = await db.from(Users)
  .delete()
  .byId("user-123")
  .execute();

// 批量操作(原子性)
const result = await db.batch([
  db.from(Users).list().top(10),
  db.from(Users).insert({ name: "Alice", email: "alice@example.com" }),
]).execute();

Relationships

关系操作

typescript
// Expand related records
const result = await db.from(Users).list()
  .expand(Contacts, (b) =>
    b.select({ name: Contacts.name })
     .where(eq(Contacts.active, true))
  )
  .execute();

// Navigate from a record
const result = await db.from(Contacts).get("contact-123")
  .navigate(Users)
  .select({ username: Users.username })
  .execute();
typescript
// 展开关联记录
const result = await db.from(Users).list()
  .expand(Contacts, (b) =>
    b.select({ name: Contacts.name })
     .where(eq(Contacts.active, true))
  )
  .execute();

// 从记录导航到关联对象
const result = await db.from(Contacts).get("contact-123")
  .navigate(Users)
  .select({ username: Users.username })
  .execute();

Error Handling

错误处理

typescript
import { isHTTPError, ValidationError, TimeoutError } from "@proofkit/fmodata";

const result = await db.from(Users).list().execute();

if (result.error) {
  if (isHTTPError(result.error)) {
    if (result.error.isNotFound()) console.log("Not found");
    if (result.error.is5xx()) console.log("Server error");
  } else if (result.error instanceof ValidationError) {
    console.log("Validation failed:", result.error.issues);
  } else if (result.error instanceof TimeoutError) {
    console.log("Request timed out");
  }
}
typescript
import { isHTTPError, ValidationError, TimeoutError } from "@proofkit/fmodata";

const result = await db.from(Users).list().execute();

if (result.error) {
  if (isHTTPError(result.error)) {
    if (result.error.isNotFound()) console.log("未找到资源");
    if (result.error.is5xx()) console.log("服务器错误");
  } else if (result.error instanceof ValidationError) {
    console.log("验证失败:", result.error.issues);
  } else if (result.error instanceof TimeoutError) {
    console.log("请求超时");
  }
}

Troubleshooting

故障排查

Connection Issues

连接问题

"Unauthorized" or 401 errors
  • Verify
    OTTO_API_KEY
    or
    FM_USERNAME
    /
    FM_PASSWORD
    env vars
  • Ensure FM account has
    fmodata
    privilege enabled
  • Check OData service is enabled on FM Server
"Not Found" or 404 errors
  • Verify database name includes
    .fmp12
    extension
  • Check table/layout name matches exactly (case-sensitive)
  • Ensure OData is enabled for the table occurrence
"Unauthorized"或401错误
  • 验证
    OTTO_API_KEY
    FM_USERNAME
    /
    FM_PASSWORD
    环境变量是否正确
  • 确保FileMaker账户已启用
    fmodata
    权限
  • 检查FM Server上的OData服务是否已启用
"Not Found"或404错误
  • 验证数据库名称是否包含
    .fmp12
    扩展名
  • 检查表/布局名称是否完全匹配(区分大小写)
  • 确保该表布局已启用OData访问

Type Generation Issues

类型生成问题

typegen can't connect
  • Run
    npx @proofkit/typegen ui
    to debug interactively
  • Check connection health indicator in UI
  • Verify env vars are loaded (check
    --env-path
    flag)
Generated types don't match FM schema
  • Re-run
    npx @proofkit/typegen generate
    after FM schema changes
  • Use
    --reset-overrides
    to recreate override files
  • Check field type mappings in config
typegen无法连接
  • 运行
    npx @proofkit/typegen ui
    进行交互式调试
  • 检查UI中的连接健康状态指示器
  • 验证环境变量是否已加载(可使用
    --env-path
    参数指定路径)
生成的类型与FM模式不匹配
  • 在FM模式变更后重新运行
    npx @proofkit/typegen generate
  • 使用
    --reset-overrides
    参数重新创建覆盖文件
  • 检查配置中的字段类型映射

Query Issues

查询问题

"Field not found" errors
  • Ensure field is defined in
    fmTableOccurrence
  • Check
    entityId
    matches FM field ID (use typegen to auto-generate)
  • Verify field is on the OData-exposed table occurrence
Validation errors on read/write
  • Check
    readValidator
    /
    writeValidator
    schemas match FM data types
  • FM stores booleans as 0/1 numbers - use coercion validators
  • Empty strings may need
    .catch("")
    or
    .nullable()
"Field not found"错误
  • 确保字段已在
    fmTableOccurrence
    中定义
  • 检查
    entityId
    是否与FileMaker字段ID匹配(可使用typegen自动生成)
  • 验证该字段是否在OData暴露的表布局中
读写操作时出现验证错误
  • 检查
    readValidator
    /
    writeValidator
    模式是否与FileMaker数据类型匹配
  • FileMaker将布尔值存储为0/1数字 - 使用强制转换验证器
  • 空字符串可能需要使用
    .catch("")
    .nullable()
    处理

Performance Issues

性能问题

Slow queries
  • Add
    .top(n)
    to limit results
  • Use
    .select()
    to fetch only needed fields
  • Avoid expanding large related record sets
查询缓慢
  • 添加
    .top(n)
    限制结果数量
  • 使用
    .select()
    仅获取所需字段
  • 避免展开大量关联记录集

References

参考资料

  • fmodata-api.md - Complete API reference: field builders, operators, query methods
  • typegen-config.md - Configuration options and examples
  • fmodata-api.md - 完整API参考:字段构建器、操作符、查询方法
  • typegen-config.md - 配置选项及示例