import { LimeObjectContext, LimeTypeContext } from '../core/context'; import { ConfigMetadata } from '../core/metadata'; import { Expression } from '../query/query'; /** * Base marker interface for all commands in the command bus pattern. * * {@link AnyCommand} serves as the root interface for the command pattern implementation. * All commands must extend this interface (either directly or through {@link LimeObjectCommand} * or {@link LimeObjectBulkCommand}) to be handled by the {@link CommandBus}. * * Commands represent user actions or operations that can be executed, such as: * - Exporting data to Excel * - Sending emails * - Generating reports * - Creating or deleting records * - Custom business logic operations * * > **Important:** All properties of a command must be serializable and publicly assignable: * > * > **Serialization requirements:** * > - Use primitive types (string, number, boolean) * > - Use plain objects and arrays * > - Use serializable custom types (like {@link Expression}) * > - Avoid functions, DOM elements, class instances with methods, circular references, etc. * > * > **Assignability requirements:** * > - All properties must be public (not private or protected) * > - Properties cannot be readonly after instantiation * > - Constructor parameters are optional for command creation * > * > The platform creates commands by instantiating the class without constructor arguments, * > then directly assigning values to properties. Commands may also be serialized for storage, * > transmission, or recreation via {@link CommandBus.createCommand}. * * @example Declare a command with serializable properties * ```ts * import { AnyCommand, Command } from '@limetech/lime-web-components'; * * @Command({ id: 'generate-report' }) * class GenerateReportCommand implements AnyCommand { * public reportType: string; * * // Dates travel as ISO strings. A Date instance does not survive * // serialization, and the platform may serialize a command. * public startDate: string; * public endDate: string; * } * * const command = new GenerateReportCommand(); * command.reportType = 'sales'; * command.startDate = '2024-01-01'; * command.endDate = '2024-03-31'; * ``` * * @see {@link LimeObjectCommand} for commands operating on a single object * @see {@link LimeObjectBulkCommand} for commands operating on multiple objects * @see {@link CommandBus} for executing commands * * @public * @group Command bus */ export interface AnyCommand { } /** * A command that operates on a single {@link LimeObject}. * * {@link LimeObjectCommand} is used for actions that target a specific record, * such as sending an email to a contact, generating a PDF for a deal, or * archiving a specific company. The context property identifies which object * the command should operate on. * * @example Declare a single-object command * ```ts * import { * Command, * LimeObjectCommand, * LimeObjectContext, * } from '@limetech/lime-web-components'; * * // The platform instantiates a command without arguments and then assigns the * // properties, so a command class declares no constructor. * @Command({ id: 'send-email' }) * class SendEmailCommand implements LimeObjectCommand { * public context: LimeObjectContext; * public subject: string; * public body: string; * } * * const command = new SendEmailCommand(); * command.context = { limetype: 'person', id: 42 }; * command.subject = 'Meeting Invitation'; * command.body = 'Please join us for a meeting...'; * ``` * * @example Execute a single-object command * ```ts * import { * Command, * LimeObjectCommand, * LimeObjectContext, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'send-email' }) * class SendEmailCommand implements LimeObjectCommand { * public context: LimeObjectContext; * public subject: string; * public body: string; * } * * const commandBus = platform.get(PlatformServiceName.CommandBus); * * const command = new SendEmailCommand(); * command.context = { limetype: 'person', id: 42 }; * command.subject = 'Meeting Invitation'; * command.body = 'Please join us for a meeting...'; * * await commandBus.handle(command); * ``` * * @public * @group Command bus */ export interface LimeObjectCommand extends AnyCommand { /** * Context describing the {@link LimeObject} the command will operate on. * * Must include both `limetype` (the type of object) and `id` (the specific * object's identifier). This ensures the command knows exactly which record * to target. */ context: LimeObjectContext; } /** * A command that operates on multiple {@link LimeObject | LimeObjects} of a single {@link LimeType}. * * {@link LimeObjectBulkCommand} is used for batch operations on multiple records, * such as exporting selected deals to Excel, sending bulk emails, or mass-updating * status fields. The filter property determines which objects are included in the operation. * * > **Important:** Do not assume a specific filter structure in your command handler. * > The filter expression can vary depending on the user's selection: * > - **Specific selection**: `{ key: 'id', op: 'IN', exp: [1, 2, 3] }` (selected rows) * > - **Select all with criteria**: `{ key: 'status', op: '=', exp: 'won' }` (filtered view) * > - **Select all**: `null` (all objects in the limetype) * > * > Always pass the filter to your data queries rather than parsing its structure. * * @example Declare a bulk command * ```ts * import { * Command, * Expression, * LimeObjectBulkCommand, * LimeTypeContext, * } from '@limetech/lime-web-components'; * * @Command({ id: 'export-to-excel' }) * class ExportToExcelCommand implements LimeObjectBulkCommand { * public context: LimeTypeContext; * public filter: Expression | null; * public includeRelations = false; * } * * const command = new ExportToExcelCommand(); * command.context = { limetype: 'deal', id: null }; * command.filter = null; * ``` * * @example Execute a bulk command on selected items * ```ts * import { * Command, * Expression, * LimeObjectBulkCommand, * LimeTypeContext, * Operator, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'export-to-excel' }) * class ExportToExcelCommand implements LimeObjectBulkCommand { * public context: LimeTypeContext; * public filter: Expression | null; * public includeRelations = false; * } * * const commandBus = platform.get(PlatformServiceName.CommandBus); * * const selectedRows: Expression = { * key: 'id', * op: Operator.IN, * exp: [1, 2, 3, 4, 5], * }; * * const command = new ExportToExcelCommand(); * command.context = { limetype: 'deal', id: null }; * command.filter = selectedRows; * command.includeRelations = true; * * await commandBus.handle(command); * ``` * * @example Execute a bulk command on the objects of one parent * ```ts * import { * Command, * Expression, * LimeObjectBulkCommand, * LimeTypeContext, * Operator, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'update-deal-status' }) * class UpdateStatusCommand implements LimeObjectBulkCommand { * public context: LimeTypeContext; * public filter: Expression | null; * public newStatus: string; * } * * const commandBus = platform.get(PlatformServiceName.CommandBus); * * const command = new UpdateStatusCommand(); * * // The parent narrows the command to deals that belong to company 123. Together * // with the filter it selects the pending deals of that one company. * command.context = { * limetype: 'deal', * id: null, * parent: { limetype: 'company', id: 123 }, * }; * command.filter = { key: 'status', op: Operator.EQUALS, exp: 'pending' }; * command.newStatus = 'active'; * * await commandBus.handle(command); * ``` * * @public * @group Command bus */ export interface LimeObjectBulkCommand extends AnyCommand { /** * Context describing the {@link LimeType} the command will operate on. * * Must include `limetype` to identify which type of objects to operate on. * If `parent` is set on the context, it indicates that the {@link LimeObject | LimeObjects} * are all related to a common parent {@link LimeObject} via a `belongsto` relation. * * @example With and without a parent * ```ts * import { LimeTypeContext } from '@limetech/lime-web-components'; * * const allDeals: LimeTypeContext = { limetype: 'deal', id: null }; * * const dealsOfOneCompany: LimeTypeContext = { * limetype: 'deal', * id: null, * parent: { limetype: 'company', id: 456 }, * }; * ``` */ context: LimeTypeContext; /** * An {@link Expression} describing which {@link LimeObject | LimeObjects} to operate on. * * Can be `null` to operate on all objects (subject to parent context filtering), * or a filter expression to limit the operation to specific records. * * @example Selected ids, matching criteria, and all objects * ```ts * import { Expression, Operator } from '@limetech/lime-web-components'; * * const selectedIds: Expression = { * key: 'id', * op: Operator.IN, * exp: [1, 2, 3], * }; * * const largeActiveDeals: Expression = { * op: Operator.AND, * exp: [ * { key: 'status', op: Operator.EQUALS, exp: 'active' }, * { key: 'value', op: Operator.GREATER, exp: 10_000 }, * ], * }; * * // null means every object the context allows, with no further filtering. * const allObjects: Expression | null = null; * ``` */ filter: Expression | null; } /** * @public * @group Command bus */ export type CommandClass = new (...args: unknown[]) => T; /** * @public * @group Command bus */ export type CommandIdentifier = CommandClass | string; /** * Service for registering and executing commands using the command pattern. * * The {@link CommandBus} is the central hub for the command pattern implementation. * It manages command registration, handler lookup, and command execution. Commands * are decoupled from their handlers, allowing for flexible middleware chains and * cross-cutting concerns like logging, validation, and authorization. * * Key responsibilities: * - Register commands with their handlers * - Execute commands through their registered handlers * - Support middleware for command pipeline * - Dispatch command lifecycle events * - Provide command metadata and discovery * * @example Execute a command only when a handler is registered * ```ts * import { * Command, * LimeObjectCommand, * LimeObjectContext, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'send-email' }) * class SendEmailCommand implements LimeObjectCommand { * public context: LimeObjectContext; * public subject: string; * public body: string; * } * * const commandBus = platform.get(PlatformServiceName.CommandBus); * * // Another package may own the handler, so check before building the command. * if (commandBus.isSupported(SendEmailCommand)) { * const command = new SendEmailCommand(); * command.context = { limetype: 'person', id: 42 }; * command.subject = 'Meeting Invitation'; * command.body = 'Please join us for a meeting...'; * * await commandBus.handle(command); * } * ``` * * @example Register a handler, then execute the command * ```ts * import { * Command, * CommandHandler, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'generate-report' }) * class GenerateReportCommand { * public reportType: string; * } * * const handler: CommandHandler = { * handle: (command: GenerateReportCommand) => { * const http = platform.get(PlatformServiceName.Http); * * return http.post('reports', { type: command.reportType }); * }, * }; * * const commandBus = platform.get(PlatformServiceName.CommandBus); * * commandBus.register(GenerateReportCommand, handler, { * title: 'Generate Report', * description: 'Creates a PDF report', * icon: 'file-pdf', * }); * * const command = new GenerateReportCommand(); * command.reportType = 'monthly'; * * await commandBus.handle(command); * ``` * * @see {@link AnyCommand} for base command interface * @see {@link CommandHandler} for handler interface * @see {@link CommandMiddleware} for middleware implementation * * @public * @group Command bus */ export interface CommandBus extends CommandHandler { /** * Register a command to be executed by the given handler. * * Associates a command class with a handler that will process instances * of that command when {@link CommandBus.handle} is called. Optionally * attach {@link CommandMetadata} (title, description, icon, etc.) so the * command can be presented in command pickers and other UI surfaces. * * @param commandClass - The command class to register * @param handler - The {@link CommandHandler} instance that will execute the command * @param metadata - Optional presentation metadata for the command * * @example Register a command with an inline handler * ```ts * import { * Command, * LimeObjectCommand, * LimeObjectContext, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'send-email' }) * class SendEmailCommand implements LimeObjectCommand { * public context: LimeObjectContext; * public subject: string; * public body: string; * } * * const commandBus = platform.get(PlatformServiceName.CommandBus); * * commandBus.register(SendEmailCommand, { * handle: (command: SendEmailCommand) => { * const http = platform.get(PlatformServiceName.Http); * * return http.post('email', { * recipient: command.context.id, * subject: command.subject, * body: command.body, * }); * }, * }); * ``` * * @example Register a command with presentation metadata * ```ts * import { * Command, * CommandHandler, * LimeObjectCommand, * LimeObjectContext, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'send-email' }) * class SendEmailCommand implements LimeObjectCommand { * public context: LimeObjectContext; * public subject: string; * public body: string; * } * * const handler: CommandHandler = { * handle: (command: SendEmailCommand) => { * const http = platform.get(PlatformServiceName.Http); * * return http.post('email', { recipient: command.context.id }); * }, * }; * * const commandBus = platform.get(PlatformServiceName.CommandBus); * * // The metadata is what command pickers and other UI surfaces show. * commandBus.register(SendEmailCommand, handler, { * title: 'Send email', * description: 'Send an email to the contact', * icon: 'envelope', * }); * ``` */ register(commandClass: CommandClass, handler: CommandHandler, metadata?: Omit): void; /** * Execute the given command with it's registered command handler * * @param command - command to execute * * @returns result from the command handler */ handle(command: AnyCommand): unknown; /** * Check if a command is supported * * @param commandId - identifier of the command. Can be either the class or the string the class was registered with * * @returns true if the command is supported, false otherwise */ isSupported(commandId: CommandIdentifier): boolean; /** * Get a handler associated with a command * * @param commandClass - The command class * * @returns the handler for the command class */ getHandler(commandClass: CommandClass): CommandHandler; /** * Get a list of configs for all registered commands */ getAll(): CommandMetadata[]; /** * Create a command instance from a {@link CommandConfig} * * @param config - The command configuration * @throws Thrown if the command has not been registered yet */ createCommand(config: CommandConfig): CommandRegistry[Key]; createCommand(config: CommandConfig>): T; } /** * Service for executing commands * @public * @group Command bus */ export interface CommandHandler { /** * Handle the execution of the given command * * @param command - the command to handle * * @returns the result of the operation */ handle(command: AnyCommand): unknown; } /** * @public * @group Command bus */ export type CallableCommandMiddleware = (command: AnyCommand) => unknown; /** * Middleware for the command bus * @public * @group Command bus */ export interface CommandMiddleware { /** * Execute the middleware before passing the command further down the chain * * @param command - the command that is being handled * @param next - the next middleware in the chain * * @returns the result of the operation */ execute(command: AnyCommand, next: CallableCommandMiddleware): unknown; } /** * Events dispatched by the commandbus event middleware. * * The command bus dispatches events at key points in the command lifecycle, * allowing you to observe command execution, access results, handle errors, * or even prevent commands from executing. * * @example Observe results, errors, and block a command * ```ts * import { * CommandEvent, * CommandEventName, * PlatformServiceName, * createLogger, * getCommandId, * } from '@limetech/lime-web-components'; * * const logger = createLogger('my-component'); * const eventDispatcher = platform.get(PlatformServiceName.EventDispatcher); * * eventDispatcher.addListener(CommandEventName.Handled, (event: CommandEvent) => { * const { command, result } = event.detail; * * logger.debug('Command executed', { command, result }); * }); * * eventDispatcher.addListener(CommandEventName.Failed, (event: CommandEvent) => { * const { error } = event.detail; * * logger.error('Command failed', error instanceof Error ? error : undefined); * }); * * eventDispatcher.addListener( * CommandEventName.Received, * (event: CommandEvent) => { * // Preventing the default on the received event stops the command * // before a handler sees it. * if (getCommandId(event.detail.command) === 'delete-all-deals') { * event.preventDefault(); * } * } * ); * ``` * * @public * @group Command bus */ export declare enum CommandEventName { /** * Dispatched when the command has been received by the commandbus. * Calling `preventDefault()` on the event will stop the command from being handled * * @see {@link CommandEvent} */ Received = "command.received", /** * Dispatched when the command has been handled by the commandbus. * The event detail includes the result returned by the command handler. * * @see {@link CommandEvent} */ Handled = "command.handled", /** * Dispatched if an error occurs while handling the command. * The event detail includes the error that was thrown. * * @see {@link CommandEvent} */ Failed = "command.failed" } /** * @public * @group Command bus */ export type CommandEventDetail = { command: AnyCommand; result?: unknown; error?: unknown; }; /** * @public * @group Command bus */ export type CommandEvent = CustomEvent; /** * @public * @group Command bus */ export interface CommandOptions { /** * Id of the command */ id: string; } /** * Config for describing a command. * * Used with {@link CommandBus.createCommand} to create command instances from * configuration data. This is useful for storing command definitions or * transmitting them across boundaries. * * @public * @group Command bus */ export interface CommandConfig { /** * Id of the command. * * Specified by the {@link Command} decorator when declaring the command. */ id: TKey; /** * Optional parameters to set when creating the command from the config. * * **All values must be serializable** (primitive types, plain objects, arrays). * This matches the requirement that all command properties must be serializable * as defined in {@link AnyCommand}. */ params?: { [K in keyof TCommand]?: TCommand[K]; }; } /** * Registry for commands * * This interface is designed to be extended by consumers of Lime Web * Components using module augmentation. Each key in the interface corresponds * to the id of a registered command, and the value is the type of the command. * This allows for type-safe access to commands within the application. * * @example Add a command to the registry * ```ts * import { * Command, * LimeObjectCommand, * LimeObjectContext, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'send-welcome-email' }) * class SendWelcomeEmailCommand implements LimeObjectCommand { * public context: LimeObjectContext; * public template: string; * } * * declare module '@limetech/lime-web-components' { * interface CommandRegistry { * 'send-welcome-email': SendWelcomeEmailCommand; * } * } * * const commandBus = platform.get(PlatformServiceName.CommandBus); * * // The registry entry makes the id pick the type, so `command` is a * // SendWelcomeEmailCommand and `params` is checked against its properties. * const command = commandBus.createCommand({ * id: 'send-welcome-email', * params: { template: 'welcome' }, * }); * ``` * * @beta * @group Command bus */ export interface CommandRegistry { } /** * Metadata for a command * * @public * @group Command bus */ export type CommandMetadata = ConfigMetadata & { /** * Command id */ id: keyof CommandRegistry | (string & {}); }; /** * Decorator to register a class as a command with a unique identifier. * * The `@Command` decorator marks a class as a command and assigns it a unique ID * that can be used for command lookup, registration, and execution. This decorator * is essential for the command pattern implementation and enables type-safe command * creation through the {@link CommandRegistry}. * * @param options - A {@link CommandOptions} object containing the unique command ID * @returns Decorator function that registers the command class * * @example Basic command definition * ```ts * import { AnyCommand, Command } from '@limetech/lime-web-components'; * * @Command({ id: 'generate-report' }) * class GenerateReportCommand implements AnyCommand { * public reportType: string; * * // Dates travel as ISO strings. A Date instance does not survive * // serialization, and the platform may serialize a command. * public startDate: string; * public endDate: string; * } * * const command = new GenerateReportCommand(); * command.reportType = 'sales'; * command.startDate = '2024-01-01'; * command.endDate = '2024-03-31'; * ``` * * @example Single-object command with context * ```ts * import { * Command, * LimeObjectCommand, * LimeObjectContext, * } from '@limetech/lime-web-components'; * * // The platform instantiates a command without arguments and then assigns the * // properties, so a command class declares no constructor. * @Command({ id: 'send-email' }) * class SendEmailCommand implements LimeObjectCommand { * public context: LimeObjectContext; * public subject: string; * public body: string; * } * * const command = new SendEmailCommand(); * command.context = { limetype: 'person', id: 42 }; * command.subject = 'Meeting Invitation'; * command.body = 'Please join us for a meeting...'; * ``` * * @example Bulk command with filter * ```ts * import { * Command, * Expression, * LimeObjectBulkCommand, * LimeTypeContext, * } from '@limetech/lime-web-components'; * * @Command({ id: 'export-to-excel' }) * class ExportToExcelCommand implements LimeObjectBulkCommand { * public context: LimeTypeContext; * public filter: Expression | null; * public includeRelations = false; * } * * const command = new ExportToExcelCommand(); * command.context = { limetype: 'deal', id: null }; * command.filter = null; * ``` * * @example Register in the CommandRegistry for type safety * ```ts * import { * Command, * Expression, * LimeObjectBulkCommand, * LimeObjectCommand, * LimeObjectContext, * LimeTypeContext, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * @Command({ id: 'generate-pdf-report' }) * class GeneratePdfReportCommand { * public reportType: string; * public includeCharts: boolean; * } * * @Command({ id: 'archive-deal' }) * class ArchiveDealCommand implements LimeObjectCommand { * public context: LimeObjectContext; * public reason: string; * } * * @Command({ id: 'bulk-update-status' }) * class BulkUpdateStatusCommand implements LimeObjectBulkCommand { * public context: LimeTypeContext; * public filter: Expression | null; * public newStatus: string; * } * * declare module '@limetech/lime-web-components' { * interface CommandRegistry { * 'generate-pdf-report': GeneratePdfReportCommand; * 'archive-deal': ArchiveDealCommand; * 'bulk-update-status': BulkUpdateStatusCommand; * } * } * * const commandBus = platform.get(PlatformServiceName.CommandBus); * * const command = commandBus.createCommand({ * id: 'generate-pdf-report', * params: { reportType: 'monthly', includeCharts: true }, * }); * * await commandBus.handle(command); * ``` * * @public * @group Command bus */ export declare function Command(options: CommandOptions): (commandClass: CommandClass) => void; /** * Get the registered id of the command * * @param value - either a command or a command identifier * * @returns id of the command, or `null` if no id could be determined * @public * @group Command bus */ export declare function getCommandId(value: unknown): keyof CommandRegistry | (string & {}) | null; /** * Get all registered ids of a command and its parent classes * * @param value - either a command or a command identifier * * @returns ids of the command * @public * @group Command bus */ export declare function getCommandIds(value: unknown): Array;