多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SparkyFitness 新数据库迁移与新增数据表完整操作清单(New Migration / New Table Checklist 实战指南)

SparkyFitness 新数据库迁移与新增数据表完整操作清单(New Migration / New Table Checklist 实战指南) 后端前端移动开发【免费下载链接】SparkyFitnessSparkyFitness: Built for Families. Powered by AI. Track food, fitness, water, and health — together.项目地址https://gitcode.com/gh_mirrors/sp/SparkyFitness点击查看免费下载导读本文是 SparkyFitness 开源仓库内部沉淀的工程规范 —— agent-docs/new-migration-checklist.md 的深度实战指南。它面向所有需要在SparkyFitnessServer中添加或修改数据库迁移尤其是新建数据表、或改变用户可见访问行为的开发者从创建迁移文件、配置 PostgreSQL 行级安全RLS策略、重启服务应用迁移到 Zod 契约、文档维护、下游前后端同步与最终验证覆盖一次完整 schema 变更的全部流程。读完本文你将掌握该仓库迁移 RLS 重放 契约 验证四段式的工作流并理解每一步背后的源码原理从而避免提交一个漏了 RLS 策略的新表这类最常见的 Review 失败。仓库是只读的本文只介绍查看、运行与配置方式不涉及对仓库内容的修改。一、工作流总览为什么这张清单存在该清单在文档开头就给出了一条明确警告本仓库最常见的 Review 失败就是一张新表漏掉了清单中的某一项。它的全部条目都在回答一个问题——当你在 SparkyFitness 里新增/变更一张数据库表时如何保证 schema、安全、契约、文档与测试五者同步整张清单可以压缩为一条流水线创建迁移 SQL → 配置 RLS 策略 → 重启服务验证 → CI 自动schema 备份 → Zod schema 契约 → 文档更新 → 下游路由/服务/前端/移动端同步 → 验证命令在深入每一步之前先理解仓库的两个关键机制迁移如何被应用、RLS 策略如何被重放。这两个机制决定了清单第 1、2 步的写法。1.1 迁移与 RLS 的启动时序源码证据清单第 1 步强调不要发明替代的迁移机制这是因为服务器启动时会自动完成先应用未执行的迁移、再重放 RLS 策略两件事。入口在 SparkyFitnessServer/index.tsinitializeDatabase()必须在任何业务模块被 import 之前执行注释明确说明 Better Auth 会在auth.ts模块作用域内急切地校验数据库 schema并在进程生命周期内缓存不匹配结果只有它自己的迁移 runner 才能使其失效——因此原始 SQL 迁移如果延后执行升级后首次启动会出现/api/auth全部请求失败的问题对应 issues #2469 / #2470。同时这些 import 必须保持动态db/poolManager.ts在模块加载时就从process.env构建两个 pg 连接池静态 import 会被提升到 dotenv/loadSecrets 之上导致连接池带着空配置被冻结。具体的初始化逻辑在 SparkyFitnessServer/utils/initializeDatabase.ts通过getSystemClient()取得 owner 连接pg_advisory_lock(hashtext(sparkyfitness:schema-initialization))获取实例间的互斥锁序列化多个服务器实例的 schema 初始化锁名即lockName见该文件第 7 行依次执行applyMigrations(client)与applyRlsPolicies(client)pg_advisory_unlock释放锁任何一步失败都会销毁客户端连接。迁移执行器 SparkyFitnessServer/utils/dbMigrations.ts 的具体行为先CREATE SCHEMA IF NOT EXISTS system; CREATE TABLE IF NOT EXISTS system.schema_migrations(...)用system.schema_migrations表记录已应用的迁移读取SparkyFitnessServer/db/migrations/下所有.sql文件并按文件名排序readdirSync(...).filter(file file.endsWith(.sql)).sort()对每个不在已应用集合中的文件执行 SQL → 写入system.schema_migrations记录全部迁移完成后调用grantPermissions(client)见 SparkyFitnessServer/db/grantPermissions.ts为应用角色授予 public/auth/system schema 的 USAGE、表与序列的增删改查、函数的 EXECUTE 权限以及system.schema_migrations的 SELECT 权限。关键推论因为迁移按文件名而非文件内声明去重且严格排序所以文件名里的时间戳必须唯一且单调递增一旦一个迁移文件被记录进schema_migrations改名或改内容都不会再被重新执行只能靠追加新的迁移来修正。1.2 RLS 策略的单一事实来源源码证据清单第 2 步指向 SparkyFitnessServer/db/rls_policies.sql。该文件头注释自称所有 RLS 策略的单一事实来源且每次服务器启动在迁移之后执行以保证一致的安全状态。它的执行结构对应applyRlsPolicies分为四个阶段Step 1一次性 purge public schema 下所有既有策略遍历pg_policies逐条DROP POLICYStep 2对文件内硬编码的约 100 张表逐一ALTER TABLE ... ENABLE ROW LEVEL SECURITY完整清单见该文件第 24-120 行Step 3定义公共辅助函数包括current_user_id()、authenticated_user_id()、set_app_context(UUID, UUID)见第 369-379 行、can_access_user_data(targetUserId, permissionType, authUserId)第 381-418 行、has_diary_access/has_checkin_read_access等域级助手、is_admin()Step 4定义并调用通用策略生成器见下文第 2.2 节。1.3 双客户端模型getClient与getSystemClient清单第 2 步还强调了一个仓库特有的约定其实现位于 SparkyFitnessServer/db/poolManager.ts模块加载时构建两个连接池ownerPoolSPARKY_FITNESS_DB_USER与appPoolSPARKY_FITNESS_APP_DB_USER配置项包括host/database/password/port默认 5432、max: 10、idleTimeoutMillis: 30000、connectionTimeoutMillis: 5000getClient(userId, authenticatedUserId?)第 72-95 行从 app 连接池借出客户端并执行SELECT public.set_app_context($1, $2)写入app.user_id与app.authenticated_user_id两个会话变量让后续查询在该用户的 RLS 上下文内执行authenticatedUserId为空时回退到AsyncLocalStorage中记录的认证用户再回退到userId本身调用方必须在finally中释放客户端上下文设置失败则release(true)销毁它getSystemClient()第 96-99 行从 owner 连接池借出裸客户端不做任何 RLS 上下文设置绕过 RLS只用于 admin/启动/迁移工作——这也正是initializeDatabase、grantPermissions使用它的原因。二、逐条执行清单从迁移文件到验证命令2.1 第 1 步创建迁移文件在SparkyFitnessServer/db/migrations/下创建名为YYYYMMDDHHMMSS_description.sql的迁移文件。不要发明其他迁移机制服务器启动会按文件名顺序应用待执行迁移随后重放 RLS 策略。命名规范可以从仓库现有迁移得到印证例如20250703170640_InitialDB.sql 202508201215_add_garmin_integration_schema.sql 20250831063900_create_mood_entries_table.sql 20260912150000_preserve_data_on_user_and_library_deletes.sql一个典型的新表迁移以mood_entries为例见 SparkyFitnessServer/db/migrations/20250831063900_create_mood_entries_table.sql长这样-- 1) 定义 updated_at 自动刷新的触发器函数若库里还没有 CREATE OR REPLACE FUNCTION trigger_set_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- 2) 建表主键默认 gen_random_uuid()user_id 外键级联删除 CREATE TABLE mood_entries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, mood_value INTEGER NOT NULL, notes TEXT, entry_date DATE NOT NULL DEFAULT CURRENT_DATE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 3) 挂上自动更新时间戳触发器 CREATE TRIGGER set_timestamp BEFORE UPDATE ON mood_entries FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp();从源码可以归纳出仓库迁移的几条约定新表迁移应遵循用户数据表一律带user_id列且REFERENCES auth.users(id) ON DELETE CASCADE而不是public.user——auth.users是 Better Auth 管理的认证表见 数据库安全分层文档时间戳统一为TIMESTAMPTZ NOT NULL DEFAULT NOW()需要自动更新的表挂set_timestamp触发器文档 docs/src/developer/database.md 提示库表删除与日记历史的语义由迁移20260912150000_preserve_data_on_user_and_library_deletes.sql引入库表行如exercises、foods被删除时日记条目里的exercise_id/food_id采用可空 ON DELETE SET NULL保持历史日志快照可读。新建可被日记引用的库表时需沿用这一约定。2.2 第 2 步配置行级安全每一张新表都必须做在SparkyFitnessServer/db/rls_policies.sql中添加或更新策略。决定谁能读写行仅 owner如 cycle/pregnancy、家庭共享用哪种可委派权限diary、checkin、medications还是reports、还是系统/admin。优先使用rls_policies.sql中现成的create_*_policy(...)生成器。牢记getClient(userId, authenticatedUserId?)设置 RLS 上下文getSystemClient()绕过 RLS仅供 admin/启动/迁移使用。仓库在rls_policies.sql的 Step 3 定义了三个会话级身份函数所有策略都以它们为基准security-tiers 文档 有系统化说明函数读取的会话变量语义current_user_id()app.user_id当前被查看的资料上下文ID家庭委派切换时改变authenticated_user_id()app.authenticated_user_id真正登录的认证 actor ID上下文切换时永不改变can_access_user_data(targetUserId, permissionType, authUserId)—校验认证用户对目标用户数据是否持有某逻辑权限diary/checkin/medications/symptoms/reports及只读变体Step 4 提供的通用策略生成器rls_policies.sql第 432-559 行附近是给新表选型时最该优先使用的工具create_owner_policy(table_name, id_column DEFAULT user_id)FOR ALL ... USING (id_col authenticated_user_id())—— 仅 owner 读写cycle/pregnancy 系列使用如create_owner_policy(cycles)、create_owner_policy(pregnancies)不可委派create_shared_owner_policy(table_name, id_column)SELECT 用current_user_id()上下文可见增删改用authenticated_user_id()—— 适合 Tier 2 的只读共享资料表create_diary_policy(table_name)select_policy走has_diary_read_access(user_id)modify_policy走has_diary_access(user_id)—— 对应can_manage_diary委派权限用于food_entries、exercise_entries、water_intake等日志表create_checkin_policy(table_name)读走has_checkin_read_access写要求authenticated_user_id() user_id OR has_family_access(user_id, can_manage_checkin)—— 对应can_manage_checkin用于mood_entries、sleep_entries、check_in_photos等健康检查类表create_medication_policy(table_name)/create_symptom_policy(table_name)分别对应can_manage_medications与can_manage_symptoms权限域create_library_policy(table_name, shared_column, permissions text[])面向公共/共享库表如create_library_policy(exercises, shared_with_public, ARRAY[can_view_exercise_library, can_manage_diary])、create_library_policy(foods, shared_with_public, ARRAY[can_view_food_library, can_manage_diary])shared_column传false表示不共享。决策路径结合 database-security-tiers.md 的三层分级先判断新表属于哪一层 ——Tier 1 严格私有凭证、API Key、SSO 令牌、2FA、私密日志仅 owner 可读写绝不可委派用create_owner_policyTier 2 只读共享profile、偏好、自定义库表owner 可写、被切换的委派人可读用create_shared_owner_policy或带权限数组的create_library_policyTier 3 委派可写日记/日程/健康日志按业务域选create_diary_policy/create_checkin_policy/create_medication_policy/create_symptom_policy。此外还有一类System-Only表如passkey_registration_tickets、openfoodfacts_product_read_rate_limit、rate_limit启用 RLS 但不挂任何策略只有getSystemClient()能访问。清单中系统/admin选项对应的正是这种模式。配套测试佐证仓库用 SparkyFitnessServer/tests/rlsPermissionMatrix.integration.test.ts 把整张 RLS 权限矩阵钉死成测试。测试内的DOMAIN: Recordstring, Domain映射是唯一事实来源每一个启用了 RLS 的表必须恰好出现一次如第 217 行mood_entries: checkin。完整性守卫测试会反向扫描数据库 —— 若有 RLS 表没被归类新表没人分类或映射里出现了已不再启用 RLS 的表测试直接失败。因此新建表后必须同步更新这个映射否则pnpm exec vitest run tests/rlsPermissionMatrix.integration.test.ts会挂掉。2.3 第 3 步启动服务器让迁移生效重启服务器在SparkyFitnessServer/下执行pnpm start。确认迁移干净地应用、RLS 策略无错地重放。检查服务器日志中的任何迁移失败。pnpm start对应nodemon见 SparkyFitnessServer/package.json。启动时index.ts会执行上文 1.1 节的初始化序列。判定标准是日志里依次出现Ensured schema_migrations table exists.Applying migration: 你的迁移文件名与Successfully applied migration: 文件名Permissions granted to application user.grantPermissions成功RLS 策略重放无报错applyRlsPolicies完成若迁移 SQL 本身有语法错误或引用了不存在的列initializeDatabase会捕获异常并process.exit(1)见 index.ts服务器直接拒绝启动——这是仓库刻意设计的快速失败。2.4 第 4 步Schema 备份自动完成无需手动操作不要在 PR 中更新根目录的db_schema_backup.sql。合并后 CI 会从迁移重新生成它并通过.github/workflows/schema-backup.yml开一个自动同步 PR。永远不要手工编辑它也不要提交从本地数据库导出的副本 —— 本地库会偏离由迁移推导出的 schema。这一步的要点是负向清单db_schema_backup.sql是迁移推导产物而非源任何手工改动都会在下次 CI 重生成时被覆盖或引入与迁移不一致的状态。提交 PR 时该文件应保持未修改。2.5 第 5 步添加 Zod schema跨包共享契约在shared/src/schemas/database/Table.zod.ts添加或更新该表的 Zod schema。从shared/src/index.ts导出它。如果该表支撑 API在shared/src/schemas/api/DomainName.api.zod.ts创建/更新请求/响应 schema。shared包是前端、移动端、后端三方共享的类型契约层。数据库层 schema 的写法以 shared/src/schemas/database/MoodEntries.zod.ts 为范本// Generated by ts-to-zod import { z } from zod; export const moodEntriesIdSchema z.string().and( z.object({ __brand: z.literal(public.mood_entries) }), ); const userIdSchema z.any(); export const moodEntriesSchema z.object({ id: moodEntriesIdSchema, user_id: userIdSchema, mood_value: z.number(), mood_tags: z.array(z.string()), notes: z.string().nullable(), entry_date: z.date(), created_at: z.date(), updated_at: z.date(), created_by_user_id: userIdSchema.nullable(), updated_by_user_id: userIdSchema.nullable(), }); export const moodEntriesInitializerSchema z.object({ id: moodEntriesIdSchema.optional(), user_id: userIdSchema, mood_value: z.number(), mood_tags: z.array(z.string()).optional(), notes: z.string().optional().nullable(), // ... });可以从该文件归纳出的约定主键列生成带__brand的品牌化 schema如z.literal(public.mood_entries)用于在类型层面区分不同表的主键user_id等外键统一走userIdSchema每个表通常导出三件套TableSchema完整行、TableInitializerSchema插入用必填/可选与 DB 默认值对齐、以及created_by_user_id/updated_by_user_id这类审计列的可空处理。导出在 shared/src/index.ts 中以export * from ./schemas/database/MoodEntries.zod.ts;第 79 行的形式导出并同步导出对应的 API schema第 1-10 行可见export * from ./schemas/api/...模式。API 层 schema 目录shared/src/schemas/api/中每个领域一个文件如CheckInMeasurements.api.zod.ts、DailyGoals.api.zod.ts。2.6 第 6 步文档同步更新docs/src/features/family-friends-sharing.md面向用户的共享行为。更新docs/src/developer/database-security-tiers.md把新表连同其权限类型加入并归类为 Tier 1 / Tier 2 / Tier 3。如果新增了领域分类更新docs/src/developer/database.md的表索引。这步把代码事实沉淀为团队认知。database-security-tiers.md以三层分级表格 每张表的写权限 / 读权限两列组织例如mood_entries属 Tier 3-C 检查类写入需can_manage_checkin读取需can_view_reports或can_manage_checkin。新表要做的就是在对应层级表格中补一行并把rlsPermissionMatrix测试中选定的域owner/diary/checkin/medication/symptom/library与文档描述对齐。2.7 第 7 步下游契约如果新表支撑 API创建/更新路由v2 路由的 Zod 路由 schema 放在SparkyFitnessServer/schemas/、service、repository、测试和 Swagger JSDoc。检查 Web 端SparkyFitnessFrontend/与移动端SparkyFitnessMobile/是否消费该契约并同步更新。仓库的纵向分层模式是routes/HTTP 入口 Zod 路由校验→ services/业务编排→ models/repository经 getClient 访问 DBv2 路由的请求/响应校验 schema 集中在SparkyFitnessServer/schemas/如measurementSchemas.ts、waterContainerSchemas.ts、foodSchemas.ts等同时结合shared包的 API schema每个路由文件都带 Swagger JSDoc 注释仓库的 Swagger 配置见 SparkyFitnessServer/config/swagger.ts新接口必须同步补注释否则 API 文档会缺条目repository 层通过getClient(userId, authenticatedUserId?)获取带 RLS 上下文的客户端例如models/measurementRepository.ts、models/foodEntry.ts等而foodCoreService等启动/内部流程在合适场景才使用系统连接——业务代码严禁随意绕过 RLS。2.8 第 8 步验证在SparkyFitnessServer/下运行pnpm run validate—— typecheck、lint、format 三连。运行贴近改动面的测试pnpm exec vitest run tests/domain*.test.ts。如果 API 变了还要在消费方前端、移动端验证。与清单对应的脚本定义见 SparkyFitnessServer/package.json命令实际执行用途pnpm run validatepnpm run typecheck pnpm run lint pnpm run format:check类型检查tsc --noEmit ESLint--max-warnings 0 Prettier 格式校验pnpm testvitest run全量测试pnpm exec vitest run tests/domain*.test.ts单测定向执行贴近改动面的快速反馈pnpm run test:coveragevitest run --coverage覆盖率测试pnpm run test:migrationstsx tests/migrate.script.ts迁移专项脚本针对新增表场景至少要跑的两类测试是RLS 完整性测试pnpm exec vitest run tests/rlsPermissionMatrix.integration.test.ts—— 它会校验所有启用 RLS 的表都被 DOMAIN 映射归类、每张表的策略引用了预期的辅助函数新表若忘了更新映射会在这里失败初始化/启动顺序测试pnpm exec vitest run tests/initializeDatabase.test.ts与tests/bootOrder.test.ts它们守卫迁移先于 Better Auth 校验的启动时序。三、把清单串成一次真实变更的落地过程假设你要新增一张睡前补水打卡表bedtime_water_entries仅 owner 私有、不参与家庭共享完整的落地顺序应该是写迁移SparkyFitnessServer/db/migrations/20261009120000_create_bedtime_water_entries_table.sql带user_id外键、时间戳默认值、必要触发器在rls_policies.sql的 Step 2 表清单中加入bedtime_water_entriesStep 4 调用create_owner_policy(bedtime_water_entries)在tests/rlsPermissionMatrix.integration.test.ts的DOMAIN映射中登记bedtime_water_entries: owner重启pnpm start观察日志确认迁移应用、权限授予、RLS 重放均成功在shared/src/schemas/database/BedtimeWaterEntries.zod.ts建 schema并在shared/src/index.ts导出如果暴露 API再建BedtimeWaterEntries.api.zod.ts在docs/src/developer/database-security-tiers.md的 Tier 1 表格补一行若有用户可见的共享行为变化则更新docs/src/features/family-friends-sharing.md若引入新领域分类则更新docs/src/developer/database.md若提供 API补routes含 Swagger JSDoc、services、modelsrepository 与测试并在前端/移动端按需消费运行pnpm run validatepnpm exec vitest run tests/bedtimeWater*.test.ts RLS 完整性测试。四、常见踩坑与快速自查对照清单最后自检以下是仓库中反复出现的问题部分来自 agent-docs/anti-patterns.md 与文档注释新表没进rls_policies.sql的启用清单→ 表完全裸奔RLS 完整性测试直接失败策略写死user_id current_user_id()而非authenticated_user_id()→ 家庭委派/上下文切换场景出现越权或不可见问题记住current_user_id是资料上下文authenticated_user_id才是真实 actor忘了在DOMAIN映射登记新表→rlsPermissionMatrix.integration.test.ts的 completeness guard 失败手工编辑/提交db_schema_backup.sql→ 与迁移推导结果漂移CI 自动生成的 sync PR 会再次覆盖迁移文件改名或复用时间戳→ 已被system.schema_migrations记录的文件不会再执行时间戳撞名会导致排序不稳定新 API 缺 Swagger JSDoc / 缺 v2 Zod 路由 schema→ 文档与契约断裂业务代码直接用 owner 连接→ 绕过 RLS 的通道应一律通过getClient(...)走 app 池并在finally释放只有 admin/启动/迁移才用getSystemClient()。五、参考资料仓库内清单原文agent-docs/new-migration-checklist.md迁移执行器SparkyFitnessServer/utils/dbMigrations.ts启动初始化SparkyFitnessServer/utils/initializeDatabase.ts 与 SparkyFitnessServer/index.tsRLS 单一事实来源SparkyFitnessServer/db/rls_policies.sql连接池与双客户端模型SparkyFitnessServer/db/poolManager.ts权限授予SparkyFitnessServer/db/grantPermissions.ts安全层级说明docs/src/developer/database-security-tiers.md数据库总览docs/src/developer/database.md家庭共享行为docs/src/features/family-friends-sharing.mdRLS 权限矩阵测试SparkyFitnessServer/tests/rlsPermissionMatrix.integration.test.tsZod 契约示例shared/src/schemas/database/MoodEntries.zod.ts 与 shared/src/index.ts迁移示例SparkyFitnessServer/db/migrations/20250831063900_create_mood_entries_table.sql赞分享后端前端移动开发【免费下载链接】SparkyFitnessSparkyFitness: Built for Families. Powered by AI. Track food, fitness, water, and health — together.项目地址https://gitcode.com/gh_mirrors/sp/SparkyFitness点击查看免费下载相关推荐SparkyFitness 数据库迁移与建表实战从 SQL 迁移、RLS 策略到跨端 Zod 契约的完整清单SparkyFitness 数据库迁移与建表实战从 SQL 迁移、RLS 策略到跨端 Zod 契约的完整清单 本指南围绕 SparkyFitness 仓库中强后端前端移动开发Yii 2 数据库迁移Database Migration完整实战指南从建表、回滚到多数据库迁移Yii 2 数据库迁移Database Migration完整实战指南从建表、回滚到多数据库迁移 在开发与维护数据库驱动型应用的过程中数据库结构会像源代后端Web框架openvr_fsr高级调试技巧GPU性能分析和图像质量评估openvr_fsr高级调试技巧GPU性能分析和图像质量评估 openvr_fsr是一款为SteamVR游戏提供AMD FidelityFX SuperRes游戏开发图形学上一篇Python-readability性能优化处理大规模网页数据的3个终极策略下一篇告别GPU依赖用llama-cpp-pythonLangChain构建企业级本地AI应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表