-
-
Notifications
You must be signed in to change notification settings - Fork 407
feat(#10706): add bulk operation framework and contact hierarchy delete #11236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vikrantwiz02
wants to merge
17
commits into
medic:10706-dmp-2026-add-api-endpoints-for-advanced-contact-management-operations
Choose a base branch
from
vikrantwiz02:10706-delete-contact-hierarchy
base: 10706-dmp-2026-add-api-endpoints-for-advanced-contact-management-operations
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
d4e0d80
feat(#10706): add bulk operation document model
vikrantwiz02 25f8e20
build(#10706): add @medic/bulk-operations to package-lock
vikrantwiz02 a088c6b
feat(#10706): add bulk operation status endpoint
vikrantwiz02 aa5a218
feat(#10706): add bulk operation queue helper
vikrantwiz02 aaa0546
feat(#10706): process bulk operations in sentinel
vikrantwiz02 50ed392
feat(#10706): add contact hierarchy delete endpoints
vikrantwiz02 77448a5
refactor(#10706): reduce delete-contact service complexity
vikrantwiz02 93d333c
test(#10706): cover the sentinel bulk-operation listener
vikrantwiz02 1526ef0
docs(#10706): use standard JSDoc types so the generated docs build pa…
vikrantwiz02 2b7b945
feat(#10706): declare the can_delete_contact_hierarchy permission
vikrantwiz02 b403287
Merge branch '10706-dmp-2026-add-api-endpoints-for-advanced-contact-m…
vikrantwiz02 a21c063
refactor(#10706): share the contact-delete handler and OpenAPI docs
vikrantwiz02 8428c7f
feat(#10706): enforce contact type on the delete endpoints
vikrantwiz02 3c454d1
refactor(#10706): address review feedback on the bulk-operation delete
vikrantwiz02 2a1b6ce
fix(#10706): resolve sonarcloud code smells in the bulk-operation act…
vikrantwiz02 1060de6
test(#10706): add integration tests for the contact deletion endpoints
vikrantwiz02 3361340
test(#10706): drop the archive-gated skipped tests flagged by sonarcloud
vikrantwiz02 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| const service = require('../services/bulk-operations'); | ||
| const serverUtils = require('../server-utils'); | ||
| const auth = require('../auth'); | ||
|
|
||
| module.exports = { | ||
| v1: { | ||
| /** | ||
| * @openapi | ||
| * /api/v1/bulk-operations/{id}: | ||
| * get: | ||
| * summary: Get the status of a bulk operation | ||
| * operationId: v1BulkOperationIdGet | ||
| * description: > | ||
| * Returns the log document for a bulk operation, including the per-action status and the | ||
| * count of changes applied so far. Used to poll the progress of an operation that was | ||
| * started through one of the bulk endpoints. | ||
| * tags: [Bulk] | ||
| * x-since: 5.3.0 | ||
| * parameters: | ||
| * - in: path | ||
| * name: id | ||
| * required: true | ||
| * schema: | ||
| * type: string | ||
| * description: The id of the bulk operation, as returned when it was started. | ||
| * responses: | ||
| * '200': | ||
| * description: The bulk operation log | ||
| * content: | ||
| * application/json: | ||
| * schema: | ||
| * type: object | ||
| * properties: | ||
| * _id: | ||
| * type: string | ||
| * description: The bulk operation id. | ||
| * start_date: | ||
| * type: string | ||
| * format: date-time | ||
| * description: When the operation was started. | ||
| * actions: | ||
| * type: object | ||
| * description: Per-action status, keyed by action id. | ||
| * additionalProperties: | ||
| * type: object | ||
| * properties: | ||
| * status: | ||
| * type: string | ||
| * enum: [queued, completed, failed] | ||
| * action: | ||
| * type: string | ||
| * enum: [archive, set-contact, delete-user] | ||
| * updated_date: | ||
| * type: string | ||
| * format: date-time | ||
| * total_changes_count: | ||
| * type: integer | ||
| * failed_operations: | ||
| * type: array | ||
| * description: The operations that failed, present only when status is failed. | ||
| * items: | ||
| * type: object | ||
| * '401': | ||
| * $ref: '#/components/responses/Unauthorized' | ||
| * '403': | ||
| * $ref: '#/components/responses/Forbidden' | ||
| * '404': | ||
| * $ref: '#/components/responses/NotFound' | ||
| */ | ||
| get: serverUtils.doOrError(async (req, res) => { | ||
| await auth.assertPermissions(req, { isOnline: true }); | ||
| const log = await service.getLog(req.params.id); | ||
| if (!log) { | ||
| return serverUtils.error({ status: 404, message: 'Bulk operation not found' }, req, res); | ||
| } | ||
| res.json(log); | ||
| }) | ||
| } | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| const { v7: uuid } = require('uuid'); | ||
| const db = require('../db'); | ||
| const { BULK_OPERATIONS } = require('@medic/constants'); | ||
|
|
||
| const { LOG_ID_PREFIX, ACTION_ID_PREFIX, OPERATIONS_ATTACHMENT, STATUSES } = BULK_OPERATIONS; | ||
| const OPERATIONS_CONTENT_TYPE = 'application/json'; | ||
|
|
||
| const getOperationUuid = (operationId) => operationId.slice(LOG_ID_PREFIX.length); | ||
| const generateOperationId = () => `${LOG_ID_PREFIX}${uuid()}`; | ||
| const generateActionId = (operationId) => `${ACTION_ID_PREFIX}${getOperationUuid(operationId)}:${uuid()}`; | ||
|
|
||
| // Params live in an attachment so advancing the cursor as Sentinel batches does not rewrite them. | ||
| const encodeOperations = (operations) => ({ | ||
| content_type: OPERATIONS_CONTENT_TYPE, | ||
| data: Buffer.from(JSON.stringify(operations)).toString('base64'), | ||
| }); | ||
|
|
||
| const buildActionDoc = (operationId, action, operations) => ({ | ||
| _id: generateActionId(operationId), | ||
| bulk_operation_id: operationId, | ||
| action, | ||
| cursor: 0, | ||
| total: operations.length, | ||
| _attachments: { | ||
| [OPERATIONS_ATTACHMENT]: encodeOperations(operations), | ||
| }, | ||
| }); | ||
|
|
||
| const buildLogAction = (action, totalChangesCount, date) => ({ | ||
| status: STATUSES.QUEUED, | ||
| action, | ||
| updated_date: date, | ||
| total_changes_count: totalChangesCount, | ||
| }); | ||
|
|
||
| const buildBulkOperation = (actionOperations, date) => { | ||
| const operationId = generateOperationId(); | ||
| const actions = actionOperations.map(({ action, operations }) => buildActionDoc(operationId, action, operations)); | ||
|
|
||
| const logActions = {}; | ||
| actions.forEach((actionDoc) => { | ||
| logActions[actionDoc._id] = buildLogAction(actionDoc.action, actionDoc.total, date); | ||
| }); | ||
|
|
||
| const log = { | ||
| _id: operationId, | ||
| start_date: date, | ||
| actions: logActions, | ||
| }; | ||
|
|
||
| return { log, actions }; | ||
| }; | ||
|
|
||
| // Guards against returning the other kinds of log doc that share the medic-logs database. | ||
| const getLog = async (id) => { | ||
| if (!id?.startsWith(LOG_ID_PREFIX)) { | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| const log = await db.medicLogs.get(id); | ||
| delete log._rev; | ||
| return log; | ||
| } catch (err) { | ||
| if (err.status === 404) { | ||
| return null; | ||
| } | ||
| throw err; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Queues a bulk operation: writes the log document to medic-logs (the status record the polling | ||
| * endpoint reads) and one action document per action type to medic-sentinel (what the Sentinel | ||
| * listener processes). The log is written first so it exists before the listener picks up an action. | ||
| * Action groups with no operations are skipped. | ||
| * @param {Object[]} actionOperations - one group per action type | ||
| * @param {string} actionOperations[].action - the action type (`archive`, `set-contact`, `delete-user`) | ||
| * @param {Object[]} actionOperations[].operations - the per-item params for that action | ||
| * @returns {Promise<string>} the bulk operation id | ||
| */ | ||
| const queue = async (actionOperations) => { | ||
| const nonEmpty = actionOperations.filter(({ operations }) => operations.length); | ||
| const { log, actions } = buildBulkOperation(nonEmpty, new Date()); | ||
|
|
||
| await db.medicLogs.put(log); | ||
| await db.sentinel.bulkDocs(actions); | ||
|
|
||
| return log._id; | ||
| }; | ||
|
|
||
| module.exports = { | ||
| getLog, | ||
| queue, | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If possible, can we also include the valid action properties here? E.g. each property on this
actionsrecord should have astatus, etc.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, spelled out the per-action shape (status/action/updated_date/total_changes_count/failed_operations).