@openreachtech/mentsu-action-value
v0.0.0
Published
Renchan action value - module to handle the value of an action in ORT crm-kit
Keywords
Readme
@openreachtech/mentsu-action-value
The shared record-action contract for Mentsu: the envelope a seeded action is stored
in, the vocabulary its payload draws on, and BaseRecordActionHandler — the one class a
frontend writes to declare a new action.
Zero runtime dependencies, pure ESM, browser-safe. The backend emits the envelope; the frontend fires it; neither can drift from the other on what an action value is.
The envelope
Exactly three properties. Every action, without exception, has these and nothing else at the top level:
{
"actionType": "INVOKE_FORM_SEQUENCE",
"originObjectCategoryId": 5,
"payload": {}
}actionType— the discriminator; selects the handler class. It is the action's master category name, supplied alongside the stored value. The stored value's ownactionType, if a seed still carries one, is never read.originObjectCategoryId— the origin object category the action is based on.payload— an object. This package requires that it is one; what is inside belongs to the action.
That is the whole of the fixed part. An action whose actionType no handler serves simply
yields no handler — the button does nothing rather than crashing the view.
The payload vocabulary
Every payload property whose value is not a plain literal is a two-key envelope,
{ sourceValueType, value }:
| sourceValueType | value | resolved from |
| :-- | :-- | :-- |
| FIXED_VALUE | the literal itself, any JSON type | — |
| COLUMN_REFERENCE | { sourceOriginObjectColumnId?, columnKey? }, at least one | columns |
| RECORD_UNIQUE_KEY | omitted | originObjectUniqueKey |
| FORM_SEQUENCE | { formSequenceId } | submittedValues |
Not every property is a value source: plain scalars (hasAssociationSource: true) and
nested objects (associationSourceRecord) stay as they are, and each is resolved by what
its shape says it is. At most one form sequence per action.
{
"actionType": "INVOKE_FORM_SEQUENCE",
"originObjectCategoryId": 5,
"payload": {
"submittedValues": {
"sourceValueType": "FORM_SEQUENCE",
"value": { "formSequenceId": 40001 }
},
"hasAssociationSource": true,
"associationSourceRecord": {
"originObjectUniqueKey": { "sourceValueType": "RECORD_UNIQUE_KEY" }
},
"otherProperty": { "sourceValueType": "FIXED_VALUE", "value": 123 },
"otherProperty3": {
"sourceValueType": "COLUMN_REFERENCE",
"value": {
"sourceOriginObjectColumnId": 605101,
"columnKey": "client_job_offer_industries_industry_major_category_id"
}
}
}
}Install
npm install @openreachtech/mentsu-action-valueConsuming repos that pin @openreachtech to GitHub Packages in .npmrc need a scope
override for this one — it is published to npmjs.org.
Declaring an action
One class. No registration call, no shared list to edit.
import {
BaseRecordActionHandler,
} from '@openreachtech/mentsu-action-value'
import CopyOriginObjectRecordMutationGraphqlLauncher from '~/app/graphql/client/mutations/copyOriginObjectRecord/CopyOriginObjectRecordMutationGraphqlLauncher'
export default class InvokeCopyRecordActionHandler extends BaseRecordActionHandler {
/** @override */
static get actionType () {
return 'INVOKE_COPY_RECORD'
}
/** @override */
static get MutationLauncherCtor () {
return CopyOriginObjectRecordMutationGraphqlLauncher
}
/** @override */
static get modalKey () {
return null
}
/** @override */
buildActionMutationPayload ({
columns,
originObjectUniqueKey,
submittedValues,
}) {
const resolved = this.resolveFields({
columns,
originObjectUniqueKey,
submittedValues,
})
return {
originObjectCategoryId: this.originObjectCategoryId,
originObjectUniqueKey: Number(resolved.originObjectUniqueKey),
}
}
}How a host reads a handler
| modalKey | MutationLauncherCtor | behaviour |
| :-- | :-- | :-- |
| null | set | build the input, call the mutation |
| set | set | open the modal with buildActionModalProps(), take the values it submits, build the input, call the mutation |
| set | null | open the modal with buildActionModalProps(); the modal submits itself |
One action, one mutation: an action that needs a different mutation is a different action, with its own seeded row and its own handler class.
buildActionModalProps() defaults to the mutation's input built without submitted values,
which is right whenever the modal opens on the same object the mutation takes; override it
when the two shapes differ.
Registry
The glob must live in the app — a bundler rewrites it at build time and a package cannot glob its consumer's directory — but everything after it belongs to this package.
import {
ActionHandlerRegistry,
} from '@openreachtech/mentsu-action-value'
const registry = ActionHandlerRegistry.fromModules(
import.meta.glob('./handlers/*.js', { eager: true })
)
const handler = registry.createHandler({
actionValue: action.actionValue,
actionType: action.actionType,
originObjectCategoryId: record.originObjectCategoryId,
})Where the three fixed properties come from
actionValue may arrive as the stored JSON string — a backend that hands the column
through untouched never has to read, parse or reshape it, and a malformed seed can never
break its query. In that arrangement the other two properties are supplied alongside it:
actionType from the action's own master category, originObjectCategoryId from the
record the action fires on. The parser folds all three into one envelope.
Where the stored value already is an envelope and declares those properties too,
originObjectCategoryId and payload keep what the column says — that is the action's
configuration, and the column is where it is configured.
actionType is not taken from the column at all. It comes from the supplied
master-category name, and from nowhere else. The category is what classifies an action,
and that classification picks the handler and therefore the mutation — so the column an
author edits must not be able to redirect a row to a different action than the one it is
filed under. A seed that still carries an actionType of its own is carrying dead data:
nothing reads it, and it does not reach the payload either.
A payload is trusted: it is seeded data, checked where it is authored. Nothing here
validates it — an unreadable action value yields no handler, and an unreadable property
resolves to null.
API
| export | purpose |
| :-- | :-- |
| BaseRecordActionHandler | the extension point: one subclass per action type |
| ActionHandlerRegistry | actionType → handler class; fromModules() |
| ActionValueParser | stored JSON (string or object) → canonical envelope |
| ValueSourceResolver | the four source value types |
| ACTION_FAMILY / SOURCE_VALUE_TYPE / LEGACY_SOURCE_VALUE_TYPE | the vocabulary |
| MAX_PAYLOAD_DEPTH | how deep a payload's own object nesting may go. Past it a payload resolves to {} rather than raising — read it when authoring a seed, since nothing validates one |
Handler members
| member | purpose |
| :-- | :-- |
| hasFormSequence() | whether the payload contains a FORM_SEQUENCE value |
| extractFormSequenceId() | that form sequence's id, or null |
| extractFormSequenceDescriptor({ columns, originObjectUniqueKey }) | the resolved { formSequenceId, hasAssociationSource, associationSourceRecord } |
| resolveFields({ columns, originObjectUniqueKey, submittedValues }) | the payload with every value source resolved |
| buildActionMutationPayload({ … }) | the mutation's input — see the behaviour table |
| buildActionModalProps({ columns, originObjectUniqueKey }) | the props the action's modal opens with |
The build parameters are the record the action fires from plus submittedValues — what
the action's modal submitted — not one per action, so the signature only widens when this
package adds a source value type. The action value itself is never a parameter: the
instance holds it.
Legacy rows
A row stored before the envelope existed — a bare payload with no payload property —
parses the same way, from the actionType supplied alongside it, so an un-migrated
database keeps working:
parser.parse({
actionValue: row.actionValue, // '{"memoId":{"sourceValueType":"RECORD_UNIQUE_KEY"},…}'
actionType: 'INVOKE_UPDATE_MEMO',
originObjectCategoryId: 7,
})On the way in, the parser
- hoists a payload-level
originObjectCategoryIdonto the envelope, - rewrites
FORM_FIELD_REFERENCEtoCOLUMN_REFERENCE, - folds a legacy
{ sourceValueType, fixedValue, formFieldReference }triple and a bare{ columnKey }reference into the two-key envelope.
A legacy bare formSequenceId in the payload is still read by
extractFormSequenceId(), so an un-migrated form-sequence action still opens its form.
A COLUMN_REFERENCE resolves to the column's value verbatim; a handler converts it to
what its field means — Number(resolved.memoId) for an id, String(resolved.title ?? '')
for text.
Scope
- In: the record-action envelope, its payload vocabulary, the handler extension point.
- Out: column edit-trigger tools (condition-shaped, served by
@openreachtech/mentsu-validation-rules), search conditions (owned by@openreachtech/mentsu-search-condition), and resolving anything the host alone knows — which record is on screen, which mutation client to call.
Development
npm run lint
npm testLicense
This project is released under the Apache License 2.0.
For more details, please see in the LICENSE file.
Developer
Copyright
© 2026 Open Reach Tech Inc.
