从 Hasura v2 迁移到 Provisa¶
前提条件¶
- 一个正在运行的 Hasura v2 实例(v2.x),并已导出元数据。
- 以两种布局之一导出元数据:
CLI 的元数据目录:
这会创建一个 metadata/ 目录。databases/databases.yaml 列出每个数据源的类型
和连接,并通过 !include 索引指向其表和函数;
actions.yaml、cron_triggers.yaml、inherited_roles.yaml、remote_schemas.yaml 以及其余
文件位于其旁。较早的扁平布局(根目录下的 tables.yaml)
同样可以转换。
元数据 API 的单一文档:
curl -X POST http://localhost:8080/v1/metadata \
-H "X-Hasura-Admin-Secret: <secret>" \
-d '{"type":"export_metadata","args":{}}' > metadata.json
裸对象和 {resource_version, metadata} 信封都可使用。管理端导入
(Admin、Import Hasura Config)使用此形式;下面的命令行转换器
读取目录。
3. 已安装 provisa 包的 Python 3.11+。
CLI 用法¶
参数¶
| 参数 | 是否必需 | 说明 |
|---|---|---|
metadata_dir |
是 | 已导出的 Hasura v2 元数据目录的路径 |
选项¶
| 选项 | 默认值 | 说明 |
|---|---|---|
-o, --output FILE |
stdout | 输出 YAML 文件的路径 |
--source-overrides FILE |
无 | 包含按数据源覆盖连接配置的 YAML 文件 |
--domain-map KEY=VAL ... |
无 | 架构到域的映射(例如 public=core hr=people) |
--auth-env-file FILE |
无 | 包含 JWT/admin-secret 身份验证配置的 .env 文件路径 |
--dry-run |
关闭 | 仅解析并验证,不写入输出 |
数据源覆盖文件¶
一个以数据源名称为键的 YAML 文件,包含要覆盖的连接属性:
default:
host: prod-db.example.com
port: 5432
database: myapp
username: provisa_user
password: "${env:PROD_DB_PASSWORD}"
身份验证环境文件¶
一个 .env 风格的文件,包含要转换的 Hasura 身份验证配置。转换器执行以下映射:
- 带
jwk_url的 JWT -> Provisaprovider: oauth。 - JWT 的
claims_map-> Provisarole_mapping[]。 - Admin secret -> Provisa
superuser。 - Webhook 身份验证 -> 系统会发出警告(Provisa 无对应功能)。
报告¶
转换以一份分为三部分的报告结束:
- 原样带过来。 对象在 Provisa 中有对应物,转换后含义不变。
- 映射到更好的 Provisa 功能。 每一项都会说明为什么 Provisa 功能做得更好。
- 需手工重做。 每一项都会说明该怎么做。这些就是警告。
命令行转换器在仍有需重做的内容时,把报告输出到 stderr,标题为 Carried as、Mapped to 和 Redo by hand,各带一个计数。管理端导入随预览返回这三个列表。
功能对等表¶
| Hasura v2 功能 | 部分 | Provisa 对应项 | 说明 |
|---|---|---|---|
| 数据源(postgres、mysql、mssql、bigquery、citus) | 原样带过来 | sources[] |
类型已映射:pg/postgres -> postgresql,mssql -> sqlserver。明文写出的连接 URL 解析为 host/port/database/username/password。连接池大小保留。从环境变量读取的 URL 不会被猜测(见下文)。 |
| 表(已跟踪的表) | 原样带过来 | tables[] |
保留 schema 和表名。source_id 关联到数据源。 |
自定义表名(custom_name、custom_root_fields.select) |
原样带过来 | tables[].alias |
取 select、select_by_pk、custom_name 中第一个非空值。 |
自定义列名(column_config,较旧的 custom_column_names) |
原样带过来 | columns[].alias |
每一列的自定义名称成为它的别名。 |
其他根字段名(select_by_pk、select_aggregate、insert 等) |
映射 | 表的唯一字段 | 按名称报告。按键读取是对表的过滤,聚合是它的 _aggregate 字段,写入是它的变更。 |
| select 权限(列、过滤器) | 原样带过来 | columns[].visible_to[]、rls_rules[] |
列列表成为 visible_to;支持 *。过滤器转为 SQL;会话变量转为 current_setting('provisa.<name>')。 |
| 穿过 relationship 的过滤器 | 原样带过来 | rls_rules[] 中的 EXISTS 子查询 |
规则沿 relationship 到达它的表。 |
| insert、update 和 delete 权限 | 原样带过来 | tables[].write_grants[] |
每个角色和操作一条授权:列来自 columns,rows 来自 filter,check 来自 check,预设来自 set。角色获得 write 权利。 |
| admin 角色 | 原样带过来 | org_admin |
持有覆盖所有列和行的每项授权,并在每个导入的列上被指名。 |
| Object relationship | 原样带过来 | relationships[],cardinality: many-to-one |
已批准。GraphQL 字段是 Hasura relationship 的名称。 |
| Array relationship | 原样带过来 | relationships[],cardinality: one-to-many |
已批准。GraphQL 字段是 Hasura relationship 的名称。 |
| 继承角色(一个角色) | 原样带过来 | roles[].parent_role_id |
该角色成为父角色。 |
| 继承角色(多个角色) | 映射 | 持有它们授权并集的角色 | Provisa 角色只有一个父角色,因此并集被明确写在该角色上。 |
| Computed field | 重做 | 视图列 | 导出指明函数,但不含其函数体。报告给出需要添加的 SELECT。 |
| 已跟踪的函数 | 原样带过来,否则重做 | functions[] 命令 |
预览从数据库读取签名。读不到时,在 Commands 页面添加该函数。 |
| Action | 映射 | 调用处理程序的命令 | 处理程序 URL 中的 {{VAR}} 写作 ${env:VAR},在命令被调用时解析。 |
| Cron 触发器 | 原样带过来 | scheduled_triggers[] |
cron 表达式、URL、payload 和 enabled 标志。重试不会被带过来(警告)。 |
| Event 触发器 | 映射 | 针对表变更的 scheduled_triggers[] |
任何变更时每批变更触发一次。重试不会被带过来(警告)。 |
| Remote schema | 映射 | 落地为表的 sources[](graphql_remote) |
保留名称、URL、请求头和身份验证;根字段成为受治理的表。 |
| 枚举表 | 原样带过来 | 已创建表 | 不带过 is_enum 标志(Provisa 中无对应项)。 |
| Allow list、query collection、REST 端点 | 重做 | 无 | 出现在合并导出中时会被报告;不会转换。 |
Relationship¶
- 每个 relationship 的 GraphQL 字段是 Hasura 名称。表上已有该名称的字段时,relationship 采用 Provisa 给它的名称,报告会发出警告。
- 声明同一个 join 的两个 relationship 合并为一个。报告会说明哪个并入了哪个。
- 在多个列对上连接的 relationship 会被警告且不会创建,因为 Provisa 关系只在一个列对上连接。穿过它的行规则仍会转换。
- 仅由外键列声明的 object relationship 通过反向的 array relationship 解析;没有时会被警告。
写入授权¶
一条授权声明某个角色通过一种操作可以向某张表写入什么。空的 filter 或 check 表示所有行(TRUE)。set 值 X-Hasura-User-Id 成为 hasura_user_id 的 session 预设;其他任何值成为 literal 预设。预设列不会同时出现在授权的列中。
Action、已跟踪的函数和触发器¶
- Action 成为调用其处理程序的命令,对其权限所指名的角色可见(未指名任何角色时只对
org_admin可见)。异步 action 会被警告:命令等待处理程序的应答。 - 当预览能从 PostgreSQL 数据源读取已跟踪的函数的参数和返回表时,它成为命令。离线时,以及对于缺失、被重载或返回导入未注册的表的任何函数,它转入重做。
- Event 触发器 在其表的每次变更时触发,每批一次,并发送表和已变更的行,而不是每行一个带旧值和新值的事件。监听少于全部三种操作的触发器会被警告。
转换后步骤¶
- 审阅报告。 从
Redo by hand开始。 - 配置数据源连接。 导出明文写出的连接按原样转换。从环境变量读取的连接以空值到达。使用
--source-overrides,或直接编辑输出。 - 核对域分配。 没有
--domain-map时,所有表都落在default。 使用--domain-map public=core analytics=reporting将 schema 分配到域。 - 检查行规则。 过滤器会转换为 SQL。请审阅复杂的布尔
表达式(嵌套的
_and/_or/_exists)以及穿过 relationship 的过滤器。 - 重做所列内容。 Computed field、预览无法读取的已跟踪函数、 触发器重试和 webhook 身份验证没有自动转换。
- 设置身份验证。 如果你的 Hasura 实例使用 JWT/webhook 身份验证,请创建一个
身份验证环境文件,并用
--auth-env-file重新运行。 - 传入会话变量。 配置身份提供方,使令牌携带报告在“映射到”下列出的每个会话变量(例如
x-hasura-user-id,在顶层或 Hasura claims 对象内),或按成员将其记录为属性。 - 测试。 启动 Provisa 服务器,并针对你的数据源验证查询。
常见问题与故障排查¶
连接来自环境变量¶
如果数据源的 database_url 是环境变量引用({"from_env": "PG_URL"}),
其值不在导出中。转换器不会猜测任何值:该数据源的 host、数据库、
用户名和密码为空,报告会把它列在 Redo by hand 之下。
在导入的数据源设置中(或通过 --source-overrides)输入连接,然后再次
转换。
通配符列¶
当某项权限授予 columns: "*" 时,转换器会创建一个通配符列条目。转换后,
您可能希望通过检查实际的数据库架构,将其替换为明确的列列表。
事件触发器的保真度¶
Event 触发器变为一个针对其表变更的触发器,调用同一个 webhook。任何变更时它都按 每批变更触发一次,无论变更是经由 Provisa 还是直接写入数据库,并发送表和 已变更的行。Hasura 的每行旧值和新值、按操作过滤以及重试都不会被带过来; 报告会针对适用的每一项发出警告。 让处理程序只处理它应当处理的行,如果漏掉一次调用会有影响,请在处理程序中重试。
缺失角色¶
角色从表权限、action 权限和继承角色中收集。如果某个角色存在于 Hasura 中, 但不出现在其中任何一处,它就不会出现在输出里。
自定义根字段¶
select 和 select_by_pk 根字段名给出表别名。其他自定义根字段
(select_aggregate、insert、update、delete)按名称报告:Provisa 把
一张表作为一个字段提供,聚合和写入分别是它的 _aggregate 字段和它的变更。
示例¶
转换一个典型的 Hasura v2 项目,其中两个架构映射到不同的域:
# Export metadata from Hasura
hasura metadata export --endpoint http://localhost:8080
# Convert with domain mapping and source overrides
python -m provisa.hasura_v2 metadata/ \
-o provisa.yaml \
--domain-map public=core hr=people \
--source-overrides overrides.yaml \
--auth-env-file auth.env
# Dry run first to check for warnings
python -m provisa.hasura_v2 metadata/ --dry-run
输出结构(节选):
sources:
- id: default
type: postgresql
host: prod-db.example.com
port: 5432
database: myapp
domains:
- id: core
- id: people
tables:
- source_id: default
domain_id: core
schema_name: public
table_name: users
columns:
- name: id
visible_to: [org_admin, user]
- name: email
visible_to: [org_admin, user]
write_grants:
- role: user
operation: update
columns: [email]
rows: "id = current_setting('provisa.hasura_user_id')"
check: "TRUE"
- role: org_admin
operation: insert
columns: [id, email]
check: "TRUE"
- role: org_admin
operation: update
columns: [id, email]
rows: "TRUE"
check: "TRUE"
- role: org_admin
operation: delete
rows: "TRUE"
roles:
- id: user
capabilities: [query_development, write]
domain_access: ["*"]
rls_rules:
- table_id: users
role_id: user
filter: "id = current_setting('provisa.hasura_user_id')"
relationships:
- id: orders.user
source_table_id: orders
target_table_id: users
source_column: user_id
target_column: id
cardinality: many-to-one
graphql_alias: user