@mmapp/react 0.1.0-alpha.1 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/dist/index.d.mts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +70 -3
- package/dist/index.mjs +74 -12
- package/package.json +4 -3
- package/package.json.backup +0 -41
- package/src/Blueprint.ts +0 -216
- package/src/__tests__/Blueprint.test.ts +0 -106
- package/src/__tests__/action-context.test.ts +0 -166
- package/src/__tests__/actionCreators.test.ts +0 -179
- package/src/__tests__/builders.test.ts +0 -336
- package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
- package/src/__tests__/factories.test.ts +0 -229
- package/src/__tests__/loader.test.ts +0 -159
- package/src/__tests__/logger.test.ts +0 -70
- package/src/__tests__/type-inference.test.ts +0 -160
- package/src/__tests__/typed-transitions.test.ts +0 -126
- package/src/__tests__/useModuleConfig.test.ts +0 -61
- package/src/actionCreators.ts +0 -132
- package/src/actions.ts +0 -547
- package/src/atoms/index.ts +0 -600
- package/src/authoring.ts +0 -92
- package/src/browser-player.ts +0 -783
- package/src/builders.ts +0 -1342
- package/src/components/ExperienceWorkflowBridge.tsx +0 -123
- package/src/components/PlayerProvider.tsx +0 -43
- package/src/components/atoms/index.tsx +0 -269
- package/src/components/index.ts +0 -36
- package/src/conditions.ts +0 -692
- package/src/config/defineBlueprint.ts +0 -329
- package/src/config/defineModel.ts +0 -753
- package/src/config/defineWorkspace.ts +0 -24
- package/src/core/WorkflowRuntime.ts +0 -153
- package/src/factories.ts +0 -425
- package/src/grammar/index.ts +0 -173
- package/src/hooks/index.ts +0 -106
- package/src/hooks/useAuth.ts +0 -288
- package/src/hooks/useChannel.ts +0 -304
- package/src/hooks/useComputed.ts +0 -154
- package/src/hooks/useDomainSubscription.ts +0 -110
- package/src/hooks/useDuringAction.ts +0 -99
- package/src/hooks/useExperienceState.ts +0 -59
- package/src/hooks/useExpressionLibrary.ts +0 -129
- package/src/hooks/useForm.ts +0 -352
- package/src/hooks/useGeolocation.ts +0 -207
- package/src/hooks/useMapView.ts +0 -259
- package/src/hooks/useMiddleware.ts +0 -291
- package/src/hooks/useModel.ts +0 -363
- package/src/hooks/useModule.ts +0 -59
- package/src/hooks/useModuleConfig.ts +0 -61
- package/src/hooks/useMutation.ts +0 -237
- package/src/hooks/useNotification.ts +0 -151
- package/src/hooks/useOnChange.ts +0 -30
- package/src/hooks/useOnEnter.ts +0 -59
- package/src/hooks/useOnEvent.ts +0 -37
- package/src/hooks/useOnExit.ts +0 -27
- package/src/hooks/useOnTransition.ts +0 -30
- package/src/hooks/usePackage.ts +0 -128
- package/src/hooks/useParams.ts +0 -33
- package/src/hooks/usePlayer.ts +0 -308
- package/src/hooks/useQuery.ts +0 -184
- package/src/hooks/useRealtimeQuery.ts +0 -222
- package/src/hooks/useRole.ts +0 -191
- package/src/hooks/useRouteParams.ts +0 -100
- package/src/hooks/useRouter.ts +0 -347
- package/src/hooks/useServerAction.ts +0 -178
- package/src/hooks/useServerState.ts +0 -284
- package/src/hooks/useToast.ts +0 -164
- package/src/hooks/useTransition.ts +0 -39
- package/src/hooks/useView.ts +0 -102
- package/src/hooks/useWhileIn.ts +0 -48
- package/src/hooks/useWorkflow.ts +0 -63
- package/src/index.ts +0 -465
- package/src/loader/experience-workflow-loader.ts +0 -192
- package/src/loader/index.ts +0 -6
- package/src/local/LocalEngine.ts +0 -388
- package/src/local/LocalEngineAdapter.ts +0 -175
- package/src/local/LocalEngineContext.ts +0 -30
- package/src/logger.ts +0 -37
- package/src/mixins.ts +0 -1160
- package/src/providers/RuntimeContext.ts +0 -20
- package/src/providers/WorkflowProvider.tsx +0 -28
- package/src/routing/instance-key.ts +0 -107
- package/src/server/transition-context.ts +0 -172
- package/src/testing/index.ts +0 -9
- package/src/testing/useBlueprintTestRunner.ts +0 -91
- package/src/testing/useGraphAnalysis.ts +0 -18
- package/src/testing/useTestRunner.ts +0 -77
- package/src/testing.ts +0 -995
- package/src/types/workflow-inference.ts +0 -158
- package/src/types.ts +0 -114
- package/tsconfig.json +0 -27
- package/vitest.config.ts +0 -8
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* defineWorkspace — configuration function for mm.config.ts files.
|
|
3
|
-
*
|
|
4
|
-
* Declares workspace metadata used by the mmrc CLI (build, deploy, pull).
|
|
5
|
-
* This is a compile-time configuration — the function simply returns its
|
|
6
|
-
* input for the project compiler to consume.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface WorkspaceConfig {
|
|
10
|
-
slug: string;
|
|
11
|
-
name?: string;
|
|
12
|
-
version?: string;
|
|
13
|
-
category?: string;
|
|
14
|
-
description?: string;
|
|
15
|
-
models?: string[];
|
|
16
|
-
entries?: string[];
|
|
17
|
-
actions?: string[];
|
|
18
|
-
pages?: string[];
|
|
19
|
-
components?: string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function defineWorkspace(config: WorkspaceConfig): WorkspaceConfig {
|
|
23
|
-
return config;
|
|
24
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WorkflowRuntime — Class-based runtime for MindMatrix workflows
|
|
3
|
-
*
|
|
4
|
-
* Wraps StateMachine + EventBus + ActionDispatcher in a subscribable class
|
|
5
|
-
* that can be used outside of React hooks (e.g., in vanilla JS contexts).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
StateMachine,
|
|
10
|
-
EventBus,
|
|
11
|
-
ActionDispatcher,
|
|
12
|
-
createEvaluator,
|
|
13
|
-
WEB_FAILURE_POLICIES,
|
|
14
|
-
} from '@mindmatrix/player-core';
|
|
15
|
-
import type {
|
|
16
|
-
PlayerWorkflowDefinition,
|
|
17
|
-
TransitionResult,
|
|
18
|
-
ActionHandler,
|
|
19
|
-
ExpressionContext,
|
|
20
|
-
PlayerAction,
|
|
21
|
-
Evaluator,
|
|
22
|
-
} from '@mindmatrix/player-core';
|
|
23
|
-
|
|
24
|
-
export interface RuntimeConfig {
|
|
25
|
-
definition: PlayerWorkflowDefinition;
|
|
26
|
-
initialData?: Record<string, unknown>;
|
|
27
|
-
actionHandlers?: Record<string, (config: Record<string, unknown>, ctx: ExpressionContext) => void | Promise<void>>;
|
|
28
|
-
debug?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface RuntimeSnapshot {
|
|
32
|
-
currentState: string;
|
|
33
|
-
stateData: Record<string, unknown>;
|
|
34
|
-
memory: Record<string, unknown>;
|
|
35
|
-
status: 'ACTIVE' | 'COMPLETED' | 'CANCELLED';
|
|
36
|
-
availableTransitions: string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class WorkflowRuntime {
|
|
40
|
-
readonly sm: StateMachine;
|
|
41
|
-
readonly eventBus: EventBus;
|
|
42
|
-
readonly dispatcher: ActionDispatcher;
|
|
43
|
-
readonly evaluator: Evaluator;
|
|
44
|
-
private listeners = new Set<(snap: RuntimeSnapshot) => void>();
|
|
45
|
-
|
|
46
|
-
constructor(config: RuntimeConfig) {
|
|
47
|
-
// 1. Create evaluator with WEB_FAILURE_POLICIES.EVENT_REACTION
|
|
48
|
-
this.evaluator = createEvaluator({
|
|
49
|
-
functions: [],
|
|
50
|
-
failurePolicy: WEB_FAILURE_POLICIES.EVENT_REACTION,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// 2. Create action handlers map (built-in set_field, set_memory + custom)
|
|
54
|
-
const actionHandlers = new Map<string, ActionHandler>();
|
|
55
|
-
|
|
56
|
-
// Wrap custom ActionHandlerFn (config, ctx) into ActionHandler (action, ctx)
|
|
57
|
-
if (config.actionHandlers) {
|
|
58
|
-
for (const [type, handler] of Object.entries(config.actionHandlers)) {
|
|
59
|
-
actionHandlers.set(type, (action: PlayerAction, ctx: ExpressionContext) =>
|
|
60
|
-
handler(action.config, ctx)
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Built-in set_field handler
|
|
66
|
-
let smRef: StateMachine | null = null;
|
|
67
|
-
actionHandlers.set('set_field', (action: PlayerAction) => {
|
|
68
|
-
if (smRef && typeof action.config.field === 'string') {
|
|
69
|
-
smRef.setField(action.config.field, action.config.value);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Built-in set_memory handler
|
|
74
|
-
actionHandlers.set('set_memory', (action: PlayerAction) => {
|
|
75
|
-
if (smRef && typeof action.config.key === 'string') {
|
|
76
|
-
smRef.setMemory(action.config.key, action.config.value);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// 3. Create StateMachine(definition, initialData, { evaluator, actionHandlers })
|
|
81
|
-
this.sm = new StateMachine(
|
|
82
|
-
config.definition,
|
|
83
|
-
config.initialData ?? {},
|
|
84
|
-
{ evaluator: this.evaluator, actionHandlers },
|
|
85
|
-
);
|
|
86
|
-
smRef = this.sm;
|
|
87
|
-
|
|
88
|
-
// 4. Create EventBus + ActionDispatcher
|
|
89
|
-
this.eventBus = new EventBus();
|
|
90
|
-
this.dispatcher = new ActionDispatcher();
|
|
91
|
-
|
|
92
|
-
// 5. Mirror custom handlers to dispatcher
|
|
93
|
-
// Register built-in handlers on dispatcher (for on_event action dispatch)
|
|
94
|
-
this.dispatcher.register('set_field', (cfg: Record<string, unknown>) => {
|
|
95
|
-
if (smRef && typeof cfg.field === 'string') {
|
|
96
|
-
smRef.setField(cfg.field, cfg.value);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
this.dispatcher.register('set_memory', (cfg: Record<string, unknown>) => {
|
|
100
|
-
if (smRef && typeof cfg.key === 'string') {
|
|
101
|
-
smRef.setMemory(cfg.key, cfg.value);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Mirror custom action handlers to dispatcher (dispatcher uses ActionHandlerFn signature)
|
|
106
|
-
if (config.actionHandlers) {
|
|
107
|
-
for (const [type, handler] of Object.entries(config.actionHandlers)) {
|
|
108
|
-
this.dispatcher.register(type, handler);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
getSnapshot(): RuntimeSnapshot {
|
|
114
|
-
return {
|
|
115
|
-
currentState: this.sm.currentState,
|
|
116
|
-
stateData: this.sm.stateData,
|
|
117
|
-
memory: this.sm.getSnapshot().memory,
|
|
118
|
-
status: this.sm.status as RuntimeSnapshot['status'],
|
|
119
|
-
availableTransitions: this.sm.getAvailableTransitions().map(t => t.name),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
subscribe(listener: (snap: RuntimeSnapshot) => void): () => void {
|
|
124
|
-
this.listeners.add(listener);
|
|
125
|
-
return () => this.listeners.delete(listener);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async transition(name: string, data?: Record<string, unknown>): Promise<TransitionResult> {
|
|
129
|
-
const result = await this.sm.transition(name, data);
|
|
130
|
-
this.notify();
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
setField(field: string, value: unknown): void {
|
|
135
|
-
this.sm.setField(field, value);
|
|
136
|
-
this.notify();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
setMemory(key: string, value: unknown): void {
|
|
140
|
-
this.sm.setMemory(key, value);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
publishEvent(topic: string, payload?: Record<string, unknown>): void {
|
|
144
|
-
this.eventBus.emit(topic, payload);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private notify(): void {
|
|
148
|
-
const snap = this.getSnapshot();
|
|
149
|
-
for (const listener of this.listeners) {
|
|
150
|
-
listener(snap);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
package/src/factories.ts
DELETED
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transition factories — reusable workflow patterns as ModelDefinition mixins.
|
|
3
|
-
*
|
|
4
|
-
* Each factory returns a mixin function compatible with ModelBuilder.mixin().
|
|
5
|
-
* Factories add states, transitions, fields, and roles as a cohesive unit.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import { model, field, state } from '@mindmatrix/react';
|
|
10
|
-
* import { approval, escalation, review, crud } from '@mindmatrix/react/factories';
|
|
11
|
-
*
|
|
12
|
-
* export default model('invoice')
|
|
13
|
-
* .field('amount', field.currency().required())
|
|
14
|
-
* .state('draft', state.initial())
|
|
15
|
-
* .mixin(approval({ approverRole: 'manager', field: 'status' }))
|
|
16
|
-
* .mixin(escalation({ timeout: '24h', notifyRole: 'admin' }))
|
|
17
|
-
* .build();
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import type {
|
|
22
|
-
ModelDefinition,
|
|
23
|
-
TransitionDescriptor,
|
|
24
|
-
StateDescriptor,
|
|
25
|
-
ActionDefinition,
|
|
26
|
-
WorkflowFieldDescriptor,
|
|
27
|
-
RoleDefinition,
|
|
28
|
-
} from './config/defineModel';
|
|
29
|
-
import { setField, logEvent, notify } from './actions';
|
|
30
|
-
|
|
31
|
-
// =============================================================================
|
|
32
|
-
// Helpers
|
|
33
|
-
// =============================================================================
|
|
34
|
-
|
|
35
|
-
/** Merge fields into a definition without overwriting existing ones. */
|
|
36
|
-
function mergeFields(
|
|
37
|
-
def: ModelDefinition,
|
|
38
|
-
fields: Record<string, WorkflowFieldDescriptor>,
|
|
39
|
-
): void {
|
|
40
|
-
for (const [name, desc] of Object.entries(fields)) {
|
|
41
|
-
if (!def.fields[name]) {
|
|
42
|
-
def.fields[name] = desc;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Merge states into a definition without overwriting existing ones. */
|
|
48
|
-
function mergeStates(
|
|
49
|
-
def: ModelDefinition,
|
|
50
|
-
states: Record<string, StateDescriptor>,
|
|
51
|
-
): void {
|
|
52
|
-
for (const [name, desc] of Object.entries(states)) {
|
|
53
|
-
if (!def.states[name]) {
|
|
54
|
-
def.states[name] = desc;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Merge transitions into a definition without overwriting existing ones. */
|
|
60
|
-
function mergeTransitions(
|
|
61
|
-
def: ModelDefinition,
|
|
62
|
-
transitions: Record<string, TransitionDescriptor>,
|
|
63
|
-
): void {
|
|
64
|
-
for (const [name, desc] of Object.entries(transitions)) {
|
|
65
|
-
if (!def.transitions[name]) {
|
|
66
|
-
def.transitions[name] = desc;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Merge roles into a definition without overwriting existing ones. */
|
|
72
|
-
function mergeRoles(
|
|
73
|
-
def: ModelDefinition,
|
|
74
|
-
roles: Record<string, RoleDefinition>,
|
|
75
|
-
): void {
|
|
76
|
-
if (!def.roles) def.roles = {};
|
|
77
|
-
for (const [name, desc] of Object.entries(roles)) {
|
|
78
|
-
if (!def.roles[name]) {
|
|
79
|
-
def.roles[name] = desc;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// =============================================================================
|
|
85
|
-
// Approval Pattern
|
|
86
|
-
// =============================================================================
|
|
87
|
-
|
|
88
|
-
export interface ApprovalConfig {
|
|
89
|
-
/** Role required to approve (default: 'approver'). */
|
|
90
|
-
approverRole?: string;
|
|
91
|
-
/** Field to track approval status (default: 'approvalStatus'). */
|
|
92
|
-
field?: string;
|
|
93
|
-
/** State to transition FROM for approval (default: 'pending_approval'). */
|
|
94
|
-
fromState?: string;
|
|
95
|
-
/** State to transition TO on approval (default: 'approved'). */
|
|
96
|
-
approvedState?: string;
|
|
97
|
-
/** State to transition TO on rejection (default: 'rejected'). */
|
|
98
|
-
rejectedState?: string;
|
|
99
|
-
/** Require a reason for rejection. */
|
|
100
|
-
requireRejectionReason?: boolean;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Approval pattern — adds approve/reject transitions with role guards.
|
|
105
|
-
*
|
|
106
|
-
* Adds: pending_approval state, approved state, rejected state,
|
|
107
|
-
* approve transition, reject transition, approvalStatus field.
|
|
108
|
-
*/
|
|
109
|
-
export function approval(config: ApprovalConfig = {}): (def: ModelDefinition) => ModelDefinition {
|
|
110
|
-
const {
|
|
111
|
-
approverRole = 'approver',
|
|
112
|
-
field: statusField = 'approvalStatus',
|
|
113
|
-
fromState = 'pending_approval',
|
|
114
|
-
approvedState = 'approved',
|
|
115
|
-
rejectedState = 'rejected',
|
|
116
|
-
requireRejectionReason = false,
|
|
117
|
-
} = config;
|
|
118
|
-
|
|
119
|
-
return (def) => {
|
|
120
|
-
mergeFields(def, {
|
|
121
|
-
[statusField]: { type: 'string', default: 'pending', enum: ['pending', 'approved', 'rejected'] },
|
|
122
|
-
approvedBy: { type: 'string', default: '' },
|
|
123
|
-
approvedAt: { type: 'datetime' },
|
|
124
|
-
rejectionReason: { type: 'string', default: '' },
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
mergeStates(def, {
|
|
128
|
-
[fromState]: {},
|
|
129
|
-
[approvedState]: {},
|
|
130
|
-
[rejectedState]: {},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
mergeRoles(def, {
|
|
134
|
-
[approverRole]: { description: `Can approve or reject`, permissions: ['approve', 'reject'] },
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const approveActions: ActionDefinition[] = [
|
|
138
|
-
setField(statusField, '"approved"'),
|
|
139
|
-
setField('approvedBy', 'context.actor_id'),
|
|
140
|
-
setField('approvedAt', 'NOW()'),
|
|
141
|
-
logEvent('approved'),
|
|
142
|
-
];
|
|
143
|
-
|
|
144
|
-
const rejectActions: ActionDefinition[] = [
|
|
145
|
-
setField(statusField, '"rejected"'),
|
|
146
|
-
setField('rejectionReason', 'COALESCE(input.reason, "")'),
|
|
147
|
-
logEvent('rejected', { reason: '{{ input.reason }}' }),
|
|
148
|
-
];
|
|
149
|
-
|
|
150
|
-
mergeTransitions(def, {
|
|
151
|
-
approve: {
|
|
152
|
-
from: fromState,
|
|
153
|
-
to: approvedState,
|
|
154
|
-
roles: [approverRole],
|
|
155
|
-
actions: approveActions,
|
|
156
|
-
description: 'Approve the request',
|
|
157
|
-
},
|
|
158
|
-
reject: {
|
|
159
|
-
from: fromState,
|
|
160
|
-
to: rejectedState,
|
|
161
|
-
roles: [approverRole],
|
|
162
|
-
actions: rejectActions,
|
|
163
|
-
...(requireRejectionReason ? { requiredFields: ['reason'] } : {}),
|
|
164
|
-
description: 'Reject the request',
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
return def;
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// =============================================================================
|
|
173
|
-
// Escalation Pattern
|
|
174
|
-
// =============================================================================
|
|
175
|
-
|
|
176
|
-
export interface EscalationConfig {
|
|
177
|
-
/** Duration before escalation (e.g., '24h', '2d'). */
|
|
178
|
-
timeout: string;
|
|
179
|
-
/** Role to notify on escalation (default: 'admin'). */
|
|
180
|
-
notifyRole?: string;
|
|
181
|
-
/** State to escalate FROM (default: 'pending_approval'). */
|
|
182
|
-
fromState?: string;
|
|
183
|
-
/** State to escalate TO (default: 'escalated'). */
|
|
184
|
-
escalatedState?: string;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Escalation pattern — adds timeout-based escalation with notification.
|
|
189
|
-
*
|
|
190
|
-
* Adds: escalated state, escalate transition (auto, timed),
|
|
191
|
-
* notification to the specified role.
|
|
192
|
-
*/
|
|
193
|
-
export function escalation(config: EscalationConfig): (def: ModelDefinition) => ModelDefinition {
|
|
194
|
-
const {
|
|
195
|
-
timeout,
|
|
196
|
-
notifyRole = 'admin',
|
|
197
|
-
fromState = 'pending_approval',
|
|
198
|
-
escalatedState = 'escalated',
|
|
199
|
-
} = config;
|
|
200
|
-
|
|
201
|
-
return (def) => {
|
|
202
|
-
mergeFields(def, {
|
|
203
|
-
escalatedAt: { type: 'datetime' },
|
|
204
|
-
escalationLevel: { type: 'number', default: 0 },
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Add timeout to the source state
|
|
208
|
-
if (def.states[fromState]) {
|
|
209
|
-
def.states[fromState].timeout = {
|
|
210
|
-
duration: timeout,
|
|
211
|
-
fallback: { transition: 'escalate' },
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
mergeStates(def, {
|
|
216
|
-
[escalatedState]: {
|
|
217
|
-
onEnter: [
|
|
218
|
-
setField('escalatedAt', 'NOW()'),
|
|
219
|
-
setField('escalationLevel', 'state_data.escalationLevel + 1'),
|
|
220
|
-
logEvent('escalated'),
|
|
221
|
-
notify('escalation', { to: `role:${notifyRole}` }),
|
|
222
|
-
],
|
|
223
|
-
},
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
mergeTransitions(def, {
|
|
227
|
-
escalate: {
|
|
228
|
-
from: fromState,
|
|
229
|
-
to: escalatedState,
|
|
230
|
-
auto: true,
|
|
231
|
-
description: `Auto-escalate after ${timeout}`,
|
|
232
|
-
actions: [logEvent('escalated', { timeout })],
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
return def;
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// =============================================================================
|
|
241
|
-
// Review Pattern
|
|
242
|
-
// =============================================================================
|
|
243
|
-
|
|
244
|
-
export interface ReviewConfig {
|
|
245
|
-
/** Role that submits for review (default: 'author'). */
|
|
246
|
-
authorRole?: string;
|
|
247
|
-
/** Role that reviews (default: 'reviewer'). */
|
|
248
|
-
reviewerRole?: string;
|
|
249
|
-
/** State the item starts in (default: 'draft'). */
|
|
250
|
-
draftState?: string;
|
|
251
|
-
/** State when under review (default: 'in_review'). */
|
|
252
|
-
reviewState?: string;
|
|
253
|
-
/** State when revisions requested (default: 'revisions_requested'). */
|
|
254
|
-
revisionsState?: string;
|
|
255
|
-
/** State when approved (default: 'approved'). */
|
|
256
|
-
approvedState?: string;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Review cycle pattern — submit → review → approve/request-revisions → resubmit.
|
|
261
|
-
*
|
|
262
|
-
* Adds: in_review state, revisions_requested state, approved state,
|
|
263
|
-
* submit, approve_review, request_revisions, resubmit transitions.
|
|
264
|
-
*/
|
|
265
|
-
export function review(config: ReviewConfig = {}): (def: ModelDefinition) => ModelDefinition {
|
|
266
|
-
const {
|
|
267
|
-
authorRole = 'author',
|
|
268
|
-
reviewerRole = 'reviewer',
|
|
269
|
-
draftState = 'draft',
|
|
270
|
-
reviewState = 'in_review',
|
|
271
|
-
revisionsState = 'revisions_requested',
|
|
272
|
-
approvedState = 'approved',
|
|
273
|
-
} = config;
|
|
274
|
-
|
|
275
|
-
return (def) => {
|
|
276
|
-
mergeFields(def, {
|
|
277
|
-
reviewNotes: { type: 'string', default: '' },
|
|
278
|
-
reviewedBy: { type: 'string', default: '' },
|
|
279
|
-
reviewedAt: { type: 'datetime' },
|
|
280
|
-
revisionCount: { type: 'number', default: 0 },
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
mergeStates(def, {
|
|
284
|
-
[reviewState]: {
|
|
285
|
-
onEnter: [logEvent('submitted_for_review')],
|
|
286
|
-
},
|
|
287
|
-
[revisionsState]: {
|
|
288
|
-
onEnter: [
|
|
289
|
-
setField('revisionCount', 'state_data.revisionCount + 1'),
|
|
290
|
-
logEvent('revisions_requested'),
|
|
291
|
-
],
|
|
292
|
-
},
|
|
293
|
-
[approvedState]: {
|
|
294
|
-
onEnter: [
|
|
295
|
-
setField('reviewedBy', 'context.actor_id'),
|
|
296
|
-
setField('reviewedAt', 'NOW()'),
|
|
297
|
-
logEvent('review_approved'),
|
|
298
|
-
],
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
mergeRoles(def, {
|
|
303
|
-
[authorRole]: { description: 'Can submit and revise', permissions: ['submit', 'revise'] },
|
|
304
|
-
[reviewerRole]: { description: 'Can approve or request revisions', permissions: ['approve', 'request_revisions'] },
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
mergeTransitions(def, {
|
|
308
|
-
submit_for_review: {
|
|
309
|
-
from: draftState,
|
|
310
|
-
to: reviewState,
|
|
311
|
-
roles: [authorRole],
|
|
312
|
-
description: 'Submit for review',
|
|
313
|
-
},
|
|
314
|
-
approve_review: {
|
|
315
|
-
from: reviewState,
|
|
316
|
-
to: approvedState,
|
|
317
|
-
roles: [reviewerRole],
|
|
318
|
-
description: 'Approve the submission',
|
|
319
|
-
},
|
|
320
|
-
request_revisions: {
|
|
321
|
-
from: reviewState,
|
|
322
|
-
to: revisionsState,
|
|
323
|
-
roles: [reviewerRole],
|
|
324
|
-
actions: [setField('reviewNotes', 'COALESCE(input.notes, "")')],
|
|
325
|
-
description: 'Request revisions',
|
|
326
|
-
},
|
|
327
|
-
resubmit: {
|
|
328
|
-
from: revisionsState,
|
|
329
|
-
to: reviewState,
|
|
330
|
-
roles: [authorRole],
|
|
331
|
-
description: 'Resubmit after revisions',
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
return def;
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// =============================================================================
|
|
340
|
-
// CRUD Pattern
|
|
341
|
-
// =============================================================================
|
|
342
|
-
|
|
343
|
-
export interface CrudConfig {
|
|
344
|
-
/** Use soft delete (archived state) instead of hard delete. Default: true. */
|
|
345
|
-
softDelete?: boolean;
|
|
346
|
-
/** Owner role name (default: 'owner'). */
|
|
347
|
-
ownerRole?: string;
|
|
348
|
-
/** Admin role name (default: 'admin'). */
|
|
349
|
-
adminRole?: string;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* CRUD lifecycle pattern — draft → active → archived/deleted.
|
|
354
|
-
*
|
|
355
|
-
* Adds: draft (initial), active, archived/deleted states,
|
|
356
|
-
* publish, update, archive/delete, restore transitions.
|
|
357
|
-
*/
|
|
358
|
-
export function crud(config: CrudConfig = {}): (def: ModelDefinition) => ModelDefinition {
|
|
359
|
-
const {
|
|
360
|
-
softDelete = true,
|
|
361
|
-
ownerRole = 'owner',
|
|
362
|
-
adminRole = 'admin',
|
|
363
|
-
} = config;
|
|
364
|
-
|
|
365
|
-
return (def) => {
|
|
366
|
-
mergeFields(def, {
|
|
367
|
-
createdBy: { type: 'string', default: '' },
|
|
368
|
-
createdAt: { type: 'datetime' },
|
|
369
|
-
updatedAt: { type: 'datetime' },
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
mergeStates(def, {
|
|
373
|
-
draft: { type: 'initial', onEnter: [setField('createdAt', 'NOW()'), setField('createdBy', 'context.actor_id')] },
|
|
374
|
-
active: { onEnter: [setField('updatedAt', 'NOW()')] },
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
mergeRoles(def, {
|
|
378
|
-
[ownerRole]: { description: 'Record owner', permissions: ['read', 'write', 'delete'] },
|
|
379
|
-
[adminRole]: { description: 'Administrator', permissions: ['read', 'write', 'delete', 'manage'] },
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
const transitions: Record<string, TransitionDescriptor> = {
|
|
383
|
-
publish: {
|
|
384
|
-
from: 'draft',
|
|
385
|
-
to: 'active',
|
|
386
|
-
description: 'Publish the draft',
|
|
387
|
-
actions: [logEvent('published')],
|
|
388
|
-
},
|
|
389
|
-
update: {
|
|
390
|
-
from: 'active',
|
|
391
|
-
to: 'active',
|
|
392
|
-
description: 'Update the record',
|
|
393
|
-
actions: [setField('updatedAt', 'NOW()'), logEvent('updated')],
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
if (softDelete) {
|
|
398
|
-
mergeStates(def, { archived: {} });
|
|
399
|
-
transitions.archive = {
|
|
400
|
-
from: 'active',
|
|
401
|
-
to: 'archived',
|
|
402
|
-
description: 'Archive the record',
|
|
403
|
-
actions: [logEvent('archived')],
|
|
404
|
-
};
|
|
405
|
-
transitions.restore = {
|
|
406
|
-
from: 'archived',
|
|
407
|
-
to: 'active',
|
|
408
|
-
description: 'Restore from archive',
|
|
409
|
-
actions: [logEvent('restored')],
|
|
410
|
-
};
|
|
411
|
-
} else {
|
|
412
|
-
mergeStates(def, { deleted: { type: 'end' } });
|
|
413
|
-
transitions.delete_record = {
|
|
414
|
-
from: ['draft', 'active'],
|
|
415
|
-
to: 'deleted',
|
|
416
|
-
roles: [ownerRole, adminRole],
|
|
417
|
-
description: 'Permanently delete',
|
|
418
|
-
actions: [logEvent('deleted')],
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
mergeTransitions(def, transitions);
|
|
423
|
-
return def;
|
|
424
|
-
};
|
|
425
|
-
}
|