intent: a status a processes: flow writes is refused to a direct create/update (#7339) - #7357
Open
delchev wants to merge 1 commit into
Open
intent: a status a processes: flow writes is refused to a direct create/update (#7339)#7357delchev wants to merge 1 commit into
delchev wants to merge 1 commit into
Conversation
…te/update (#7339) An entity whose `function: EntityStatus` relation is moved by a `processes:` step had that column generated as an ordinary writable property on all three REST surfaces, so a plain `PUT {"Status": 3}` moved the document straight into APPROVED with the flow bypassed end to end: no check ran, no task was ever raised, nothing the flow charges was charged - and the record read approved while the accounts knew nothing about it. `immutableWhen:` cannot close it (it locks the way OUT of a final status, while this is the way IN, from a DRAFT that is mutable by definition), and a `transitions[]` button is an ADDITIONAL guarded endpoint beside the plain PUT. The status is derived state owned by the flow, the same class of column as a roll-up target, so the EDM generator marks it - `workflowStatusProperty` plus `workflowStatusInitial` (the relation's `init:`), derived from the process that writes it rather than declared - and the three controller templates refuse a create or update that sets or changes it with 409 naming the column. The flow's own writers lose nothing: a `setRelationField` step and a `transitions[]` endpoint reach the repository through the targeted updateProperty / updateProperties primitives, never through a controller. Two deliberate non-refusals: an absent value is not a change - it is taken from the stored row, which is also what stops a partial payload from erasing the status - and a create carrying exactly the declared `init:` starts the record where the model says it starts. A `transitions:`-only entity keeps its writable column on purpose: the button is a hand move over a status a person may also hold otherwise, and the construct guarding every other hand write is `lifecycle:`, enforced in the repository precisely because writers other than the button exist. Claiming the column there would make an unmodeled move reachable from nowhere and the state machine's refusal observable from nowhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
The cause
An entity whose
function: EntityStatusrelation is moved by aprocesses:step had that column generated as an ordinary writable property on all three REST surfaces. A plainmoved the document into APPROVED with the flow bypassed end to end - no capacity check, no manager task, nothing charged - and the record read approved while the accounts knew nothing about it.
immutableWhen:cannot close it (it locks the way OUT of a final status; this is the way IN, from a DRAFT that is mutable by definition) and atransitions[]button is an additional guarded endpoint beside the plain PUT.The change
The status a flow writes is derived state owned by the flow, the same class of column as a roll-up target.
EdmIntentGenerator.putWorkflowStatusemitsworkflowStatusProperty(the FK) andworkflowStatusInitial(the relation'sinit:) on the entity map - derived from the process that writes it (writesStatus: asetRelationFieldstep of a process this entity triggers), not declared, since a model that states a flow over the status has already said who owns it. Scalars, so they reach the.edmtwin likeimmutableStatusValues.EntityController,EntityMyController,EntityPartnerController) refuse a create or update that sets or changes it: 409'Status' changes through the workflow, not a direct edit.setRelationFieldstep and atransitions[]endpoint reach the repository through the targetedupdateProperty/updatePropertiesprimitives, never through a controller.Two deliberate non-refusals:
init:starts the record where the model says it starts; any other value, and any value at all where noinit:is declared, is refused - a record cannot be created mid-flow.What I deliberately left out, and why
The issue asks for the refusal when a
processes:flow ortransitions:writes the status. Only theprocesses:half is implemented. Atransitions[]button is a user action over the status - the declared way a person moves it by hand - and the construct that guards every OTHER hand write islifecycle:, enforced in the generated repository precisely because writers other than the button exist. Claiming the column on the strength of a button would make an unmodeled move reachable from nowhere and the state machine's own refusal observable from nowhere (IntentEmissionCoverageIT's lifecycle assertions are exactly that: an illegal edge pushed through the plain REST surface, "which no transition guard covers"). That is a different construct removed rather than this one delivered. Happy to extend it if you want the broader reading - it is one predicate inwritesStatus, plus rerouting the coverage fixture's status writes through transition endpoints.Verification
EdmIntentGeneratorTest- the process-driven status emits both scalars, a same-nomenclature entity with no flow emits neither, a transitions-only entity stays writable (68 tests green).IntentWorkflowStatusIT(new, HTTP-only, ~35 s) - generates + publishes a real intent and asserts all four answers end to end: the DRAFT -> APPROVED jump is 409 with the message, a create naming APPROVED is 409, an ordinary edit carrying the status back unchanged is 200, an edit that omits it does not erase it, and the flow's ownsetRelationFieldstill moves the record to APPROVED on task completion.IntentEmissionCoverageIT(compiles + runs the generated client Java),IntentEngineIT(80),IntentCheckGateRefusalIT,ModelGenerationIT- green.components/engine/engine-intent+components/ide/ide-templateunit suites green;mvn formatter:validategreen with the cache wiped; javadoc release check green.Not run locally: the rest of the IT suite (CI's job).
Docs: dirigible-io/dirigible-io.github.io#244 (help site) and IntentFile/intent-specification#75 (spec proposal).
Fixes #7339
🤖 Generated with Claude Code