Skip to main content

Flow Steps

  1. Client Interaction (Presentation Layer): User interaction occurs in a React component (page.tsx or optional container/) within an apps/* application. It prepares data matching the expected input shape (e.g., CreateUserInput).
  2. Call Server Action (API Entry Point): The component invokes an imported Server Action function (@core/actions). This function is marked with 'use server'. It receives the raw input from the client.
  3. Resolve Validated Service Instance (Action): The Server Action:
    • Determines the required service implementation type (e.g., based on context, tenant ID).
    • Calls the appropriate Service Factory function (e.g., getValidatedUserService(type) from @core/services/[domain]/userServiceFactory.ts). This factory internally uses the serviceMap to get the base implementation and then applies validation wrappers (like createServiceMethod) to its methods using the relevant argument schemas (@core/lib/schemas). The factory returns the validated service instance.
  4. Instantiate & Start Machine (Action): The Action:
    • Instantiates (interpret) the relevant XState machine (@core/machines), providing the resolved and validated service instance via the initial context.
    • Starts the machine interpreter.
    • Sends the initial event (e.g., { type: 'CREATE', input: rawClientInput }) to the machine, passing the original input received from the client. Note: Validation happens later when the service method is invoked.
    • Listens/awaits the machine reaching a final state.
  5. Orchestrate Logic (Machine): The XState machine transitions through its states based on events and its internal context. Its primary role is orchestration.
  6. Prepare Service Input & Invoke (Machine): Within specific states (often using invoke services or entry/exit actions), the machine:
    • Constructs the appropriate Service Input DTO (defined in services/[domain]/domain/[Domain]Types.ts) using data from its context (like the input received in the initial event).
    • Calls the appropriate method on the validated Service implementation (passed via context) using the prepared DTO (e.g., context.userService.createUser(serviceInputDto)). This call now triggers the validation wrapper.
  7. Validate & Execute (Service Wrapper -> Service Implementation):
    • The createServiceMethod wrapper function executes.
    • It validates the arguments (containing the Service Input DTO) against its pre-configured Yup schema.
    • If validation fails, the wrapper throws a yup.ValidationError.
    • If validation succeeds, the wrapper calls the original, unwrapped service implementation method.
    • The core service logic executes, potentially throwing its own execution errors.
  8. Process Service Output/Error & Update Context (Machine): When the invoked service call completes (or throws):
    • The machine’s invoke configuration handles the outcome.
    • onDone: If successful, the machine receives the Service Output DTO. It updates its internal context based on the DTO’s structure (e.g., extracting the User entity).
    • onError: If the invoke catches an error (either yup.ValidationError from the wrapper or an execution error from the implementation), the machine updates its context with error details (e.g., setting context.error and potentially context.validationErrors).
  9. Reach Final State (Machine): The machine eventually transitions to a terminal state (e.g., success, failure), indicated by type: 'final', potentially outputting its final context.
  10. Return Structured Result (Action → Client): The Server Action, having awaited the machine’s final state:
    • Inspects the machine’s final state and output/context.
    • Formats the outcome into a standard ActionResult object (e.g., { success: true, data: finalMachineOutput.user } or { success: false, error: finalMachineOutput.error, validationErrors: finalMachineOutput.validationErrors }).
    • Returns this ActionResult to the calling client component.
Clear Roles:
  • Client (apps/*): Renders UI, captures user input, calls Actions, displays results/errors from ActionResult.
  • Action (@core/actions): API boundary. Resolves correct validated service, interprets & starts Machine, awaits result, formats ActionResult based on machine outcome. Initial input validation is now deferred to the service wrapper.
  • Machine (@core/machines): Orchestrates flow (XState). Maps client input to Service DTOs, invokes validated Service methods, processes Service output DTOs or catches errors (including validation errors) from the invoke, manages internal state/context.
  • Service Factory (@core/services/[domain]/userServiceFactory.ts): Retrieves base implementation (via serviceMap), applies validation wrappers (createServiceMethod) to methods, returns the composed, validated service object.
  • Service Implementation (@core/services/[domain]/implementations): Executes discrete tasks, contains core logic. Defines contract (Interface + DTOs in domain/). Assumes input is valid by the time its methods are called.

Abbreviated Example: Create User Flow (Action → Machine → Service with Wrapped Validation)

This simplified example demonstrates the core flow: Action resolves the validated service and starts the machine, the machine prepares a DTO and calls the validated service method, the wrapper validates, the implementation performs the work, and the result/error flows back through the machine to the action. 1. Base Types (@core/types)
// packages/core/types/entities/User.ts
export interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}
// packages/core/types/common/ActionResult.ts
// Standardized Action Result Structure for the client
export interface ActionResult<TData = any, TError = string> {
	success: boolean;
	data?: TData; // Holds the final relevant data (e.g., the created User)
	error?: TError; // Holds general error message or structured error
	validationErrors?: { path: string; message: string }[]; // Specific validation errors
}
// packages/core/types/machines/user/UserCreationMachineTypes.ts
import type { User } from '../../entities/User';
// Use the type derived from the Yup schema for validated input
import type { CreateUserInput } from '../../../../validation/userSchema'; // Adjust path if needed
import type { UserService } from '../../../services/user/domain/UserService'; // Service Interface for context typing

// Machine Context: Stores input, potential result (User), error info, and service instance.
export interface UserCreationMachineContext {
	input?: CreateUserInput; // Holds the input received via the CREATE event
	user?: User; // Holds the final User entity after successful service call
	error?: string; // Holds error message from service failure or validation failure caught by invoke
	validationErrors?: { path: string; message: string }[]; // Specific validation errors caught by invoke
	userService?: UserService; // The resolved (and potentially validated) service instance
}

// Machine Events
export type UserCreationMachineEvent =
	| { type: 'CREATE'; input: CreateUserInput } // Event carries input
	| { type: 'RETRY' }; // Example event for retry logic
2. Client Input Validation Schema (@core/validation)
// packages/core/validation/userSchema.ts
// Defines the shape expected from the client / sent in the action payload.
import * as yup from 'yup';

export const userSchema = yup.object({
  name: yup.string().required("Name is required").min(2, 'Name is too short.'),
  email: yup.string().email("Invalid email format").required("Email is required"),
});

// Type derived from the validation schema
export type CreateUserInput = yup.InferType<typeof userSchema>;
3. Service Definition (@core/services/user/domain)
// packages/core/services/user/domain/UserTypes.ts
import type { User } from '../../../types/entities/User';

// --- Service Method DTOs ---
export interface CreateUserMethodInput {
  name: string;
  email: string;
}
export interface CreateUserMethodOutput {
  user: User;
}
// packages/core/services/user/domain/UserService.ts
import type { CreateUserMethodInput, CreateUserMethodOutput } from './UserTypes';

// --- Service Interface (Contract) ---
export interface UserService {
  createUser(data: CreateUserMethodInput): Promise<CreateUserMethodOutput>;
}
4. Service Implementation (@core/services/user/implementations)
// packages/core/services/user/implementations/DefaultUserService.ts
import type { UserService } from '../domain/UserService';
import type { CreateUserMethodInput, CreateUserMethodOutput } from '../domain/UserTypes';
import type { User } from '../../../types/entities/User';

// Concrete implementation - NO validation logic here.
export class DefaultUserService implements UserService {
  async createUser(data: CreateUserMethodInput): Promise<CreateUserMethodOutput> {
    console.log('IMPLEMENTATION (DefaultUserService): Processing DTO:', data);
    // Assumes 'data' is valid because the wrapper validated it
    const newUserEntity: User = {
      id: crypto.randomUUID(),
      name: data.name,
      email: data.email,
      createdAt: new Date(),
    };
    await new Promise((resolve) => setTimeout(resolve, 50)); // Simulate async work
    console.log('IMPLEMENTATION (DefaultUserService): Entity created:', newUserEntity);
    return { user: newUserEntity };
  }
}
5. Service Argument Validation Schema (@core/lib/schemas)
// packages/core/lib/schemas/user.schemas.ts
// Defines schemas for the *arguments* passed to service methods.
import * as yup from 'yup';
// Import the base validation schema to ensure consistency
import { userSchema } from '../../../validation/userSchema'; // Adjust path

// Schema for the arguments tuple of the createUser method.
// createUser expects one argument: an object matching CreateUserMethodInput (derived from userSchema).
export const CreateUserArgsSchema = yup.tuple([
    // Use the userSchema for the first (and only) argument.
    userSchema.required('User data is required.'),
]).defined();
6. Service Resolution (@core/services/user/serviceMap.ts & userServiceFactory.ts)
// packages/core/services/user/serviceMap.ts
import type { UserService } from './domain/UserService';
import { DefaultUserService } from './implementations/DefaultUserService';

// Holds UNWRAPPED instances
const serviceImplementations: Record<string, UserService> = {
  default: new DefaultUserService(),
  // clientA: new ClientAUserService(),
};

// Gets the BASE implementation
export function getBaseUserService(type: string = 'default'): UserService {
  const serviceKey = type.toLowerCase();
  const serviceInstance = serviceImplementations[serviceKey];
  if (!serviceInstance) {
    const fallbackInstance = serviceImplementations['default'];
    if (fallbackInstance) {
       console.warn(`UserService strategy type "${type}" not found, falling back to default.`);
       return fallbackInstance;
    }
    throw new Error(`UserService strategy type "${type}" not found and no default is available.`);
  }
  return serviceInstance;
}
// packages/core/services/user/userServiceFactory.ts
// *** Applies validation wrappers ***
import { getBaseUserService } from './serviceMap';
import type { UserService } from './domain/UserService';
// Adjust path to where your createServiceMethod utility resides
import { createServiceMethod } from '@/server-utils/utils/create-service-method';
// Import the *argument* schema for validation
import { CreateUserArgsSchema } from '../../lib/schemas/user.schemas';

// Factory that returns the VALIDATED service instance
export function getValidatedUserService(type: string = 'default'): UserService {
  // 1. Get the base implementation
  const baseServiceInstance = getBaseUserService(type);

  // 2. Wrap its methods with validation
  const validatedCreateUser = createServiceMethod(
    baseServiceInstance,
    'createUser',        // Method name from interface
    CreateUserArgsSchema // Argument validation schema
  );

  // 3. Return the composed service with wrapped methods
  return {
    createUser: validatedCreateUser,
    // wrap other methods...
  };
}
// packages/core/services/user/index.ts
// Barrel file exporting the factory function that provides the *validated* service
export { getValidatedUserService } from './userServiceFactory';
7. XState Machine (@core/machines)
// packages/core/machines/userCreationMachine.ts
import { createMachine, assign } from 'xstate';
import * as yup from 'yup';
import type {
    UserCreationMachineContext,
    UserCreationMachineEvent
} from '../types/machines/user/UserCreationMachineTypes'; // Adjust path
import type {
    CreateUserMethodInput,
    CreateUserMethodOutput
} from '../services/user/domain/UserTypes'; // Adjust path
import type { UserService } from '../services/user/domain/UserService'; // Adjust path

export const userCreationMachine = createMachine({
  id: 'userCreation',
  types: {} as { context: UserCreationMachineContext, events: UserCreationMachineEvent },
  initial: 'idle',
  context: {
    input: undefined,
    user: undefined,
    error: undefined,
    validationErrors: undefined, // For storing Yup errors
    userService: undefined, // Injected, expected to be the *validated* one
  },
  states: {
    idle: {
      on: {
        CREATE: {
          target: 'creating',
          guard: ({ context }) => !!context.userService,
          actions: assign({
            input: ({ event }) => event.input, // Store raw input
            error: undefined,
            validationErrors: undefined,
          }),
        },
      },
    },
    creating: {
      invoke: {
        id: 'invokeCreateUserService',
        src: async ({ context }) => { // Calls the potentially validated service
          if (!context.userService || !context.input) {
            throw new Error('Machine context prerequisites not met.');
          }
          const serviceInput: CreateUserMethodInput = { // Prepare DTO
            name: context.input.name,
            email: context.input.email,
          };
          console.log('MACHINE: Invoking context.userService.createUser with DTO:', serviceInput);
          // This call might throw yup.ValidationError or an execution error
          return await context.userService.createUser(serviceInput);
        },
        onDone: { // Service call (including validation) succeeded
          target: 'success',
          actions: assign({
            user: ({ event }) => (event.output as CreateUserMethodOutput).user,
          }),
        },
        onError: { // Handle errors caught from invoke
						target: 'failure',
						actions: assign({
							error: ({ event }) => {
								const error = event.data as unknown;
								// <<< Check for ServiceValidationError FIRST >>>
								if (error instanceof ServiceValidationError) {
									console.error(
										'MACHINE: Caught Service Validation Error:',
										error.validationErrors,
									);
									// Use the message from the custom error
									return error.message;
								}
								// Fallback for other execution errors
								if (error instanceof Error) {
									console.error(
										'MACHINE: Caught Service Execution Error:',
										error.message,
									);
									return error.message;
								}
								return 'User creation failed due to an unknown service error';
							},
							validationErrors: ({ event }) => {
								const error = event.data as unknown;
								// <<< Extract details from ServiceValidationError >>>
								if (error instanceof ServiceValidationError) {
                                    // Reconstruct format if needed, or use error.validationErrors directly if structure matches
									return error.validationErrors.map(message => ({ path: error.path ?? 'unknown', message }));
								}
								return undefined; // Clear if not a validation error
							},
						}),
					},
      },
    },
    success: {
      type: 'final',
      output: ({ context }) => ({ user: context.user }), // Output final data
    },
    failure: {
      type: 'final',
      output: ({ context }) => ({ error: context.error, validationErrors: context.validationErrors }), // Output error info
    },
  },
});
8. Server Action (@core/actions)
// packages/core/actions/userActions.ts
'use server';

// Import the factory that provides the *validated* service
import { getValidatedUserService } from '../services/user/userServiceFactory'; // Adjust path
import { userCreationMachine } from '../machines/userCreationMachine'; // Adjust path
import { interpret, Actor } from 'xstate';
// Import types
import type { CreateUserInput } from '../validation/userSchema'; // Adjust path
import type { ActionResult } from '../types/common/ActionResult'; // Adjust path
import type { User } from '../types/entities/User'; // Adjust path
import type { UserCreationMachineContext } from '../types/machines/user/UserCreationMachineTypes';

// Server Action orchestrates service resolution and machine execution
export async function createUserAction(
  input: CreateUserInput, // Action receives raw client input
  serviceType: string = 'default' // Example: Determine service type
): Promise<ActionResult<User>> { // Promises the standard result structure
  console.log(`ACTION: Received input for service type "${serviceType}":`, input);
  let machineActor: Actor<typeof userCreationMachine> | undefined = undefined;

  try {
    // 1. Resolve the VALIDATED Service Instance using the factory
    const validatedUserService = getValidatedUserService(serviceType);
    console.log(`ACTION: Resolved validated UserService instance for type "${serviceType}"`);

    // 2. Interpret and run the XState machine
    console.log('ACTION: Interpreting userCreationMachine...');
    return new Promise((resolve) => {
      machineActor = interpret(
        userCreationMachine.withContext({
            // Inject the resolved **validated** service instance
            userService: validatedUserService,
            // Initialize other context fields
            input: undefined,
            user: undefined,
            error: undefined,
            validationErrors: undefined,
        })
      )
        .onDone((event) => { // Machine reached a final state
            const finalOutput = event.output as Partial<UserCreationMachineContext>; // Get data from final state
            console.log('ACTION: Machine finished. Final Output:', finalOutput);
            if (finalOutput?.user) { // Success case
                resolve({ success: true, data: finalOutput.user });
            } else { // Failure case (includes validation errors caught by machine)
                resolve({
                    success: false,
                    error: finalOutput?.error ?? 'Machine failed without specific error',
                    validationErrors: finalOutput?.validationErrors // Pass captured validation errors
                });
            }
        })
        .onStop(() => console.log("ACTION: Machine actor stopped."))
        .start(); // Start the machine

      // 3. Send the initial event with the raw client input
      // Validation will occur when the machine invokes the service method
      console.log('ACTION: Sending CREATE event to machine with input:', input);
      machineActor.send({ type: 'CREATE', input: input });
    });

  } catch (error) {
    // Handle errors during service resolution or machine setup
    console.error(`ACTION: Unexpected error during setup for service type "${serviceType}":`, error);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'An unexpected server setup error occurred.',
    };
  } finally {
      // Optional: Ensure actor cleanup
      if (machineActor?.status === 1) machineActor.stop();
  }
}
9. UI Page (apps/[appName]/app/.../page.tsx)
// apps/radicacion/app/crear-usuario/page.tsx (Example - Simplified UI)
'use client';
import React, { useState, useTransition, FormEvent } from 'react';
// Use the action defined above
import { createUserAction } from '@core/actions/userActions'; // Adjust path
import type { CreateUserInput } from '@core/validation/userSchema'; // Adjust path
import type { ActionResult } from '@core/types/common/ActionResult'; // Adjust path
import type { User } from '@core/types/entities/User'; // Adjust path
// --- Import actual Design System components ---
// import { Button } from 'mappnext/ds-tw/atoms/Button';
// import { InputField } from 'mappnext/ds-tw/molecules/InputField';
// --- ---

// Mock Components for demo
const InputField = ({ label, name, value, onChange, error, ...props }: any) => (
	<div>
		<label htmlFor={name} className="block text-sm font-medium text-gray-700">{label}</label>
		<input id={name} name={name} value={value} onChange={onChange} {...props} className={`mt-1 block w-full px-3 py-2 border ${error ? 'border-red-500' : 'border-gray-300'} rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:opacity-50`} />
		{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
	</div>
);
const Button = ({ children, ...props }: any) => (
	<button {...props} className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50">
		{children}
	</button>
);
// End Mock Components

export default function CreateUserPage() {
  const [formData, setFormData] = useState<CreateUserInput>({ name: '', email: '' });
  const [isPending, startTransition] = useTransition();
  const [result, setResult] = useState<ActionResult<User> | null>(null);
  const [selectedServiceType] = useState('default'); // Example: could be dynamic

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
    if (result) setResult(null);
  };

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    setResult(null);
    startTransition(async () => {
      // Call the server action (pass serviceType if dynamic)
      const actionResult = await createUserAction(formData, selectedServiceType);
      setResult(actionResult); // Update state with the structured result
      if (actionResult.success) {
         setFormData({ name: '', email: '' }); // Reset form
         console.log("UI: User created!", actionResult.data);
         alert("User created successfully!");
      } else {
         console.error("UI: Creation failed:", actionResult.error || actionResult.validationErrors);
         // Errors (including validation errors) are now displayed via result state
         alert(`Failed: ${actionResult.error || 'Validation errors occurred.'}`);
      }
    });
  };

  // Helper to get validation error for a specific form field path
  // Now looks at the `validationErrors` array in the `ActionResult`
  const getValidationError = (path: string): string | undefined => {
    return result?.validationErrors?.find(err => err.path === path)?.message;
  };

  return (
    <div className="p-4 max-w-md mx-auto">
      <h1 className="text-2xl font-bold mb-4">Create User (Validated Service Example)</h1>
      {/* Service type display/selector could go here if dynamic */}
      <form onSubmit={handleSubmit} className="space-y-4">
        <InputField
          name="name"
          label="Name"
          value={formData.name}
          onChange={handleChange}
          error={getValidationError('name')} // Display specific validation error
          disabled={isPending}
          required
        />
        <InputField
          name="email"
          label="Email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          error={getValidationError('email')} // Display specific validation error
          disabled={isPending}
          required
        />

        {/* Display general server error (from machine/service execution failure) */}
        {result && !result.success && result.error && (
           <p className="text-sm text-red-600 mt-2">Error: {result.error}</p>
        )}

        {/* Display specific validation errors NOT tied to a field */}
        {result?.validationErrors?.filter(err => !['name', 'email'].includes(err.path ?? '')).map(err => (
             <p key={err.path ?? 'unknown'} className="text-sm text-red-600 mt-1">Validation ({err.path ?? 'general'}): {err.message}</p>
        ))}

        {/* Display success message */}
        {result?.success && result.data && (
           <p className="text-sm text-green-600 mt-2">User "{result.data.name}" created successfully!</p>
        )}

        <Button type="submit" disabled={isPending} className="w-full mt-4">
          {isPending ? 'Creating...' : 'Create User'}
        </Button>
      </form>
    </div>
  );
}