插件开发范式

数据库与数据归属

根据数据归属选择 Plugin 数据库或应用数据库,并管理 Drizzle schema 与迁移。

先判断数据是否必须跟随 Plugin 独立安装、替换和迁移,再选择数据库组织方式。不要仅因为代码写在 Plugin class 中,就默认使用 ctx.database。

数据与生命周期要求正确组织方式
Plugin 可以独立发布、安装或替换,数据也属于这个 PlugindefineDatabase() + ctx.require(Database).use(),每个 Plugin 使用独立实例
需要 Plugin 独立 lineage 或旧 generation handle 撤销defineDatabase() + ctx.require(Database).use()
fixed catalog、schema 和部署由同一个应用团队控制application-private database package
没有共享数据库,整个应用 就无法成立host prepare() + root-bound typed accessor
只有部分内置 Plugin 依赖共享数据库,其他 Plugin 应继续运行application-private provider Plugin + constructor dependency
多个内置 Plugin 的表必须 join、使用 foreign key 或共享原子事务application-private database package

Managed Plugin database 统一使用 PostgreSQL dialect 和 Drizzle。Plugin 作者只依赖 drizzle-orm,不选择 driver;部署宿主在 native PostgreSQL 与 PGlite 之间选择,同一份 schema、migration 和 query 不编写 driver 分支。

Application-private database 由应用自行选择 PostgreSQL、SQLite、ORM 和 migration 方案。它不是“多个 Plugin 共用一份 Plugin database”:使用它的 Plugin 是应用内部模块,不再拥有独立的数据可移植性,也不会自动获得 Pluxel 的 per-plugin lineage、隔离、旧 handle 撤销或 owner-scoped invalidation。

发布与否只是常见线索,不是最终判断。私有 Plugin 如果仍要被独立启停、替换并保留自己的数据,应该使用 managed database;公开 Plugin 如果不拥有结构化数据,则不需要数据库。

Managed Plugin database

选择 managed database 后,正式部署使用 native PostgreSQL;本机开发和测试使用 PGlite。两者的能力与验证边界见backend 选择。

宿主通过 database({ backend: pglite(...) }) 或 database({ backend: postgres(...) }) 显式安装;默认服务组合不安装数据库,见 宿主配置。插件包安装 drizzle-orm,driver 由宿主提供。下面的 schema 与插件文件放在同一个插件包内。

定义 Plugin schema

// @filename: database.ts
// 仅服务端
import { , , , ,  } from 'drizzle-orm/pg-core'
import {  } from '@pluxel/services/database'

export const  = (
	'notes',
	{
		: ('id').().(),
		: ('title').(),
		: ('created_at', { : true }).().(),
	},
	() => [('notes_created_at_idx').(.)],
)

export const  = ({
	: {  },
})

// @filename: NotesPlugin.ts
import { , type  } from '@pluxel/services/database'
import { ,  } from '@pluxel/core'
import { ,  } from './database.ts'

@({ : 'Notes' })
export class  extends  {
	private !: <typeof >

	protected override async () {
		this. = await this..().()
	}

	() {
		return this..(() => .().().(.))
	}

	(: string) {
		return this..(async () => {
			const [] = await .().({  }).()
			return 
		})
	}
}

使用普通 pgTable()。不要使用 pgSchema()、Plugin name prefix、public. 或 physical schema;owner isolation 由 database instance 和 runtime namespace 管理。

schema module 是 server-only。Workbench API/browser bundle 不能导入它。Plugin package 依赖受支持的 drizzle-orm,不依赖 pg、PGlite 或其他 driver。

defineDatabase() 必须作为 module-level const 的直接 initializer,显式 evolution 必须写在直接 object literal 中。不要把它包进 function、branch 或 config factory;compiler 必须静态看到 package 唯一的 definition,才能注入对应 artifact。

use() 在返回前完成 active instance 选择和 migration prepare。handle 绑定当前 Plugin owner/generation,stop/replacement 后旧 handle 失效。

读操作放进 read(),写操作放进完整 transaction() callback。callback 内是标准 Drizzle database/transaction;不要缓存 callback 参数或 row lock,也不要在 transaction 中等待网络、用户交互或 worker task。

一个 Plugin 只能 use() 一个 definition。definition 可以作为 schema 模板由多个 Plugin 复用,但每个 owner 都有独立数据、role 和 operation queue;不能跨 Plugin 共享 handle 或 transaction。

PluginPart 也不拥有第二个 definition;它的表和 migration 都归属根 Plugin 的这一个 definition。Part 只帮助组织代码和资源,不创造新的数据边界。

Migration evolution

每个 definition 只走一条表演进流程;不要在两者之间混用 artifact 或命令:

evolution改表后的作者动作repository 中的 artifact
migrations(默认)修改 schema → generate --name → check → 提交 → build提交 drizzle/;history 只能追加
reset-on-schema-change修改 schema → build/测试不创建 drizzle/;generate、check、rebase 会明确拒绝

保留权威数据

默认 evolution: 'migrations'。在 definition 所在 package root 运行:

pluxel database generate --name add-notes
pluxel database check
pluxel build

提交整个 drizzle/,包括 SQL、meta/ 和 pluxel-migrations.json。已经提交的 SQL 不可重写;schema 变化生成下一条 migration。

