数据库写作
- 作者仓库星标 3,199
- 许可证 MIT
- 作者更新于 实时读取
- 作者仓库 remult
- 领域
- 文档
- 兼容 Agent
-
- Claude Code
- Cursor
- Cline
- Codex
- Windsurf
- Gemini CLI
- +20
- 信任分
- 94 / 100 · 已通过审计
- 作者 / 版本 / 许可
- @remult · MIT
- Token 消耗评级
- 低消耗
- 接入复杂程度
- 需简单配置
- 是否需要外部 API Key
- 不需要
- 兼容的系统
- macOS · Linux · Windows
- 底层运行要求
- Node.js
- 文件与系统权限
-
- 只读
- 允许写入 / 修改
- 读取环境变量
- 网络行为
- 允许外网请求
- 安装命令数
- 26 条
档案由构建时根据 SKILL.md 与安装命令自动衍生,可能与作者实际意图存在差异。
需要注意: 未限定 allowed-tools,默认拥有全部工具权限。
---
name: remult
description: Remult patterns - entities, fields, repo() usage, lifecycle hooks, permissions (allowApi + apiPr…
category: 文档
runtime: Node.js
---
# remult 输出预览
## PART A: 任务判断
- 适用问题:PRD、RFC、README、项目说明或知识库整理。
- 输入要求:目标材料、限制条件、期望输出和验收方式。
- 证据边界:围绕“repo() Usage / Entity ID Field / Entity Declaration”读取原文规则,不把推断写成作者承诺。
## PART B: 执行结果
- **01** 任务判断:确认你的需求是否属于PRD、RFC、README、项目说明或知识库整理,并标出输入、限制和预期结果。
- **02** 执行计划:优先按“repo() Usage / Entity ID Field / Entity Declaration”拆成步骤,说明每一步会读取什么、修改什么、产出什么。
- **03** 交付结果:给出可复制的命令、文件改动、检查清单或内容草稿,并说明如何继续迭代。
- **04** 风险边界:结合 读取文件、写入/修改文件、读取环境变量、会按任务需要访问外部网络、通常不需要额外 API Key 给出执行前确认项。
## Running Rules
- 读取文件、写入/修改文件、读取环境变量;会按任务需要访问外部网络;通常不需要额外 API Key。
- 先小样例验证,再放大到真实任务。
- 交付时同时给结果、检查口径和下一步迭代建议。 原文没有稳定的斜杠命令要求。安装验证后通常全局生效,直接在对话里点名这个 Skill 并描述任务即可。
告诉 Agent 目标文件或材料、期望结果、不可改范围、是否允许联网或执行命令。本 Skill 的权限画像是:读取文件、写入/修改文件、读取环境变量。
先用一个小任务确认它会围绕“repo() Usage / Entity ID Field / Entity Declaration”工作;涉及文件或命令时,先看 diff、日志、预览或测试结果。
检查最终产物是否包含明确结果、必要证据和下一步动作;如果输出泛泛而谈,就补充输入、边界和验收标准后重跑。
---
name: remult
description: Remult patterns - entities, fields, repo() usage, lifecycle hooks, permissions (allowApi + apiPr…
category: 文档
source: remult/remult
---
# remult
## 什么时候使用
- 把项目文档方向的常用动作沉淀成 Agent 可调用的技能 适合处理README、PRD、RFC、教程和知识库文档,核心价值是把输入、判断、执行、验证和交付边界固定下来,避免 Agent 泛泛回答。 把任务拆成可执行、可检查、可继续迭代的…
- 面向PRD、RFC、README、项目说明或知识库整理,优先处理能明确输入、步骤和验收标准的工作。
## 需要提供什么
- 目标材料、目录范围、期望结果和不可改动内容。
- 是否允许联网、执行命令、读写文件或调用外部服务。
## 执行规则
- 围绕「repo() Usage / Entity ID Field / Entity Declaration」组织步骤,不把推断写成作者事实。
- 读取文件、写入/修改文件、读取环境变量;会按任务需要访问外部网络;通常不需要额外 API Key。
- 先跑小样例,确认结果可检查后再扩大任务范围。
## 输出要求
- 给出最终产物、关键证据、验证方式和下一步动作。
- 信息不足时标记 unknown,不编造命令、平台或依赖。 作者原文负责流程事实;仓库文件负责来源和命令;流狐只补充适用场景、限制和质量判断。
skill "remult" {
输入层 -> 用户目标 + 目标文件 + 禁止范围 + 验收标准
上下文层 -> repo() Usage / Entity ID Field / Entity Declaration
规则层 -> SKILL.md 触发条件 / 执行顺序 / 输出格式
运行层 -> Node.js | 读取文件、写入/修改文件、读取环境变量 | 会按任务需要访问外部网络
安全层 -> 通常不需要额外 API Key + 小任务验证 + diff / 日志复核
输出层 -> 可复制结果 + 检查清单 + 下一步迭代
} Remult
Remult is a full-stack TypeScript framework: define an entity once, get a typed REST API, a typed client, validation, and permissions everywhere.
Need more docs? Fetch https://remult.dev/llms.txt - it's the curated index of every doc page. Pull the specific page you need from there.
repo() Usage
Always call repo(Entity) inline - never store in a variable. It's cheap, context-aware (frontend/backend), and per-request scoped.
await repo(Task).find(...)
Entity ID Field
Prefer @Fields.id() (UUID, no DB autoincrement coupling). Use @Fields.autoIncrement() only if a numeric DB-side sequence is required.
@Fields.id()
id!: string
Entity Declaration
import { Entity, Fields, Allow } from 'remult'
@Entity<Task>('tasks', {
allowApiCrud: Allow.authenticated,
allowApiDelete: 'admin',
})
export class Task {
@Fields.id() id!: string
@Fields.string() title = ''
@Fields.boolean() completed = false
}
Permissions: allowApi* vs. apiPrefilter
Two layers, applied in order:
allowApi*- entity-level gate. Acceptsboolean | Allow.* | Role | Role[] | (item, remult) => boolean. Decides whether a caller can hit this entity for read/insert/update/delete at all. Keys:allowApiCrud,allowApiRead,allowApiInsert,allowApiUpdate,allowApiDelete.apiPrefilter- row-level filter. Returns anEntityFilterthat's automatically AND-ed into every API query (find,count, updates, deletes). Decides which rows the caller can see/touch.
@Entity<Task>('tasks', {
allowApiRead: Allow.authenticated,
apiPrefilter: () =>
remult.isAllowed('admin')
? {} // admins see all
: { ownerId: remult.user!.id }, // everyone else: own rows only
})
Use apiPrefilter for row-level security - never recreate the WHERE clause on the client. The prefilter runs server-side regardless of what the client sends.
Lifecycle Hooks
Order on save: saving -> save -> saved. On delete: deleting -> delete -> deleted. Each hook receives (entity, event); event.isNew distinguishes insert from update.
@Entity<Post>('posts', {
saving(entity, event) {
if (event.isNew && remult.user) entity.userId = remult.user.id
},
async saved(entity, event) {
if (event.isNew) {
// server-only side effect
}
},
})
Server-only Code in Hooks / BackendMethods
For Node-only deps (sharp, fs, ...), wrap the server section in if (import.meta.env.SSR) { ... } and dynamically import() inside the block. Vite drops the entire branch from the client bundle.
@BackendMethod({ allowed: true })
static async log(msg: string) {
if (import.meta.env.SSR) {
const { appendFileSync } = await import('fs')
appendFileSync('./logs/log.txt', `${new Date().toISOString()} ${msg}\n`)
}
}
Same pattern in hooks:
@Entity<Post>('posts', {
saved: async (post) => {
if (import.meta.env.SSR) {
const { appendFileSync } = await import('fs')
appendFileSync('./logs/log.txt', `${new Date().toISOString()} saved ${post.id}\n`)
}
},
})
Do not use if (!import.meta.env.SSR) return as an early-return - it does NOT strip the Node-only deps from the client bundle. Always wrap the server-only section in if (import.meta.env.SSR) { ... }.
For more (abstract-the-call, bundler exclusion), see https://remult.dev/docs/using-server-only-packages.
Entity-first vs. BackendMethod
Remult's main lever: put logic in entity hooks and let clients call repo(X).insert/update/delete. Reach for BackendMethod only for genuinely cross-entity or client-invisible flows.
| Entity hook (preferred) | BackendMethod (when needed) |
|---|---|
| Default a field on insert | Multi-entity transactions |
| Per-row validation | Aggregations across many repos |
| Single-row side effects | Cross-entity bulk/clone ops |
| Image optimization on save | Reads from entities not exposed to clients |
Repository Methods - upsert
upsert matches by where, updates with set if found, inserts if not. Idempotent and works in single or batch form.
// single
await repo(Task).upsert({
where: { slug: 'hello' },
set: { title: 'Hello' },
})
// batch
await repo(Task).upsert([
{ where: { slug: 'a' }, set: { title: 'A' } },
{ where: { slug: 'b' }, set: { title: 'B' } },
])
Use it instead of hand-rolling find-then-insert-or-update.
ValueList Enums
Use over @Fields.enum / @Fields.literal it's more future-proof, supports extra properties (label, color, etc.) and behavior on each value, and is easier to maintain as the list grows.
import { ValueListFieldType, getValueList, ValueListInfo } from 'remult'
@ValueListFieldType()
export class TaskStatus {
static Open = new TaskStatus('open', 'Open', '#22c55e')
static Done = new TaskStatus('done', 'Done', '#94a3b8')
constructor(
public id: string,
public caption: string,
public color: string,
) {}
}
for (const s of getValueList(TaskStatus)) {
// s.id, s.caption, s.color
}
getValueList(EnumClass | fieldRef | fieldMetadata) returns the values (use to populate dropdowns). id is what's stored in the DB and sent over the API; caption is what you display.
Binding to <select> / query string
Cross any string boundary (HTML inputs, URLs, FormData, localStorage) via ValueListInfo.get(EnumClass):
toInput(instance)-> theidas a stringfromInput(idString)-> the instance
const info = ValueListInfo.get(TaskStatus)
// React <select>
<select
value={info.toInput(task.status)}
onChange={(e) => setTask({ ...task, status: info.fromInput(e.target.value) })}
>
{getValueList(TaskStatus).map((s) => (
<option key={s.id} value={info.toInput(s)} style={{ color: s.color }}>
{s.caption}
</option>
))}
</select>
// URL <-> instance
const params = new URLSearchParams(location.search)
const status = info.fromInput(params.get('status') ?? '')
params.set('status', String(info.toInput(TaskStatus.Open)))
In Svelte/Vue/Angular template syntax with two-way binds on objects, you bind the instance directly and skip toInput/fromInput - reach for them only when something forces strings.
Field Metadata in the UI
Don't hard-code field labels, types, or validators in templates - read them from repo(X).metadata.fields.<field> so a single source of truth drives the UI.
@Entity('tasks', { allowApiCrud: true })
export class Task {
@Fields.string({ label: 'Task title' })
title = ''
}
const f = repo(Task).metadata.fields.title
f.label // 'Task title'
f.key // 'title' (the field name)
f.valueType // String
f.options // raw FieldOptions (validate, includeInApi, ...)
<label htmlFor={f.key}>{f.label}</label>
<input id={f.key} placeholder={f.label} value={task.title} />
If a field doesn't set label, remult auto-generates one from the key (firstName -> First Name). Same auto-titleising applies to ValueList items.
Permission Checks in UI
Use entity metadata - never duplicate permission logic in components.
const canEdit = repo(Post).metadata.apiUpdateAllowed(post)
const canDelete = repo(Post).metadata.apiDeleteAllowed(post)
const canInsert = repo(Post).metadata.apiInsertAllowed
These re-evaluate with the current remult.user and (where relevant) the row, mirroring the server-side check exactly.
Relations - Typed Includes
@Fields.string()
authorId = ''
@Relations.toOne(() => User, { field: 'authorId' })
author?: User
Read with include:
await repo(Post).find({ include: { author: true } })
For one-to-many: @Relations.toMany(() => Comment, 'postId').
Reusable Filters - Filter.createCustom
Encapsulate complex/computed WHERE logic so it's reusable, type-safe, and runs server-side.
import { Entity, Fields, Filter, repo } from 'remult'
@Entity('orders')
export class Order {
@Fields.id() id!: string
@Fields.string() status = ''
@Fields.createdAt() createdAt = new Date()
static activeIn = Filter.createCustom<Order, { year: number }>(
async ({ year }) => ({
status: { $in: ['created', 'pending', 'confirmed'] },
createdAt: { $gte: new Date(year, 0, 1), $lt: new Date(year + 1, 0, 1) },
}),
)
}
await repo(Order).find({ where: Order.activeIn({ year: 2024 }) })
The arg generic types the call site; the filter body always runs on the server.
Computed Fields - sqlExpression
Fields backed by a SQL expression instead of a physical column. Filterable and sortable in one round-trip, no row loading.
@Entity('tasks')
export class Task {
@Fields.id() id!: string
@Fields.string() title = ''
@Fields.integer({ sqlExpression: () => 'length(title)' })
titleLength = 0
}
await repo(Task).find({ where: { titleLength: { $gt: 10 } } })
The function form receives (entity, args?, command?) so you can build dynamic expressions; pair with dbNamesOf(Entity) for safe identifiers.
SQL-Driven Relations / Derived Joins
Inline a related value via subquery using sqlExpression + dbNamesOf. Flat shape, single query, fully queryable from the API.
import { dbNamesOf, Entity, Fields, repo } from 'remult'
@Entity('orders')
export class Order {
@Fields.id() id!: string
@Fields.string() customerId = ''
@Fields.string<Order>({
sqlExpression: async () => {
const cust = await dbNamesOf(Customer)
const ord = await dbNamesOf(Order)
return `(select ${cust.city} from ${cust} where ${cust.id} = ${ord.customerId})`
},
})
customerCity = ''
}
await repo(Order).find({ where: { customerCity: 'London' } })
Use this when you want a derived column queryable by the API client without exposing the related entity or doing N+1.
Sharing Shape Across Entities
Prefer class extends class for shared fields + hooks. Concrete entities extend a base class.
abstract class Auditable {
@Fields.id() id!: string
@Fields.createdAt() createdAt = new Date()
@Fields.string() createdBy = ''
}
@Entity<Post>('posts', { allowApiCrud: Allow.authenticated })
export class Post extends Auditable {
@Fields.string() title = ''
@Fields.string() body = ''
}
@Entity<Comment>('comments', { allowApiCrud: Allow.authenticated })
export class Comment extends Auditable {
@Fields.string() postId = ''
@Fields.string() text = ''
}
For shared options (permissions, hooks), extract a typed helper returning EntityOptions<T> and spread it into each @Entity({...}).
Module Pattern
Bundle related entities + init logic into a module so apps register them in one line. See https://remult.dev/docs/modules.
Quick References
- Entity options: https://remult.dev/docs/ref_entity
- Field options: https://remult.dev/docs/ref_field
- Repository API: https://remult.dev/docs/ref_repository
- EntityFilter: https://remult.dev/docs/entityFilter
- Custom filters: https://remult.dev/docs/custom-filter
- Access control: https://remult.dev/docs/access-control
- BackendMethod: https://remult.dev/docs/ref_backendmethod
- Validators: https://remult.dev/docs/ref_validators
- Migrations: https://remult.dev/docs/migrations
- Admin UI: https://remult.dev/docs/admin-ui
- Full doc index: https://remult.dev/llms.txt
先判断是否适合
作者设计意图
作者的方法与取舍
边界和复核