@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.
Files changed (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.mts +27 -2
  3. package/dist/index.d.ts +27 -2
  4. package/dist/index.js +70 -3
  5. package/dist/index.mjs +74 -12
  6. package/package.json +4 -3
  7. package/package.json.backup +0 -41
  8. package/src/Blueprint.ts +0 -216
  9. package/src/__tests__/Blueprint.test.ts +0 -106
  10. package/src/__tests__/action-context.test.ts +0 -166
  11. package/src/__tests__/actionCreators.test.ts +0 -179
  12. package/src/__tests__/builders.test.ts +0 -336
  13. package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
  14. package/src/__tests__/factories.test.ts +0 -229
  15. package/src/__tests__/loader.test.ts +0 -159
  16. package/src/__tests__/logger.test.ts +0 -70
  17. package/src/__tests__/type-inference.test.ts +0 -160
  18. package/src/__tests__/typed-transitions.test.ts +0 -126
  19. package/src/__tests__/useModuleConfig.test.ts +0 -61
  20. package/src/actionCreators.ts +0 -132
  21. package/src/actions.ts +0 -547
  22. package/src/atoms/index.ts +0 -600
  23. package/src/authoring.ts +0 -92
  24. package/src/browser-player.ts +0 -783
  25. package/src/builders.ts +0 -1342
  26. package/src/components/ExperienceWorkflowBridge.tsx +0 -123
  27. package/src/components/PlayerProvider.tsx +0 -43
  28. package/src/components/atoms/index.tsx +0 -269
  29. package/src/components/index.ts +0 -36
  30. package/src/conditions.ts +0 -692
  31. package/src/config/defineBlueprint.ts +0 -329
  32. package/src/config/defineModel.ts +0 -753
  33. package/src/config/defineWorkspace.ts +0 -24
  34. package/src/core/WorkflowRuntime.ts +0 -153
  35. package/src/factories.ts +0 -425
  36. package/src/grammar/index.ts +0 -173
  37. package/src/hooks/index.ts +0 -106
  38. package/src/hooks/useAuth.ts +0 -288
  39. package/src/hooks/useChannel.ts +0 -304
  40. package/src/hooks/useComputed.ts +0 -154
  41. package/src/hooks/useDomainSubscription.ts +0 -110
  42. package/src/hooks/useDuringAction.ts +0 -99
  43. package/src/hooks/useExperienceState.ts +0 -59
  44. package/src/hooks/useExpressionLibrary.ts +0 -129
  45. package/src/hooks/useForm.ts +0 -352
  46. package/src/hooks/useGeolocation.ts +0 -207
  47. package/src/hooks/useMapView.ts +0 -259
  48. package/src/hooks/useMiddleware.ts +0 -291
  49. package/src/hooks/useModel.ts +0 -363
  50. package/src/hooks/useModule.ts +0 -59
  51. package/src/hooks/useModuleConfig.ts +0 -61
  52. package/src/hooks/useMutation.ts +0 -237
  53. package/src/hooks/useNotification.ts +0 -151
  54. package/src/hooks/useOnChange.ts +0 -30
  55. package/src/hooks/useOnEnter.ts +0 -59
  56. package/src/hooks/useOnEvent.ts +0 -37
  57. package/src/hooks/useOnExit.ts +0 -27
  58. package/src/hooks/useOnTransition.ts +0 -30
  59. package/src/hooks/usePackage.ts +0 -128
  60. package/src/hooks/useParams.ts +0 -33
  61. package/src/hooks/usePlayer.ts +0 -308
  62. package/src/hooks/useQuery.ts +0 -184
  63. package/src/hooks/useRealtimeQuery.ts +0 -222
  64. package/src/hooks/useRole.ts +0 -191
  65. package/src/hooks/useRouteParams.ts +0 -100
  66. package/src/hooks/useRouter.ts +0 -347
  67. package/src/hooks/useServerAction.ts +0 -178
  68. package/src/hooks/useServerState.ts +0 -284
  69. package/src/hooks/useToast.ts +0 -164
  70. package/src/hooks/useTransition.ts +0 -39
  71. package/src/hooks/useView.ts +0 -102
  72. package/src/hooks/useWhileIn.ts +0 -48
  73. package/src/hooks/useWorkflow.ts +0 -63
  74. package/src/index.ts +0 -465
  75. package/src/loader/experience-workflow-loader.ts +0 -192
  76. package/src/loader/index.ts +0 -6
  77. package/src/local/LocalEngine.ts +0 -388
  78. package/src/local/LocalEngineAdapter.ts +0 -175
  79. package/src/local/LocalEngineContext.ts +0 -30
  80. package/src/logger.ts +0 -37
  81. package/src/mixins.ts +0 -1160
  82. package/src/providers/RuntimeContext.ts +0 -20
  83. package/src/providers/WorkflowProvider.tsx +0 -28
  84. package/src/routing/instance-key.ts +0 -107
  85. package/src/server/transition-context.ts +0 -172
  86. package/src/testing/index.ts +0 -9
  87. package/src/testing/useBlueprintTestRunner.ts +0 -91
  88. package/src/testing/useGraphAnalysis.ts +0 -18
  89. package/src/testing/useTestRunner.ts +0 -77
  90. package/src/testing.ts +0 -995
  91. package/src/types/workflow-inference.ts +0 -158
  92. package/src/types.ts +0 -114
  93. package/tsconfig.json +0 -27
  94. 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
- }