-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Summary
Creating or updating a Concept in OCL via the current workflow requires multiple sequential requests (e.g., create concept → add names → add descriptions → add mappings). When any one request fails (often due to timeouts or slow processing), the Concept ends up partially written on the OCL side (missing pieces). This is increasingly common and forces manual repair/retry logic client-side.
We need a single GraphQL mutation that can validate and apply a batch of concept actions atomically (all succeed or none persist), while returning the final, real-time Concept representation.
Problem
Current behavior
- The client performs multiple REST calls (or multiple GraphQL calls) for a single logical operation:
- Create concept
- Add/update Names
- Add/update Descriptions
- Add/update Mappings
- (and potentially other “concept extras”)
- Each action hits a different endpoint.
- Failures occur mid-flight:
- Timeouts / slow processing
- 5xx (some already addressed by PRs)
- 4xx from validations (client side issues; expected - I solved most of it, though)
- Result: partial state on OCL (concept created, but incomplete), which is hard to recover from reliably and leads to inconsistent UI/UX.
Why bulk import is not a fit
- Bulk import queues work and is not real-time.
- The client needs to immediately display the final resolved Concept after the action completes (no background queue).
Expected behavior
- The client sends one request describing all intended actions.
- Server performs:
- Validation of all actions (including permissions, schema validation, domain rules)
- If validation passes, apply all actions in a single atomic transaction
- Return the final Concept (or a structured error report if validation fails)
- If any action fails, no partial writes remain.
Proposed solution
Add a GraphQL mutation that accepts a list of concept operations to be executed atomically.
Mutation name
conceptBulkActions
Signature (proposal)
NOTE: naming/types can be aligned with existing GraphQL conventions in OCL.
type Mutation {
conceptBulkActions(id: Int, input: [ConceptBulkActionInput!]!): ConceptBulkActionsPayload!
}- If
idis provided → treat as update for that concept. - If
idis not provided → treat as create, and return the created concept id/URI.
Input type
input ConceptBulkActionInput {
uri: String! # e.g. "/orgs/CIEL/sources/CIEL/HEAD/concepts/..."
method: HttpMethod! # POST | PUT | PATCH | DELETE
body: JSON # request payload
}Where uri would include paths such as:
/orgs/CIEL/sources/CIEL/HEAD/concepts/(create concept)/orgs/CIEL/sources/CIEL/HEAD/concepts/{concept_id}/names//orgs/CIEL/sources/CIEL/HEAD/concepts/{concept_id}/descriptions//orgs/CIEL/sources/CIEL/HEAD/concepts/{concept_id}/mappings/- etc.
Payload type
type ConceptBulkActionsPayload {
ok: Boolean!
conceptId: Int
conceptUri: String
concept: Concept
errors: [ConceptBulkActionError!]
results: [ConceptBulkActionResult!]
}errorsshould be present whenok=false, and contain per-action details.resultsmay optionally contain per-action status/metadata whenok=true.
Example supporting types:
type ConceptBulkActionError {
index: Int! # index of the input action that failed
uri: String!
method: HttpMethod!
statusCode: Int
message: String!
details: JSON
}
type ConceptBulkActionResult {
index: Int!
uri: String!
method: HttpMethod!
statusCode: Int!
response: JSON
}Execution model
Validation-first + atomic apply
Implementation should ensure “all-or-none” semantics:
- Parse & validate all actions (schema + domain validation + permissions)
- Resolve target concept:
- If
idis null → create concept object in memory (or staged) - If
idprovided → fetch and lock the concept for update
- If
- Execute actions within a single DB transaction:
- If any action fails → rollback transaction
- Return:
ok=true+ final Concept- or
ok=false+ per-action error details (no DB changes persisted)
Real-time response requirement
Mutation must return the final resolved concept (including names/descriptions/mappings added by the action list) so the client can immediately display it.
Example request (create)
mutation CreateConceptAtomic($input: [ConceptBulkActionInput!]!) {
conceptBulkActions(input: $input) {
ok
conceptId
conceptUri
concept { id displayName ... }
errors { index message statusCode uri }
}
}Variables:
{
"input": [
{
"uri": "/orgs/CIEL/sources/CIEL/HEAD/concepts/",
"method": "POST",
"body": { "concept_class": "Diagnosis", "datatype": "N/A", "names": [] }
},
{
"uri": "/orgs/CIEL/sources/CIEL/HEAD/concepts/{newConceptId}/names/",
"method": "POST",
"body": { "name": "Example", "locale": "en", "name_type": "Fully Specified" }
},
{
"uri": "/orgs/CIEL/sources/CIEL/HEAD/concepts/{newConceptId}/mappings/",
"method": "POST",
"body": { "map_type": "SAME-AS", "to_concept_url": "..." }
}
]
}The server can support a placeholder mechanism for
{newConceptId}during execution (e.g., a token like$created.id), or apply actions after the initial create step while still remaining inside the same transaction.
Example request (update)
mutation UpdateConceptAtomic($id: Int!, $input: [ConceptBulkActionInput!]!) {
conceptBulkActions(id: $id, input: $input) {
ok
conceptId
concept { id displayName ... }
errors { index message statusCode uri }
}
}Acceptance criteria
- ✅ One mutation performs create/update + related sub-resource mutations in one request.
- ✅ No partial writes: if any sub-action fails, DB state remains unchanged.
- ✅ Returns the final, real-time Concept representation upon success.
- ✅ Provides structured per-action errors upon failure (including index + endpoint).
- ✅ Works for both create (no id) and update (id provided).
- ✅ Eliminates client-side “repair” logic for partial writes caused by timeouts.
Notes / open questions
- How should placeholders be handled for create-then-append actions (e.g., referencing created concept id within the same input list)?
- Should the mutation accept higher-level structured input (names/descriptions/mappings arrays) instead of generic
uri/method/body? (Generic is simpler to map from existing client behavior; structured is safer/cleaner long-term.) - Do we want to expose a generic “execute internal REST endpoints” mechanism, or implement a dedicated concept-specific atomic mutation only?
Proposed next steps
- Confirm whether maintainers prefer:
- A generic
uri/method/bodyexecutor, or - A concept-specific structured input model
- A generic
- Implement server-side transaction + validation strategy
- Add tests for:
- Timeout simulation / mid-flight failure
- 4xx validation failure
- 5xx internal failure
- Successful create and update flows
- Open PR with GraphQL schema + resolver + tests
Request for feedback
Would maintainers accept a PR implementing this mutation (conceptBulkActions) to provide atomic “all-or-none” semantics for concept create/update workflows and eliminate partial writes caused by timeouts/slow processing?