Firestore使用Batch Write更新关联数组时如何编写安全规则保证原子性
完整安全规则实现
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /students/{studentID} { allow read; allow create, update: if noChange('classes') || (isFieldAdd('classes') && matchAdd(studentID, 'classes', 'students')) || (isFieldRemove('classes') && matchRemove(studentID, 'classes', 'students')); allow delete: if checkDocDelete('classes', 'students'); } match /classes/{classID} { allow read; allow create, update: if noChange('students') || (isFieldAdd('students') && matchAdd(classID, 'students', 'classes')) || (isFieldRemove('students') && matchRemove(classID, 'students', 'classes')); allow delete: if checkDocDelete('students', 'classes'); } // 校验字段未变更 function noChange(field) { return !(field in request.writeFields); } // 判断字段为新增关联ID场景 function isFieldAdd(field) { return request.resource.data[field].removeAll(resource.data.get(field, [])).size() == 1; } // 判断字段为移除关联ID场景 function isFieldRemove(field) { return resource.data.get(field, []).removeAll(request.resource.data[field]).size() == 1; } // 获取新增的关联ID function getAddedId(field) { return request.resource.data[field].removeAll(resource.data.get(field, []))[0]; } // 获取移除的关联ID function getRemovedId(field) { return resource.data.get(field, []).removeAll(request.resource.data[field])[0]; } // 新增场景校验:对面文档的关联数组已包含当前文档ID function matchAdd(currentDocId, localField, remoteField) { let remoteDocPath = /databases/$(database)/documents/$(localField)/$(getAddedId(localField)); return currentDocId in getAfter(remoteDocPath).data.get(remoteField, []); } // 移除场景校验:对面文档的关联数组已不包含当前文档ID function matchRemove(currentDocId, localField, remoteField) { let remoteDocPath = /databases/$(database)/documents/$(localField)/$(getRemovedId(localField)); return !(currentDocId in getAfter(remoteDocPath).data.get(remoteField, [])); } // 整个文档删除校验:所有关联的对面文档都已移除当前文档ID function checkDocDelete(localField, remoteField) { let remoteIds = resource.data.get(localField, []); return remoteIds.size() == 0 || remoteIds.all(id => !(request.resource.id in getAfter(/databases/$(database)/documents/$(localField)/$(id)).data.get(remoteField, []))); } } }
逻辑说明
所有校验都基于getAfter()获取批量操作后的最终状态,符合批量操作的原子性要求,只要任意一侧校验不通过,整个批量会全部回滚:
- 拆分了新增、移除、文档删除三个场景分别校验,完全覆盖要求的规则:
- 数组修改必须同步修改对面:新增和移除场景都会校验对面的最终状态
- 字段未变更不触发校验:
noChange判断直接放行对应操作 - 不可单独删除关联字段/单独删除文档:移除单个关联ID时校验对面已同步移除,删除整个文档时校验所有关联的对面文档都已移除当前文档的ID
- 默认支持单条关联ID的新增/删除操作,如果你需要支持批量修改多个关联ID,只需修改对应辅助函数的判断逻辑,把
size() == 1改成size() >=1,同时遍历所有变更的ID逐一校验即可。
内容的提问来源于stack exchange,提问作者Jonathan
相关产品推荐
相关产品推荐