production startup 只应用 build 时检查过的 artifact,不执行 schema push,也不根据当前 TypeScript schema 临时猜 DDL。 普通 pgTable() 之间生成的外键在插件独立 schema 内解析;生成器不会把指向本插件表的引用固定到 PostgreSQL 的 public schema。显式声明的其他 schema 引用不会被改写。

明确重建 lineage

如果产品明确放弃旧结构和 history,生成新 lineage:

pluxel database rebase --lineage 2026-v2 --name initial
pluxel database check

命令在 staging 生成当前 schema 的全新 baseline,成功后才替换本地 drizzle/。部署时 runtime 创建隔离 candidate instance,全部 migration 成功后原子激活;旧 instance 与数据归档保留。

不要手工修改 lineage,也不要把临时权限、锁、连接中断或 SQL error 当作 rebase 信号。

可丢弃数据自动 reset

缓存、搜索索引和可重新生成的同步副本可以声明:

export const SearchDatabase = defineDatabase({
	schema: { documents },
	evolution: 'reset-on-schema-change',
})

这种 definition 只运行 pluxel build,不创建或提交 drizzle/;generate、check、rebase 会明确拒绝它。构建器从当前 schema 生成 baseline:相同 schema 保留数据,table/column/index/constraint 改变时建立空 candidate,准备成功后替换 active instance 并归档旧数据。

这个声明意味着未来任何 schema 变化都允许清空数据。需要保留或转换旧数据时必须使用 migrations。

跨 Plugin 数据关系与扩展

独立发布的 Plugin 不能修改另一个 Plugin 的表模型:不要向其 pgTable() 加列、生成 ALTER TABLE、添加 foreign key/index,也不要把对方 table object 当作自己的 schema import。表结构和 migration 是 owner 可独立替换的存储 contract;允许第三方修改会把安装顺序、卸载、rebase 和 rollback 耦合在一起。

需求正确做法
为另一个 Plugin 的实体保存本 Plugin 专属数据extension Plugin 自己拥有表,以 provider 公开的稳定 entity ID 关联,并通过 typed capability 验证或操作该实体
需要 join、foreign key、跨表原子 transaction,或 provider 必须直接查询/索引扩展字段将整组表放进同一 application-private database,由同一个应用团队迁移
需要可插拔的自定义字段provider 只在存在具体产品需求时发布自己的领域 extension protocol;不要增加通用“改别人表”的能力

第一种方式的两个写入是两个 transaction;需要一致性时,extension 应设计可重试、幂等的领域流程,而不是绕过 owner boundary。若这个代价不能接受,说明这些表本来就不应是独立 Plugin 数据。

不要通过猜测 physical schema、连接字符串或 owner prefix 跨界读另一个 Plugin 的表。Workbench target 也只能调用所属 Plugin 的领域 service,再返回 detached DTO;它不是跨 owner 数据库入口。

Document 风格数据

可以在稳定的 id、version、jsonb data 表中兼容多个 document version。JSON 字段内部变化不一定需要 SQL migration,但新增物理 index、constraint 或 column 仍是 schema evolution。

如果数据只是少量加密 JSON 且不需要 query/index,先评估 Vault;不要为一个 token 建完整关系表,也不要拿 Vault documents 替代需要查询的数据库。

在 Workbench 中读取数据库状态

Workbench 不提供数据库专用查询协议。Plugin 在自己的 Direct View API 中返回 bounded browser-safe snapshot, 需要更新时公开普通 watch(invalidate) capability;查询、分页、DTO 投影和 mutation 都留在 Plugin service/target:

interface NotesApi extends RpcTarget {
	listDto(input: { cursor: string | null; limit: number }): Promise<NotesPage>
	updateDto(input: UpdateNoteInput): Promise<UpdateNoteResult>
	watch(invalidate: () => void): RpcTarget
}

Target 内部可以使用 owner-bound database handle,但不能把 handle、Drizzle row 或 transaction 暴露给 browser。DTO 显式转换 Date、BigInt、Buffer 等 server values,并限制 rows/bytes。Mutation commit 后由 Plugin 自己触发 invalidation; Workbench 不解析 table identity,也不成为 database lifecycle owner。完整用法见 插件管理界面。

测试与发布检查

@pluxel/test/vitest 会对 database declaration 运行与开发/生产相同的 artifact transform:migration strategy 校验已提交 history,reset strategy 生成临时 baseline。

日常测试使用 memory:// PGlite;部署门禁增加真实 PostgreSQL integration suite。PGlite 覆盖语义与生命周期,native PostgreSQL suite 覆盖并发、锁、连接池和故障行为。

至少验证:

  • first startup 与 restart;
  • read/transaction callback;
  • migration failure 阻止 Plugin running;
  • required dependent 被 blocked、无关 Plugin 继续;
  • stop/replacement 后旧 handle 失效;
  • package artifact 包含 dist/database/migrations/ 的 checked SQL/manifest。

不要在测试里调用 internal helper 或手工构造 migration artifact,否则测试没有覆盖作者真正发布的 schema。

应用共享数据库的装配见Application-private database;后端、连接池与部署验证见Host backend 选择。

最后更新于

本页目录