@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
package/src/builders.ts
DELETED
|
@@ -1,1342 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @mindmatrix/react/builders — Fluent builder API for ModelDefinition objects.
|
|
3
|
-
*
|
|
4
|
-
* Provides a chainable API for constructing workflow models, fields, states,
|
|
5
|
-
* and transitions without deeply nested object literals.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import { model, field, state, transition } from '@mindmatrix/react/builders';
|
|
10
|
-
*
|
|
11
|
-
* export default model('invoice')
|
|
12
|
-
* .version('1.0.0')
|
|
13
|
-
* .category('data')
|
|
14
|
-
* .field('amount', field.currency().required().min(0))
|
|
15
|
-
* .field('status', field.enum('draft', 'sent', 'paid').default('draft'))
|
|
16
|
-
* .state('draft', state.initial())
|
|
17
|
-
* .state('sent')
|
|
18
|
-
* .state('paid', state.end())
|
|
19
|
-
* .transition('send', transition.from('draft').to('sent').require('amount'))
|
|
20
|
-
* .transition('pay', transition.from('sent').to('paid').auto().when('state_data.paymentReceived == true'))
|
|
21
|
-
* .build();
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
defineModel,
|
|
27
|
-
type ModelDefinition,
|
|
28
|
-
type WorkflowFieldDescriptor,
|
|
29
|
-
type FieldValidation,
|
|
30
|
-
type ValidationRule,
|
|
31
|
-
type StateHome,
|
|
32
|
-
type ActionDefinition,
|
|
33
|
-
type EventSubscription,
|
|
34
|
-
type DuringAction,
|
|
35
|
-
type StateDescriptor,
|
|
36
|
-
type TransitionCondition,
|
|
37
|
-
type TransitionDescriptor,
|
|
38
|
-
type RoleDefinition,
|
|
39
|
-
} from './config/defineModel';
|
|
40
|
-
|
|
41
|
-
// =============================================================================
|
|
42
|
-
// ActionContext — typed context for inline action bodies
|
|
43
|
-
// =============================================================================
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Context object passed to inline action handler functions.
|
|
47
|
-
*
|
|
48
|
-
* Methods map to action compositions in the IR — the compiler translates
|
|
49
|
-
* the function body into an action pipeline. At SDK level, the function
|
|
50
|
-
* reference is stored as an `inline_handler` action definition.
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```typescript
|
|
54
|
-
* .onEnter(async (ctx) => {
|
|
55
|
-
* ctx.setField("activatedAt", new Date().toISOString());
|
|
56
|
-
* ctx.log("User activated");
|
|
57
|
-
* await ctx.notify("admin", "New user activated");
|
|
58
|
-
* })
|
|
59
|
-
* ```
|
|
60
|
-
*/
|
|
61
|
-
export interface ActionContext {
|
|
62
|
-
/** Set a single field value. */
|
|
63
|
-
setField(field: string, value: unknown): void;
|
|
64
|
-
/** Set multiple field values at once. */
|
|
65
|
-
setFields(fields: Record<string, unknown>): void;
|
|
66
|
-
/** Emit a log entry to the workflow audit trail. */
|
|
67
|
-
log(message: string, data?: Record<string, unknown>): void;
|
|
68
|
-
/** Send a notification to a recipient. */
|
|
69
|
-
notify(recipient: string, message: string, data?: Record<string, unknown>): Promise<void>;
|
|
70
|
-
/** Spawn a child workflow instance. */
|
|
71
|
-
spawn(slug: string, input?: Record<string, unknown>): Promise<void>;
|
|
72
|
-
/** Execute a named server-side action. */
|
|
73
|
-
serverAction(name: string, config?: Record<string, unknown>): Promise<unknown>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Handler function type for inline action bodies. */
|
|
77
|
-
export type ActionHandler = (ctx: ActionContext) => void | Promise<void>;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Convert an ActionHandler to an ActionDefinition for IR storage.
|
|
81
|
-
* The function body is stored as a string; the compiler handles actual compilation.
|
|
82
|
-
* @internal
|
|
83
|
-
*/
|
|
84
|
-
function handlerToAction(handler: ActionHandler, qualifier: string): ActionDefinition {
|
|
85
|
-
return {
|
|
86
|
-
id: `inline-${qualifier}-${Math.random().toString(36).slice(2, 8)}`,
|
|
87
|
-
type: 'inline_handler',
|
|
88
|
-
mode: 'auto',
|
|
89
|
-
config: {
|
|
90
|
-
handler: handler.toString(),
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// =============================================================================
|
|
96
|
-
// FieldBuilder
|
|
97
|
-
// =============================================================================
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Fluent builder for WorkflowFieldDescriptor objects.
|
|
101
|
-
*
|
|
102
|
-
* Use the `field.*` factory functions to create instances:
|
|
103
|
-
* ```typescript
|
|
104
|
-
* field.string('hello') // string field with default
|
|
105
|
-
* field.number() // number field
|
|
106
|
-
* field.enum('a', 'b', 'c') // enum field
|
|
107
|
-
* field.email().required() // email field, required
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
export class FieldBuilder<FT extends string = string> {
|
|
111
|
-
private _descriptor: WorkflowFieldDescriptor;
|
|
112
|
-
private _validation: FieldValidation = {};
|
|
113
|
-
private _rules: ValidationRule[] = [];
|
|
114
|
-
|
|
115
|
-
constructor(type: FT, defaultValue?: unknown) {
|
|
116
|
-
this._descriptor = { type };
|
|
117
|
-
if (defaultValue !== undefined) {
|
|
118
|
-
this._descriptor.default = defaultValue;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Mark the field as required. */
|
|
123
|
-
required(): this {
|
|
124
|
-
this._descriptor.required = true;
|
|
125
|
-
return this;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Set the default value. */
|
|
129
|
-
default(value: unknown): this {
|
|
130
|
-
this._descriptor.default = value;
|
|
131
|
-
return this;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Set the human-readable label. */
|
|
135
|
-
label(text: string): this {
|
|
136
|
-
this._descriptor.label = text;
|
|
137
|
-
return this;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Set the description / help text. */
|
|
141
|
-
description(text: string): this {
|
|
142
|
-
this._descriptor.description = text;
|
|
143
|
-
return this;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// --- Validation ---
|
|
147
|
-
|
|
148
|
-
/** Set the minimum numeric value. */
|
|
149
|
-
min(n: number): this {
|
|
150
|
-
this._validation.min = n;
|
|
151
|
-
return this;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Set the maximum numeric value. */
|
|
155
|
-
max(n: number): this {
|
|
156
|
-
this._validation.max = n;
|
|
157
|
-
return this;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Set the minimum string length. */
|
|
161
|
-
minLength(n: number): this {
|
|
162
|
-
this._validation.minLength = n;
|
|
163
|
-
return this;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Set the maximum string length. */
|
|
167
|
-
maxLength(n: number): this {
|
|
168
|
-
this._validation.maxLength = n;
|
|
169
|
-
return this;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Set a regex pattern the value must match. */
|
|
173
|
-
pattern(regex: string): this {
|
|
174
|
-
this._validation.pattern = regex;
|
|
175
|
-
return this;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Add a custom expression-based validation rule. */
|
|
179
|
-
validate(expression: string, message: string, severity?: 'error' | 'warning'): this {
|
|
180
|
-
this._rules.push({ expression, message, severity });
|
|
181
|
-
return this;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// --- ACL ---
|
|
185
|
-
|
|
186
|
-
/** Restrict field visibility to specific states. */
|
|
187
|
-
visibleIn(...states: string[]): this {
|
|
188
|
-
this._descriptor.visibleInStates = states;
|
|
189
|
-
return this;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Restrict field editability to specific states. */
|
|
193
|
-
editableIn(...states: string[]): this {
|
|
194
|
-
this._descriptor.editableInStates = states;
|
|
195
|
-
return this;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Restrict field visibility to specific roles. */
|
|
199
|
-
visibleTo(...roles: string[]): this {
|
|
200
|
-
this._descriptor.visibleToRoles = roles;
|
|
201
|
-
return this;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Restrict field editability to specific roles. */
|
|
205
|
-
editableBy(...roles: string[]): this {
|
|
206
|
-
this._descriptor.editableByRoles = roles;
|
|
207
|
-
return this;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Set an expression that controls visibility at runtime. */
|
|
211
|
-
visibleWhen(expression: string): this {
|
|
212
|
-
this._descriptor.visibleWhen = expression;
|
|
213
|
-
return this;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Set an expression that controls editability at runtime. */
|
|
217
|
-
editableWhen(expression: string): this {
|
|
218
|
-
this._descriptor.editableWhen = expression;
|
|
219
|
-
return this;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// --- Computed ---
|
|
223
|
-
|
|
224
|
-
/** Make this a computed field with the given expression and optional deps. */
|
|
225
|
-
computed(expression: string, deps?: string[]): this {
|
|
226
|
-
this._descriptor.computed = expression;
|
|
227
|
-
if (deps) {
|
|
228
|
-
this._descriptor.computedDeps = deps;
|
|
229
|
-
}
|
|
230
|
-
return this;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// --- State Home ---
|
|
234
|
-
|
|
235
|
-
/** Set the state home configuration (scope, persistence, sync). */
|
|
236
|
-
stateHome(config: StateHome): this {
|
|
237
|
-
this._descriptor.stateHome = config;
|
|
238
|
-
return this;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Mark the field as ephemeral (UI-only, not persisted). */
|
|
242
|
-
ephemeral(): this {
|
|
243
|
-
this._descriptor.stateHome = { scope: 'ephemeral', persistence: 'none' };
|
|
244
|
-
return this;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Build the WorkflowFieldDescriptor with its literal type preserved. */
|
|
248
|
-
build(): WorkflowFieldDescriptor & { type: FT } {
|
|
249
|
-
const desc = { ...this._descriptor };
|
|
250
|
-
// Merge validation if any was set
|
|
251
|
-
const hasValidation =
|
|
252
|
-
this._validation.min !== undefined ||
|
|
253
|
-
this._validation.max !== undefined ||
|
|
254
|
-
this._validation.minLength !== undefined ||
|
|
255
|
-
this._validation.maxLength !== undefined ||
|
|
256
|
-
this._validation.pattern !== undefined ||
|
|
257
|
-
this._rules.length > 0;
|
|
258
|
-
|
|
259
|
-
if (hasValidation) {
|
|
260
|
-
const validation: FieldValidation = { ...this._validation };
|
|
261
|
-
if (this._rules.length > 0) {
|
|
262
|
-
validation.rules = this._rules;
|
|
263
|
-
}
|
|
264
|
-
desc.validation = validation;
|
|
265
|
-
}
|
|
266
|
-
return desc as WorkflowFieldDescriptor & { type: FT };
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// --- FieldBuilder factory functions ---
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Field builder namespace with factory functions for common field types.
|
|
274
|
-
*
|
|
275
|
-
* @example
|
|
276
|
-
* ```typescript
|
|
277
|
-
* field.string('hello') // { type: 'string', default: 'hello' }
|
|
278
|
-
* field.number() // { type: 'number' }
|
|
279
|
-
* field.email().required() // { type: 'email', required: true }
|
|
280
|
-
* field.enum('a', 'b', 'c') // { type: 'string', enum: ['a', 'b', 'c'] }
|
|
281
|
-
* ```
|
|
282
|
-
*/
|
|
283
|
-
export const field = {
|
|
284
|
-
/** Create a string field. */
|
|
285
|
-
string(defaultValue?: string): FieldBuilder<'string'> {
|
|
286
|
-
return new FieldBuilder('string', defaultValue);
|
|
287
|
-
},
|
|
288
|
-
|
|
289
|
-
/** Create a number field. */
|
|
290
|
-
number(defaultValue?: number): FieldBuilder<'number'> {
|
|
291
|
-
return new FieldBuilder('number', defaultValue);
|
|
292
|
-
},
|
|
293
|
-
|
|
294
|
-
/** Create a boolean field. */
|
|
295
|
-
boolean(defaultValue?: boolean): FieldBuilder<'boolean'> {
|
|
296
|
-
return new FieldBuilder('boolean', defaultValue);
|
|
297
|
-
},
|
|
298
|
-
|
|
299
|
-
/** Create a currency field. */
|
|
300
|
-
currency(defaultValue?: number): FieldBuilder<'currency'> {
|
|
301
|
-
return new FieldBuilder('currency', defaultValue);
|
|
302
|
-
},
|
|
303
|
-
|
|
304
|
-
/** Create an email field. */
|
|
305
|
-
email(): FieldBuilder<'email'> {
|
|
306
|
-
return new FieldBuilder('email');
|
|
307
|
-
},
|
|
308
|
-
|
|
309
|
-
/** Create a URL field. */
|
|
310
|
-
url(): FieldBuilder<'url'> {
|
|
311
|
-
return new FieldBuilder('url');
|
|
312
|
-
},
|
|
313
|
-
|
|
314
|
-
/** Create a date field. */
|
|
315
|
-
date(): FieldBuilder<'date'> {
|
|
316
|
-
return new FieldBuilder('date');
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
/** Create a datetime field. */
|
|
320
|
-
datetime(): FieldBuilder<'datetime'> {
|
|
321
|
-
return new FieldBuilder('datetime');
|
|
322
|
-
},
|
|
323
|
-
|
|
324
|
-
/** Create an array field. */
|
|
325
|
-
array(): FieldBuilder<'array'> {
|
|
326
|
-
return new FieldBuilder('array');
|
|
327
|
-
},
|
|
328
|
-
|
|
329
|
-
/** Create an object field. */
|
|
330
|
-
object(defaultValue?: Record<string, unknown>): FieldBuilder<'object'> {
|
|
331
|
-
return new FieldBuilder('object', defaultValue);
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
/** Create an enum field with allowed values. */
|
|
335
|
-
enum(...values: string[]): FieldBuilder<'string'> {
|
|
336
|
-
const builder = new FieldBuilder('string');
|
|
337
|
-
(builder as unknown as { _descriptor: WorkflowFieldDescriptor })._descriptor.enum = values;
|
|
338
|
-
return builder;
|
|
339
|
-
},
|
|
340
|
-
|
|
341
|
-
/** Create a select field (alias for enum). */
|
|
342
|
-
select(...values: string[]): FieldBuilder<'string'> {
|
|
343
|
-
return field.enum(...values);
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// =============================================================================
|
|
348
|
-
// StateBuilder
|
|
349
|
-
// =============================================================================
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Fluent builder for StateDescriptor objects.
|
|
353
|
-
*
|
|
354
|
-
* Use the `state.*` factory functions for typed states:
|
|
355
|
-
* ```typescript
|
|
356
|
-
* state.initial() // { type: 'initial' }
|
|
357
|
-
* state.end().onEnter([...actions]) // { type: 'end', onEnter: [...] }
|
|
358
|
-
* new StateBuilder().description('...') // regular state
|
|
359
|
-
* ```
|
|
360
|
-
*/
|
|
361
|
-
export class StateBuilder {
|
|
362
|
-
private _descriptor: StateDescriptor = {};
|
|
363
|
-
/** @internal Inline transitions declared via .to() */
|
|
364
|
-
private _inlineTransitions: Array<{
|
|
365
|
-
target: string;
|
|
366
|
-
name: string;
|
|
367
|
-
configure?: (t: TransitionBuilder) => TransitionBuilder;
|
|
368
|
-
}> = [];
|
|
369
|
-
|
|
370
|
-
constructor(type?: 'initial' | 'end' | 'cancelled') {
|
|
371
|
-
if (type) {
|
|
372
|
-
this._descriptor.type = type;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Declare an outgoing transition inline with the state.
|
|
378
|
-
* This is syntactic sugar — the transition is registered on the model when `.build()` is called.
|
|
379
|
-
*
|
|
380
|
-
* @param targetState - The state to transition to
|
|
381
|
-
* @param transitionName - The name of the transition
|
|
382
|
-
* @param configure - Optional configuration callback for conditions, actions, roles, etc.
|
|
383
|
-
*
|
|
384
|
-
* @example
|
|
385
|
-
* ```typescript
|
|
386
|
-
* .state('draft', state.initial()
|
|
387
|
-
* .to('review', 'submit', t => t.require('amount'))
|
|
388
|
-
* .to('cancelled', 'cancel')
|
|
389
|
-
* )
|
|
390
|
-
* ```
|
|
391
|
-
*/
|
|
392
|
-
to(
|
|
393
|
-
targetState: string,
|
|
394
|
-
transitionName: string,
|
|
395
|
-
configure?: (t: TransitionBuilder) => TransitionBuilder,
|
|
396
|
-
): this {
|
|
397
|
-
this._inlineTransitions.push({
|
|
398
|
-
target: targetState,
|
|
399
|
-
name: transitionName,
|
|
400
|
-
configure,
|
|
401
|
-
});
|
|
402
|
-
return this;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* @internal Get inline transitions declared via .to(), resolved with the owning state name.
|
|
407
|
-
*/
|
|
408
|
-
getInlineTransitions(fromState: string): Record<string, TransitionDescriptor> {
|
|
409
|
-
const result: Record<string, TransitionDescriptor> = {};
|
|
410
|
-
for (const t of this._inlineTransitions) {
|
|
411
|
-
const builder = new TransitionBuilder().from(fromState).to(t.target);
|
|
412
|
-
if (t.configure) t.configure(builder);
|
|
413
|
-
result[t.name] = builder.build();
|
|
414
|
-
}
|
|
415
|
-
return result;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Set the description. */
|
|
419
|
-
description(text: string): this {
|
|
420
|
-
this._descriptor.description = text;
|
|
421
|
-
return this;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Set onEnter actions or an inline handler function.
|
|
426
|
-
*
|
|
427
|
-
* @example
|
|
428
|
-
* ```typescript
|
|
429
|
-
* // Action definitions
|
|
430
|
-
* state.initial().onEnter(setField('status', '"active"'), logEvent('activated'))
|
|
431
|
-
*
|
|
432
|
-
* // Inline handler
|
|
433
|
-
* state.initial().onEnter(async (ctx) => {
|
|
434
|
-
* ctx.setField('activatedAt', new Date().toISOString());
|
|
435
|
-
* ctx.log('User activated');
|
|
436
|
-
* })
|
|
437
|
-
* ```
|
|
438
|
-
*/
|
|
439
|
-
onEnter(handler: ActionHandler): this;
|
|
440
|
-
onEnter(...actions: ActionDefinition[]): this;
|
|
441
|
-
onEnter(...args: [ActionHandler] | ActionDefinition[]): this {
|
|
442
|
-
if (args.length === 1 && typeof args[0] === 'function') {
|
|
443
|
-
this._descriptor.onEnter = [handlerToAction(args[0] as ActionHandler, 'on-enter')];
|
|
444
|
-
} else {
|
|
445
|
-
this._descriptor.onEnter = args as ActionDefinition[];
|
|
446
|
-
}
|
|
447
|
-
return this;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Set onExit actions or an inline handler function.
|
|
452
|
-
*
|
|
453
|
-
* @example
|
|
454
|
-
* ```typescript
|
|
455
|
-
* .onExit(async (ctx) => {
|
|
456
|
-
* ctx.log('Leaving state');
|
|
457
|
-
* })
|
|
458
|
-
* ```
|
|
459
|
-
*/
|
|
460
|
-
onExit(handler: ActionHandler): this;
|
|
461
|
-
onExit(...actions: ActionDefinition[]): this;
|
|
462
|
-
onExit(...args: [ActionHandler] | ActionDefinition[]): this {
|
|
463
|
-
if (args.length === 1 && typeof args[0] === 'function') {
|
|
464
|
-
this._descriptor.onExit = [handlerToAction(args[0] as ActionHandler, 'on-exit')];
|
|
465
|
-
} else {
|
|
466
|
-
this._descriptor.onExit = args as ActionDefinition[];
|
|
467
|
-
}
|
|
468
|
-
return this;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/** Add an event subscription. */
|
|
472
|
-
onEvent(subscription: EventSubscription): this {
|
|
473
|
-
const existing = this._descriptor.onEvent
|
|
474
|
-
? [...this._descriptor.onEvent]
|
|
475
|
-
: [];
|
|
476
|
-
existing.push(subscription);
|
|
477
|
-
this._descriptor.onEvent = existing;
|
|
478
|
-
return this;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/** Add a during (scheduled) action. */
|
|
482
|
-
during(action: DuringAction): this {
|
|
483
|
-
const existing = this._descriptor.during
|
|
484
|
-
? [...this._descriptor.during]
|
|
485
|
-
: [];
|
|
486
|
-
existing.push(action);
|
|
487
|
-
this._descriptor.during = existing;
|
|
488
|
-
return this;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/** Set a timeout with fallback action or transition. */
|
|
492
|
-
timeout(duration: string, fallback: { action?: string; transition?: string }): this {
|
|
493
|
-
this._descriptor.timeout = { duration, fallback };
|
|
494
|
-
return this;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/** Build the StateDescriptor. */
|
|
498
|
-
build(): StateDescriptor {
|
|
499
|
-
return { ...this._descriptor };
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// --- StateBuilder factory functions ---
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* State builder namespace with factory functions for typed states.
|
|
507
|
-
*/
|
|
508
|
-
export const state = {
|
|
509
|
-
/** Create an initial state. */
|
|
510
|
-
initial(): StateBuilder {
|
|
511
|
-
return new StateBuilder('initial');
|
|
512
|
-
},
|
|
513
|
-
|
|
514
|
-
/** Create an end (terminal) state. */
|
|
515
|
-
end(): StateBuilder {
|
|
516
|
-
return new StateBuilder('end');
|
|
517
|
-
},
|
|
518
|
-
|
|
519
|
-
/** Create a cancelled (terminal) state. */
|
|
520
|
-
cancelled(): StateBuilder {
|
|
521
|
-
return new StateBuilder('cancelled');
|
|
522
|
-
},
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
// =============================================================================
|
|
526
|
-
// TransitionBuilder
|
|
527
|
-
// =============================================================================
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Fluent builder for TransitionDescriptor objects.
|
|
531
|
-
*
|
|
532
|
-
* @example
|
|
533
|
-
* ```typescript
|
|
534
|
-
* transition.from('draft').to('sent').require('amount', 'recipient')
|
|
535
|
-
* transition.from('sent', 'delivered').to('edited').when('context.actor_id == state_data.senderId')
|
|
536
|
-
* ```
|
|
537
|
-
*/
|
|
538
|
-
export class TransitionBuilder {
|
|
539
|
-
private _descriptor: Partial<TransitionDescriptor> = {};
|
|
540
|
-
|
|
541
|
-
/** Set source state(s). */
|
|
542
|
-
from(...states: string[]): this {
|
|
543
|
-
this._descriptor.from = states.length === 1 ? states[0] : states;
|
|
544
|
-
return this;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/** Set the target state. */
|
|
548
|
-
to(targetState: string): this {
|
|
549
|
-
this._descriptor.to = targetState;
|
|
550
|
-
return this;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/** Set the description. */
|
|
554
|
-
description(text: string): this {
|
|
555
|
-
this._descriptor.description = text;
|
|
556
|
-
return this;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Add guard conditions. Strings are treated as expression conditions.
|
|
561
|
-
*
|
|
562
|
-
* @example
|
|
563
|
-
* ```typescript
|
|
564
|
-
* .when('state_data.amount > 0')
|
|
565
|
-
* .when({ type: 'role', role: 'admin' })
|
|
566
|
-
* .when('input.email != null', { type: 'field', field: 'status', operator: 'eq', value: 'draft' })
|
|
567
|
-
* ```
|
|
568
|
-
*/
|
|
569
|
-
when(...conditions: (TransitionCondition | string)[]): this {
|
|
570
|
-
const existing = this._descriptor.conditions
|
|
571
|
-
? [...this._descriptor.conditions]
|
|
572
|
-
: [];
|
|
573
|
-
existing.push(...conditions);
|
|
574
|
-
this._descriptor.conditions = existing;
|
|
575
|
-
return this;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Add actions or an inline handler to execute on this transition.
|
|
580
|
-
*
|
|
581
|
-
* @example
|
|
582
|
-
* ```typescript
|
|
583
|
-
* // Action definitions
|
|
584
|
-
* .do(setField('status', '"approved"'), logEvent('approved'))
|
|
585
|
-
*
|
|
586
|
-
* // Inline handler
|
|
587
|
-
* .do(async (ctx) => {
|
|
588
|
-
* ctx.setField('approvedAt', new Date().toISOString());
|
|
589
|
-
* await ctx.notify('requester', 'Your request was approved');
|
|
590
|
-
* })
|
|
591
|
-
* ```
|
|
592
|
-
*/
|
|
593
|
-
do(handler: ActionHandler): this;
|
|
594
|
-
do(...actions: ActionDefinition[]): this;
|
|
595
|
-
do(...args: [ActionHandler] | ActionDefinition[]): this {
|
|
596
|
-
const existing = this._descriptor.actions
|
|
597
|
-
? [...this._descriptor.actions]
|
|
598
|
-
: [];
|
|
599
|
-
if (args.length === 1 && typeof args[0] === 'function') {
|
|
600
|
-
existing.push(handlerToAction(args[0] as ActionHandler, 'transition'));
|
|
601
|
-
} else {
|
|
602
|
-
existing.push(...(args as ActionDefinition[]));
|
|
603
|
-
}
|
|
604
|
-
this._descriptor.actions = existing;
|
|
605
|
-
return this;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/** Set the allowed roles. */
|
|
609
|
-
roles(...roles: string[]): this {
|
|
610
|
-
this._descriptor.roles = roles;
|
|
611
|
-
return this;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/** Set required fields that must have values before this transition fires. */
|
|
615
|
-
require(...fields: string[]): this {
|
|
616
|
-
this._descriptor.requiredFields = fields;
|
|
617
|
-
return this;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/** Mark this as an auto-transition (fires when conditions are met). */
|
|
621
|
-
auto(): this {
|
|
622
|
-
this._descriptor.auto = true;
|
|
623
|
-
return this;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/** Build the TransitionDescriptor. */
|
|
627
|
-
build(): TransitionDescriptor {
|
|
628
|
-
if (!this._descriptor.from) {
|
|
629
|
-
throw new Error('TransitionBuilder: from() is required');
|
|
630
|
-
}
|
|
631
|
-
if (!this._descriptor.to) {
|
|
632
|
-
throw new Error('TransitionBuilder: to() is required');
|
|
633
|
-
}
|
|
634
|
-
return this._descriptor as TransitionDescriptor;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// =============================================================================
|
|
639
|
-
// TypedTransitionBuilder — constrains from/to to known state names
|
|
640
|
-
// =============================================================================
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* A transition builder whose `.from()` and `.to()` methods only accept
|
|
644
|
-
* state names that have been declared on the model.
|
|
645
|
-
*
|
|
646
|
-
* Obtained via the callback overload of `ModelBuilder.transition()`:
|
|
647
|
-
* ```typescript
|
|
648
|
-
* model('invoice')
|
|
649
|
-
* .state('draft', state.initial())
|
|
650
|
-
* .state('sent')
|
|
651
|
-
* .state('paid', state.end())
|
|
652
|
-
* .transition('send', t => t.from('draft').to('sent').require('amount'))
|
|
653
|
-
* // ^ t is TypedTransitionBuilder<'draft' | 'sent' | 'paid'>
|
|
654
|
-
* .build();
|
|
655
|
-
* ```
|
|
656
|
-
*/
|
|
657
|
-
export class TypedTransitionBuilder<S extends string> {
|
|
658
|
-
/** @internal Delegate to an untyped builder for the actual work. */
|
|
659
|
-
private _inner = new TransitionBuilder();
|
|
660
|
-
|
|
661
|
-
/** Set source state(s) — constrained to declared state names. */
|
|
662
|
-
from(...states: S[]): this {
|
|
663
|
-
this._inner.from(...states);
|
|
664
|
-
return this;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/** Set the target state — constrained to declared state names. */
|
|
668
|
-
to(targetState: S): this {
|
|
669
|
-
this._inner.to(targetState);
|
|
670
|
-
return this;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/** Set the description. */
|
|
674
|
-
description(text: string): this {
|
|
675
|
-
this._inner.description(text);
|
|
676
|
-
return this;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/** Add guard conditions. */
|
|
680
|
-
when(...conditions: (TransitionCondition | string)[]): this {
|
|
681
|
-
this._inner.when(...conditions);
|
|
682
|
-
return this;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/** Add actions or an inline handler. */
|
|
686
|
-
do(handler: ActionHandler): this;
|
|
687
|
-
do(...actions: ActionDefinition[]): this;
|
|
688
|
-
do(...args: [ActionHandler] | ActionDefinition[]): this {
|
|
689
|
-
(this._inner.do as (...a: unknown[]) => TransitionBuilder)(...args);
|
|
690
|
-
return this;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/** Set allowed roles. */
|
|
694
|
-
roles(...roles: string[]): this {
|
|
695
|
-
this._inner.roles(...roles);
|
|
696
|
-
return this;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/** Set required fields. */
|
|
700
|
-
require(...fields: string[]): this {
|
|
701
|
-
this._inner.require(...fields);
|
|
702
|
-
return this;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/** Mark as auto-transition. */
|
|
706
|
-
auto(): this {
|
|
707
|
-
this._inner.auto();
|
|
708
|
-
return this;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/** Build the TransitionDescriptor. */
|
|
712
|
-
build(): TransitionDescriptor {
|
|
713
|
-
return this._inner.build();
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// --- TransitionBuilder factory function ---
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Transition builder namespace with the `from()` entry point.
|
|
721
|
-
*/
|
|
722
|
-
export const transition = {
|
|
723
|
-
/** Start building a transition from the given source state(s). */
|
|
724
|
-
from(...states: string[]): TransitionBuilder {
|
|
725
|
-
return new TransitionBuilder().from(...states);
|
|
726
|
-
},
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
// =============================================================================
|
|
730
|
-
// ModelBuilder
|
|
731
|
-
// =============================================================================
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Fluent builder for ModelDefinition objects.
|
|
735
|
-
*
|
|
736
|
-
* @example
|
|
737
|
-
* ```typescript
|
|
738
|
-
* const authModel = model('mod-authentication')
|
|
739
|
-
* .version('2.0.0')
|
|
740
|
-
* .category('module')
|
|
741
|
-
* .field('appName', field.string(''))
|
|
742
|
-
* .field('layout', field.enum('card', 'split', 'minimal').default('card'))
|
|
743
|
-
* .state('unauthenticated', state.initial().onEnter(clearError))
|
|
744
|
-
* .state('authenticated')
|
|
745
|
-
* .transition('login', transition.from('unauthenticated').to('authenticating'))
|
|
746
|
-
* .build();
|
|
747
|
-
* ```
|
|
748
|
-
*/
|
|
749
|
-
export class ModelBuilder<
|
|
750
|
-
F extends Record<string, { type: string }> = {},
|
|
751
|
-
S extends string = never,
|
|
752
|
-
T extends string = never,
|
|
753
|
-
> {
|
|
754
|
-
private _slug: string;
|
|
755
|
-
private _version?: string;
|
|
756
|
-
private _name?: string;
|
|
757
|
-
private _description?: string;
|
|
758
|
-
private _category?: string | readonly string[];
|
|
759
|
-
private _tags?: string[];
|
|
760
|
-
private _fields: Record<string, WorkflowFieldDescriptor> = {};
|
|
761
|
-
private _states: Record<string, StateDescriptor> = {};
|
|
762
|
-
private _transitions: Record<string, TransitionDescriptor> = {};
|
|
763
|
-
private _roles?: Record<string, RoleDefinition>;
|
|
764
|
-
private _metadata?: Record<string, unknown>;
|
|
765
|
-
private _onEvent?: EventSubscription[];
|
|
766
|
-
private _mixins: Array<(def: ModelDefinition) => ModelDefinition> = [];
|
|
767
|
-
|
|
768
|
-
constructor(slug: string) {
|
|
769
|
-
this._slug = slug;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/** Set the semantic version. */
|
|
773
|
-
version(v: string): ModelBuilder<F, S, T> {
|
|
774
|
-
this._version = v;
|
|
775
|
-
return this;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/** Set the display name. */
|
|
779
|
-
name(n: string): ModelBuilder<F, S, T> {
|
|
780
|
-
this._name = n;
|
|
781
|
-
return this;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
/** Set the description. */
|
|
785
|
-
description(d: string): ModelBuilder<F, S, T> {
|
|
786
|
-
this._description = d;
|
|
787
|
-
return this;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/** Set the category (string or array of strings). */
|
|
791
|
-
category(c: string | string[]): ModelBuilder<F, S, T> {
|
|
792
|
-
this._category = c;
|
|
793
|
-
return this;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/** Add tags. */
|
|
797
|
-
tags(...tagList: string[]): ModelBuilder<F, S, T> {
|
|
798
|
-
if (!this._tags) {
|
|
799
|
-
this._tags = [];
|
|
800
|
-
}
|
|
801
|
-
this._tags.push(...tagList);
|
|
802
|
-
return this;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* Add a field. Accepts a WorkflowFieldDescriptor object or a FieldBuilder.
|
|
807
|
-
* The field type is tracked for type inference with `useModel()`.
|
|
808
|
-
*
|
|
809
|
-
* @example
|
|
810
|
-
* ```typescript
|
|
811
|
-
* .field('email', { type: 'email', required: true })
|
|
812
|
-
* .field('name', field.string().required().maxLength(100))
|
|
813
|
-
* ```
|
|
814
|
-
*/
|
|
815
|
-
field<N extends string, FT extends string>(
|
|
816
|
-
name: N,
|
|
817
|
-
descriptor: (WorkflowFieldDescriptor & { type: FT }) | FieldBuilder<FT>,
|
|
818
|
-
): ModelBuilder<F & Record<N, { type: FT }>, S, T> {
|
|
819
|
-
this._fields[name] = descriptor instanceof FieldBuilder
|
|
820
|
-
? descriptor.build()
|
|
821
|
-
: descriptor;
|
|
822
|
-
return this as unknown as ModelBuilder<F & Record<N, { type: FT }>, S, T>;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
/**
|
|
826
|
-
* Add a state. Accepts an optional StateDescriptor or StateBuilder.
|
|
827
|
-
* If no descriptor is provided, creates an empty state.
|
|
828
|
-
*
|
|
829
|
-
* @example
|
|
830
|
-
* ```typescript
|
|
831
|
-
* .state('draft', state.initial())
|
|
832
|
-
* .state('active')
|
|
833
|
-
* .state('done', state.end())
|
|
834
|
-
* .state('error', { timeout: { duration: '5m', fallback: { transition: 'retry' } } })
|
|
835
|
-
* ```
|
|
836
|
-
*/
|
|
837
|
-
state<N extends string>(
|
|
838
|
-
name: N,
|
|
839
|
-
descriptor?: StateDescriptor | StateBuilder,
|
|
840
|
-
): ModelBuilder<F, S | N, T> {
|
|
841
|
-
if (descriptor === undefined) {
|
|
842
|
-
this._states[name] = {};
|
|
843
|
-
} else if (descriptor instanceof StateBuilder) {
|
|
844
|
-
this._states[name] = descriptor.build();
|
|
845
|
-
// Extract inline transitions declared via .to()
|
|
846
|
-
const inlineTransitions = descriptor.getInlineTransitions(name);
|
|
847
|
-
for (const [tName, tDesc] of Object.entries(inlineTransitions)) {
|
|
848
|
-
// Standalone transitions win over inline (more explicit)
|
|
849
|
-
if (!this._transitions[tName]) {
|
|
850
|
-
this._transitions[tName] = tDesc;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
} else {
|
|
854
|
-
this._states[name] = descriptor;
|
|
855
|
-
}
|
|
856
|
-
return this as unknown as ModelBuilder<F, S | N, T>;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* Add a transition. Accepts a TransitionDescriptor, TransitionBuilder, or
|
|
861
|
-
* a callback that receives a `TypedTransitionBuilder` constrained to known state names.
|
|
862
|
-
*
|
|
863
|
-
* The callback form provides type-safe `.from()` and `.to()` — only state names
|
|
864
|
-
* already declared via `.state()` are accepted.
|
|
865
|
-
*
|
|
866
|
-
* @example
|
|
867
|
-
* ```typescript
|
|
868
|
-
* // Untyped (existing API — any string accepted)
|
|
869
|
-
* .transition('send', transition.from('draft').to('sent').require('amount'))
|
|
870
|
-
* .transition('approve', { from: 'review', to: 'approved', roles: ['admin'] })
|
|
871
|
-
*
|
|
872
|
-
* // Type-safe callback (only declared states accepted)
|
|
873
|
-
* .state('draft', state.initial())
|
|
874
|
-
* .state('sent')
|
|
875
|
-
* .transition('send', t => t.from('draft').to('sent').require('amount'))
|
|
876
|
-
* // ^ autocomplete shows 'draft' | 'sent'
|
|
877
|
-
* ```
|
|
878
|
-
*/
|
|
879
|
-
transition<N extends string>(
|
|
880
|
-
name: N,
|
|
881
|
-
descriptor: TransitionDescriptor | TransitionBuilder | ((t: TypedTransitionBuilder<S>) => TypedTransitionBuilder<S>),
|
|
882
|
-
): ModelBuilder<F, S, T | N> {
|
|
883
|
-
if (typeof descriptor === 'function') {
|
|
884
|
-
const typed = new TypedTransitionBuilder<S>();
|
|
885
|
-
descriptor(typed);
|
|
886
|
-
this._transitions[name] = typed.build();
|
|
887
|
-
} else if (descriptor instanceof TransitionBuilder) {
|
|
888
|
-
this._transitions[name] = descriptor.build();
|
|
889
|
-
} else {
|
|
890
|
-
this._transitions[name] = descriptor;
|
|
891
|
-
}
|
|
892
|
-
return this as unknown as ModelBuilder<F, S, T | N>;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
/** Add a role definition. */
|
|
896
|
-
role(name: string, def: RoleDefinition): ModelBuilder<F, S, T> {
|
|
897
|
-
if (!this._roles) {
|
|
898
|
-
this._roles = {};
|
|
899
|
-
}
|
|
900
|
-
this._roles[name] = def;
|
|
901
|
-
return this;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
/** Add a model-level event subscription. */
|
|
905
|
-
onEvent(subscription: EventSubscription): ModelBuilder<F, S, T> {
|
|
906
|
-
if (!this._onEvent) {
|
|
907
|
-
this._onEvent = [];
|
|
908
|
-
}
|
|
909
|
-
this._onEvent.push(subscription);
|
|
910
|
-
return this;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
/** Set arbitrary metadata. */
|
|
914
|
-
meta(key: string, value: unknown): ModelBuilder<F, S, T> {
|
|
915
|
-
if (!this._metadata) {
|
|
916
|
-
this._metadata = {};
|
|
917
|
-
}
|
|
918
|
-
this._metadata[key] = value;
|
|
919
|
-
return this;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* Apply a mixin function that transforms the built ModelDefinition.
|
|
924
|
-
* Mixins are applied in order after all other properties are set.
|
|
925
|
-
*/
|
|
926
|
-
mixin(fn: (def: ModelDefinition) => ModelDefinition): ModelBuilder<F, S, T> {
|
|
927
|
-
this._mixins.push(fn);
|
|
928
|
-
return this;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
/**
|
|
932
|
-
* Declare a linear sequence of states (an "execution line").
|
|
933
|
-
*
|
|
934
|
-
* Every element is a state. Transitions between consecutive states are auto-generated
|
|
935
|
-
* with names like `draft_to_review`. Use tuples to configure states or the edge
|
|
936
|
-
* leading INTO that state.
|
|
937
|
-
*
|
|
938
|
-
* Multiple `.line()` calls combine to form the full state machine graph.
|
|
939
|
-
* States appearing in multiple lines are the same state.
|
|
940
|
-
*
|
|
941
|
-
* @example Simple linear flow:
|
|
942
|
-
* ```typescript
|
|
943
|
-
* model('order')
|
|
944
|
-
* .line('placed', 'confirmed', 'shipped', 'delivered')
|
|
945
|
-
* .line('placed', 'cancelled') // branch
|
|
946
|
-
* .build()
|
|
947
|
-
* // Transitions: placed_to_confirmed, confirmed_to_shipped, shipped_to_delivered, placed_to_cancelled
|
|
948
|
-
* ```
|
|
949
|
-
*
|
|
950
|
-
* @example With state and edge configuration:
|
|
951
|
-
* ```typescript
|
|
952
|
-
* .line(
|
|
953
|
-
* ['draft', state.initial()], // state with descriptor
|
|
954
|
-
* ['review', t => t.require('title')], // edge draft→review requires 'title'
|
|
955
|
-
* ['published', t => t.roles('editor')], // edge review→published requires 'editor' role
|
|
956
|
-
* )
|
|
957
|
-
* ```
|
|
958
|
-
*
|
|
959
|
-
* @example Named transitions (use tuple with string to override auto-name):
|
|
960
|
-
* ```typescript
|
|
961
|
-
* .line(
|
|
962
|
-
* 'draft',
|
|
963
|
-
* ['review', 'submit'], // name this edge 'submit' instead of 'draft_to_review'
|
|
964
|
-
* ['published', 'approve', t => t.roles('mgr')], // named + configured
|
|
965
|
-
* )
|
|
966
|
-
* ```
|
|
967
|
-
*/
|
|
968
|
-
line(
|
|
969
|
-
...elements: Array<
|
|
970
|
-
| string
|
|
971
|
-
| [string, StateBuilder]
|
|
972
|
-
| [string, StateDescriptor]
|
|
973
|
-
| [string, (t: TransitionBuilder) => TransitionBuilder]
|
|
974
|
-
| [string, string]
|
|
975
|
-
| [string, string, (t: TransitionBuilder) => TransitionBuilder]
|
|
976
|
-
>
|
|
977
|
-
): ModelBuilder<F, S, T> {
|
|
978
|
-
// Every element is a state (possibly with config).
|
|
979
|
-
// Tuples:
|
|
980
|
-
// [name, StateBuilder] → state with descriptor
|
|
981
|
-
// [name, fn] → state + configure INCOMING edge
|
|
982
|
-
// [name, string] → state + name for INCOMING edge
|
|
983
|
-
// [name, string, fn] → state + named + configured INCOMING edge
|
|
984
|
-
const nodes: Array<{
|
|
985
|
-
name: string;
|
|
986
|
-
stateConfig?: StateBuilder | StateDescriptor;
|
|
987
|
-
edgeConfig?: (t: TransitionBuilder) => TransitionBuilder;
|
|
988
|
-
edgeName?: string;
|
|
989
|
-
}> = [];
|
|
990
|
-
|
|
991
|
-
for (const el of elements) {
|
|
992
|
-
if (Array.isArray(el)) {
|
|
993
|
-
const [name, second, third] = el as [string, unknown, unknown];
|
|
994
|
-
if (second instanceof StateBuilder) {
|
|
995
|
-
nodes.push({ name, stateConfig: second });
|
|
996
|
-
} else if (typeof second === 'function') {
|
|
997
|
-
nodes.push({ name, edgeConfig: second as (t: TransitionBuilder) => TransitionBuilder });
|
|
998
|
-
} else if (typeof second === 'string') {
|
|
999
|
-
// [name, edgeName] or [name, edgeName, configFn]
|
|
1000
|
-
const configFn = typeof third === 'function'
|
|
1001
|
-
? third as (t: TransitionBuilder) => TransitionBuilder
|
|
1002
|
-
: undefined;
|
|
1003
|
-
nodes.push({ name, edgeName: second, edgeConfig: configFn });
|
|
1004
|
-
} else if (typeof second === 'object' && second !== null) {
|
|
1005
|
-
nodes.push({ name, stateConfig: second as StateDescriptor });
|
|
1006
|
-
} else {
|
|
1007
|
-
nodes.push({ name });
|
|
1008
|
-
}
|
|
1009
|
-
} else {
|
|
1010
|
-
nodes.push({ name: el });
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Register states
|
|
1015
|
-
for (const node of nodes) {
|
|
1016
|
-
if (!this._states[node.name]) {
|
|
1017
|
-
if (node.stateConfig instanceof StateBuilder) {
|
|
1018
|
-
this._states[node.name] = node.stateConfig.build();
|
|
1019
|
-
} else if (node.stateConfig) {
|
|
1020
|
-
this._states[node.name] = node.stateConfig;
|
|
1021
|
-
} else {
|
|
1022
|
-
this._states[node.name] = {};
|
|
1023
|
-
}
|
|
1024
|
-
} else if (node.stateConfig instanceof StateBuilder) {
|
|
1025
|
-
Object.assign(this._states[node.name], node.stateConfig.build());
|
|
1026
|
-
} else if (node.stateConfig) {
|
|
1027
|
-
Object.assign(this._states[node.name], node.stateConfig);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
// Register transitions (edges between consecutive states)
|
|
1032
|
-
for (let i = 0; i < nodes.length - 1; i++) {
|
|
1033
|
-
const from = nodes[i];
|
|
1034
|
-
const to = nodes[i + 1];
|
|
1035
|
-
const transitionName = to.edgeName || `${from.name}_to_${to.name}`;
|
|
1036
|
-
|
|
1037
|
-
const existing = this._transitions[transitionName];
|
|
1038
|
-
if (existing) {
|
|
1039
|
-
// Merge from states
|
|
1040
|
-
const existingFrom = Array.isArray(existing.from) ? [...existing.from] : [existing.from];
|
|
1041
|
-
if (!existingFrom.includes(from.name)) {
|
|
1042
|
-
existingFrom.push(from.name);
|
|
1043
|
-
existing.from = existingFrom.length === 1 ? existingFrom[0] : existingFrom;
|
|
1044
|
-
}
|
|
1045
|
-
} else {
|
|
1046
|
-
const builder = new TransitionBuilder().from(from.name).to(to.name);
|
|
1047
|
-
if (to.edgeConfig) {
|
|
1048
|
-
to.edgeConfig(builder);
|
|
1049
|
-
}
|
|
1050
|
-
this._transitions[transitionName] = builder.build();
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
return this;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/**
|
|
1058
|
-
* Build the final ModelDefinition with full type information preserved.
|
|
1059
|
-
* The returned type carries field types, state names, and transition names
|
|
1060
|
-
* so that `useModel()` and `useCollection()` can provide full IntelliSense.
|
|
1061
|
-
*/
|
|
1062
|
-
build(): ModelDefinition & {
|
|
1063
|
-
fields: F;
|
|
1064
|
-
states: Record<S, StateDescriptor>;
|
|
1065
|
-
transitions: Record<T, TransitionDescriptor>;
|
|
1066
|
-
} {
|
|
1067
|
-
let def: ModelDefinition = {
|
|
1068
|
-
slug: this._slug,
|
|
1069
|
-
fields: this._fields,
|
|
1070
|
-
states: this._states,
|
|
1071
|
-
transitions: this._transitions,
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
|
-
if (this._version !== undefined) def.version = this._version;
|
|
1075
|
-
if (this._name !== undefined) def.name = this._name;
|
|
1076
|
-
if (this._description !== undefined) def.description = this._description;
|
|
1077
|
-
if (this._category !== undefined) def.category = this._category;
|
|
1078
|
-
if (this._tags !== undefined) def.tags = this._tags;
|
|
1079
|
-
if (this._roles !== undefined) def.roles = this._roles;
|
|
1080
|
-
if (this._metadata !== undefined) def.metadata = this._metadata;
|
|
1081
|
-
if (this._onEvent !== undefined) def.onEvent = this._onEvent;
|
|
1082
|
-
|
|
1083
|
-
// Apply mixins
|
|
1084
|
-
for (const mixin of this._mixins) {
|
|
1085
|
-
def = mixin(def);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
return defineModel(def) as ModelDefinition & {
|
|
1089
|
-
fields: F;
|
|
1090
|
-
states: Record<S, StateDescriptor>;
|
|
1091
|
-
transitions: Record<T, TransitionDescriptor>;
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// --- ModelBuilder entry function ---
|
|
1097
|
-
|
|
1098
|
-
/**
|
|
1099
|
-
* Create a new ModelBuilder for the given slug.
|
|
1100
|
-
*
|
|
1101
|
-
* @example
|
|
1102
|
-
* ```typescript
|
|
1103
|
-
* import { model, field, state, transition } from '@mindmatrix/react/builders';
|
|
1104
|
-
*
|
|
1105
|
-
* export default model('my-model')
|
|
1106
|
-
* .version('1.0.0')
|
|
1107
|
-
* .field('name', field.string().required())
|
|
1108
|
-
* .state('active', state.initial())
|
|
1109
|
-
* .state('done', state.end())
|
|
1110
|
-
* .transition('finish', transition.from('active').to('done'))
|
|
1111
|
-
* .build();
|
|
1112
|
-
* ```
|
|
1113
|
-
*/
|
|
1114
|
-
export function model(slug: string): ModelBuilder<{}, never, never> {
|
|
1115
|
-
return new ModelBuilder(slug);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// =============================================================================
|
|
1119
|
-
// Factory Functions (Patterns)
|
|
1120
|
-
// =============================================================================
|
|
1121
|
-
|
|
1122
|
-
/**
|
|
1123
|
-
* Pipeline stage configuration for `createPipeline`.
|
|
1124
|
-
*/
|
|
1125
|
-
export interface PipelineStage {
|
|
1126
|
-
/** Stage name (becomes a state). */
|
|
1127
|
-
name: string;
|
|
1128
|
-
/** Actions to run when entering this stage. */
|
|
1129
|
-
onEnter?: ActionDefinition[];
|
|
1130
|
-
/** Actions to run when exiting this stage. */
|
|
1131
|
-
onExit?: ActionDefinition[];
|
|
1132
|
-
/** Auto-transition condition expression (advances to next stage when truthy). */
|
|
1133
|
-
advanceWhen?: string;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Configuration for the `createPipeline` factory.
|
|
1138
|
-
*/
|
|
1139
|
-
export interface PipelineConfig {
|
|
1140
|
-
/** Workflow slug. */
|
|
1141
|
-
slug: string;
|
|
1142
|
-
/** Semantic version. */
|
|
1143
|
-
version?: string;
|
|
1144
|
-
/** Field definitions. */
|
|
1145
|
-
fields: Record<string, WorkflowFieldDescriptor>;
|
|
1146
|
-
/** Ordered list of pipeline stages. */
|
|
1147
|
-
stages: PipelineStage[];
|
|
1148
|
-
/** Failure handling configuration. */
|
|
1149
|
-
onFailure?: {
|
|
1150
|
-
/** Actions to run on failure. */
|
|
1151
|
-
actions?: ActionDefinition[];
|
|
1152
|
-
/** Whether to allow retrying from the failed state. */
|
|
1153
|
-
allowRetry?: boolean;
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
/**
|
|
1158
|
-
* Create a linear pipeline model — a chain of auto-transitioning states.
|
|
1159
|
-
*
|
|
1160
|
-
* Each stage becomes a state, with auto-transitions between consecutive stages.
|
|
1161
|
-
* An optional `failed` state is added if `onFailure` is configured.
|
|
1162
|
-
*
|
|
1163
|
-
* @example
|
|
1164
|
-
* ```typescript
|
|
1165
|
-
* const pipeline = createPipeline({
|
|
1166
|
-
* slug: 'data-import',
|
|
1167
|
-
* fields: {
|
|
1168
|
-
* source: { type: 'string', required: true },
|
|
1169
|
-
* result: { type: 'object', default: null },
|
|
1170
|
-
* error: { type: 'string', default: '' },
|
|
1171
|
-
* },
|
|
1172
|
-
* stages: [
|
|
1173
|
-
* { name: 'validating', onEnter: [validateAction] },
|
|
1174
|
-
* { name: 'transforming', advanceWhen: 'state_data.validated == true' },
|
|
1175
|
-
* { name: 'loading', advanceWhen: 'state_data.loaded == true' },
|
|
1176
|
-
* { name: 'complete' },
|
|
1177
|
-
* ],
|
|
1178
|
-
* onFailure: { allowRetry: true },
|
|
1179
|
-
* });
|
|
1180
|
-
* ```
|
|
1181
|
-
*/
|
|
1182
|
-
export function createPipeline(config: PipelineConfig): ModelDefinition {
|
|
1183
|
-
const { slug, version, fields, stages, onFailure } = config;
|
|
1184
|
-
|
|
1185
|
-
if (stages.length < 2) {
|
|
1186
|
-
throw new Error(`createPipeline(${slug}): requires at least 2 stages`);
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
const states: Record<string, StateDescriptor> = {};
|
|
1190
|
-
const transitions: Record<string, TransitionDescriptor> = {};
|
|
1191
|
-
|
|
1192
|
-
// Build states
|
|
1193
|
-
for (let i = 0; i < stages.length; i++) {
|
|
1194
|
-
const stg = stages[i];
|
|
1195
|
-
const desc: StateDescriptor = {};
|
|
1196
|
-
|
|
1197
|
-
if (i === 0) desc.type = 'initial';
|
|
1198
|
-
if (i === stages.length - 1) desc.type = 'end';
|
|
1199
|
-
if (stg.onEnter) desc.onEnter = stg.onEnter;
|
|
1200
|
-
if (stg.onExit) desc.onExit = stg.onExit;
|
|
1201
|
-
|
|
1202
|
-
states[stg.name] = desc;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// Build auto-transitions between consecutive stages
|
|
1206
|
-
for (let i = 0; i < stages.length - 1; i++) {
|
|
1207
|
-
const from = stages[i];
|
|
1208
|
-
const to = stages[i + 1];
|
|
1209
|
-
const transName = `advance_${from.name}_to_${to.name}`;
|
|
1210
|
-
const trans: TransitionDescriptor = {
|
|
1211
|
-
from: from.name,
|
|
1212
|
-
to: to.name,
|
|
1213
|
-
auto: true,
|
|
1214
|
-
};
|
|
1215
|
-
if (from.advanceWhen) {
|
|
1216
|
-
trans.conditions = [{ type: 'expression', expression: from.advanceWhen }];
|
|
1217
|
-
}
|
|
1218
|
-
transitions[transName] = trans;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// Add failed state + transitions if configured
|
|
1222
|
-
if (onFailure) {
|
|
1223
|
-
const failedDesc: StateDescriptor = { type: 'end' };
|
|
1224
|
-
if (onFailure.actions) {
|
|
1225
|
-
failedDesc.onEnter = onFailure.actions;
|
|
1226
|
-
}
|
|
1227
|
-
states['failed'] = failedDesc;
|
|
1228
|
-
|
|
1229
|
-
// Every non-terminal stage can fail
|
|
1230
|
-
const failSources = stages
|
|
1231
|
-
.filter((_, i) => i < stages.length - 1)
|
|
1232
|
-
.map((s) => s.name);
|
|
1233
|
-
|
|
1234
|
-
transitions['fail'] = {
|
|
1235
|
-
from: failSources,
|
|
1236
|
-
to: 'failed',
|
|
1237
|
-
};
|
|
1238
|
-
|
|
1239
|
-
if (onFailure.allowRetry) {
|
|
1240
|
-
// Override failed to not be terminal so retry works
|
|
1241
|
-
states['failed'] = { ...failedDesc, type: undefined };
|
|
1242
|
-
transitions['retry'] = {
|
|
1243
|
-
from: 'failed',
|
|
1244
|
-
to: stages[0].name,
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
return defineModel({
|
|
1250
|
-
slug,
|
|
1251
|
-
version,
|
|
1252
|
-
fields,
|
|
1253
|
-
states,
|
|
1254
|
-
transitions,
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
/**
|
|
1259
|
-
* Configuration for the `createCRUD` factory.
|
|
1260
|
-
*/
|
|
1261
|
-
export interface CRUDConfig {
|
|
1262
|
-
/** Workflow slug. */
|
|
1263
|
-
slug: string;
|
|
1264
|
-
/** Semantic version. */
|
|
1265
|
-
version?: string;
|
|
1266
|
-
/** Field definitions. */
|
|
1267
|
-
fields: Record<string, WorkflowFieldDescriptor>;
|
|
1268
|
-
/** Use soft delete (archived state) instead of hard delete. Default: true. */
|
|
1269
|
-
softDelete?: boolean;
|
|
1270
|
-
/** Role definitions. */
|
|
1271
|
-
roles?: Record<string, RoleDefinition>;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
/**
|
|
1275
|
-
* Create a standard CRUD model with draft/active/archived lifecycle.
|
|
1276
|
-
*
|
|
1277
|
-
* States: `draft` (initial) -> `active` -> `archived` (if softDelete) or `deleted` (end).
|
|
1278
|
-
* Transitions: `publish`, `update` (self-loop), `archive`/`delete`, `restore` (from archived).
|
|
1279
|
-
*
|
|
1280
|
-
* @example
|
|
1281
|
-
* ```typescript
|
|
1282
|
-
* const product = createCRUD({
|
|
1283
|
-
* slug: 'product',
|
|
1284
|
-
* fields: {
|
|
1285
|
-
* name: { type: 'string', required: true },
|
|
1286
|
-
* price: { type: 'currency', required: true, validation: { min: 0 } },
|
|
1287
|
-
* sku: { type: 'string', required: true },
|
|
1288
|
-
* },
|
|
1289
|
-
* softDelete: true,
|
|
1290
|
-
* roles: {
|
|
1291
|
-
* admin: { permissions: ['manage'] },
|
|
1292
|
-
* editor: { permissions: ['read', 'write'] },
|
|
1293
|
-
* },
|
|
1294
|
-
* });
|
|
1295
|
-
* ```
|
|
1296
|
-
*/
|
|
1297
|
-
export function createCRUD(config: CRUDConfig): ModelDefinition {
|
|
1298
|
-
const { slug, version, fields, softDelete = true, roles } = config;
|
|
1299
|
-
|
|
1300
|
-
const states: Record<string, StateDescriptor> = {
|
|
1301
|
-
draft: { type: 'initial' },
|
|
1302
|
-
active: {},
|
|
1303
|
-
};
|
|
1304
|
-
|
|
1305
|
-
const transitions: Record<string, TransitionDescriptor> = {
|
|
1306
|
-
publish: {
|
|
1307
|
-
from: 'draft',
|
|
1308
|
-
to: 'active',
|
|
1309
|
-
},
|
|
1310
|
-
update: {
|
|
1311
|
-
from: 'active',
|
|
1312
|
-
to: 'active',
|
|
1313
|
-
},
|
|
1314
|
-
};
|
|
1315
|
-
|
|
1316
|
-
if (softDelete) {
|
|
1317
|
-
states['archived'] = {};
|
|
1318
|
-
transitions['archive'] = {
|
|
1319
|
-
from: 'active',
|
|
1320
|
-
to: 'archived',
|
|
1321
|
-
};
|
|
1322
|
-
transitions['restore'] = {
|
|
1323
|
-
from: 'archived',
|
|
1324
|
-
to: 'active',
|
|
1325
|
-
};
|
|
1326
|
-
} else {
|
|
1327
|
-
states['deleted'] = { type: 'end' };
|
|
1328
|
-
transitions['delete'] = {
|
|
1329
|
-
from: ['draft', 'active'],
|
|
1330
|
-
to: 'deleted',
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
return defineModel({
|
|
1335
|
-
slug,
|
|
1336
|
-
version,
|
|
1337
|
-
fields,
|
|
1338
|
-
states,
|
|
1339
|
-
transitions,
|
|
1340
|
-
roles,
|
|
1341
|
-
});
|
|
1342
|
-
}
|