@motiadev/core 0.1.0-beta.10 → 0.1.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -9,3 +9,4 @@ export { isApiStep, isCronStep, isEventStep, isNoopStep } from './src/guards';
9
9
  export { LockedData } from './src/locked-data';
10
10
  export { getStepConfig } from './src/get-step-config';
11
11
  export { StateAdapter } from './src/state/state-adapter';
12
+ export { createMermaidGenerator } from './src/mermaid-generator';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.getStepConfig = exports.LockedData = exports.isNoopStep = exports.isEventStep = exports.isCronStep = exports.isApiStep = exports.setupCronHandlers = exports.createStateAdapter = exports.Logger = exports.globalLogger = exports.createEventManager = exports.createStepHandlers = exports.createServer = void 0;
17
+ exports.createMermaidGenerator = exports.getStepConfig = exports.LockedData = exports.isNoopStep = exports.isEventStep = exports.isCronStep = exports.isApiStep = exports.setupCronHandlers = exports.createStateAdapter = exports.Logger = exports.globalLogger = exports.createEventManager = exports.createStepHandlers = exports.createServer = void 0;
18
18
  __exportStar(require("./src/types"), exports);
19
19
  var server_1 = require("./src/server");
20
20
  Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_1.createServer; } });
@@ -38,3 +38,5 @@ var locked_data_1 = require("./src/locked-data");
38
38
  Object.defineProperty(exports, "LockedData", { enumerable: true, get: function () { return locked_data_1.LockedData; } });
39
39
  var get_step_config_1 = require("./src/get-step-config");
40
40
  Object.defineProperty(exports, "getStepConfig", { enumerable: true, get: function () { return get_step_config_1.getStepConfig; } });
41
+ var mermaid_generator_1 = require("./src/mermaid-generator");
42
+ Object.defineProperty(exports, "createMermaidGenerator", { enumerable: true, get: function () { return mermaid_generator_1.createMermaidGenerator; } });
@@ -38,7 +38,7 @@ const getStepConfig = (file) => {
38
38
  });
39
39
  child.on('message', (message) => {
40
40
  logger_1.globalLogger.debug('[Config] Read config', { config: message });
41
- config = command === 'node' ? eval('(' + message + ')') : message;
41
+ config = message;
42
42
  resolve(config);
43
43
  child.kill(); // we can kill the child process since we already received the message
44
44
  });
@@ -0,0 +1,4 @@
1
+ import { LockedData } from './locked-data';
2
+ export declare const createMermaidGenerator: (baseDir: string) => {
3
+ initialize: (lockedData: LockedData) => void;
4
+ };
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createMermaidGenerator = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const guards_1 = require("./guards");
10
+ // Pure function to ensure diagrams directory exists
11
+ const ensureDiagramsDirectory = (diagramsDir) => {
12
+ if (!fs_1.default.existsSync(diagramsDir)) {
13
+ fs_1.default.mkdirSync(diagramsDir, { recursive: true });
14
+ }
15
+ };
16
+ // Pure function to get node ID
17
+ const getNodeId = (step, baseDir) => {
18
+ // Get relative path from the base directory
19
+ const relativePath = path_1.default.relative(baseDir, step.filePath);
20
+ // Remove common file extensions
21
+ const pathWithoutExtension = relativePath.replace(/\.(ts|js|tsx|jsx)$/, '');
22
+ // Replace slashes with underscores and dots with underscores
23
+ // Only keep alphanumeric characters and underscores
24
+ return pathWithoutExtension.replace(/[^a-zA-Z0-9]/g, '_');
25
+ };
26
+ // Pure function to get node label
27
+ const getNodeLabel = (step) => {
28
+ // Get display name for node
29
+ const displayName = step.config.name || path_1.default.basename(step.filePath, path_1.default.extname(step.filePath));
30
+ // Add node type prefix to help distinguish types
31
+ let prefix = '';
32
+ if ((0, guards_1.isApiStep)(step))
33
+ prefix = '🌐 ';
34
+ else if ((0, guards_1.isEventStep)(step))
35
+ prefix = '⚡ ';
36
+ else if ((0, guards_1.isCronStep)(step))
37
+ prefix = '⏰ ';
38
+ else if ((0, guards_1.isNoopStep)(step))
39
+ prefix = '⚙️ ';
40
+ // Create a node label with the step name
41
+ return `["${prefix}${displayName}"]`;
42
+ };
43
+ // Pure function to get node style
44
+ const getNodeStyle = (step) => {
45
+ // Apply style class based on step type
46
+ if ((0, guards_1.isApiStep)(step))
47
+ return ':::apiStyle';
48
+ if ((0, guards_1.isEventStep)(step))
49
+ return ':::eventStyle';
50
+ if ((0, guards_1.isCronStep)(step))
51
+ return ':::cronStyle';
52
+ if ((0, guards_1.isNoopStep)(step))
53
+ return ':::noopStyle';
54
+ return '';
55
+ };
56
+ // Pure function to generate connections
57
+ const generateConnections = (emits, sourceStep, steps, sourceId, baseDir) => {
58
+ const connections = [];
59
+ if (!emits || !Array.isArray(emits) || emits.length === 0) {
60
+ return '';
61
+ }
62
+ // Helper function to check if a step subscribes to a topic
63
+ const stepSubscribesToTopic = (step, topic) => {
64
+ // Event steps use regular subscribes
65
+ if ((0, guards_1.isEventStep)(step) &&
66
+ step.config.subscribes &&
67
+ Array.isArray(step.config.subscribes) &&
68
+ step.config.subscribes.includes(topic)) {
69
+ return true;
70
+ }
71
+ // Noop and API steps use virtualSubscribes
72
+ if (((0, guards_1.isNoopStep)(step) || (0, guards_1.isApiStep)(step)) &&
73
+ step.config.virtualSubscribes &&
74
+ Array.isArray(step.config.virtualSubscribes) &&
75
+ step.config.virtualSubscribes.includes(topic)) {
76
+ return true;
77
+ }
78
+ return false;
79
+ };
80
+ emits.forEach((emit) => {
81
+ const topic = typeof emit === 'string' ? emit : emit.topic;
82
+ const label = typeof emit === 'string' ? topic : emit.label || topic;
83
+ steps.forEach((targetStep) => {
84
+ if (stepSubscribesToTopic(targetStep, topic)) {
85
+ const targetId = getNodeId(targetStep, baseDir);
86
+ connections.push(` ${sourceId} -->|${label}| ${targetId}`);
87
+ }
88
+ });
89
+ });
90
+ return connections.join('\n');
91
+ };
92
+ // Pure function to generate flow diagram
93
+ const generateFlowDiagram = (flowName, steps, baseDir) => {
94
+ // Start mermaid flowchart with top-down direction
95
+ let diagram = `flowchart TD\n`;
96
+ // Add class definitions for styling with explicit text color
97
+ const classDefinitions = [
98
+ ` classDef apiStyle fill:#f96,stroke:#333,stroke-width:2px,color:#fff`,
99
+ ` classDef eventStyle fill:#69f,stroke:#333,stroke-width:2px,color:#fff`,
100
+ ` classDef cronStyle fill:#9c6,stroke:#333,stroke-width:2px,color:#fff`,
101
+ ` classDef noopStyle fill:#3f3a50,stroke:#333,stroke-width:2px,color:#fff`,
102
+ ];
103
+ diagram += classDefinitions.join('\n') + '\n';
104
+ // Check if we have any steps
105
+ if (!steps || steps.length === 0) {
106
+ return diagram + ' empty[No steps in this flow]';
107
+ }
108
+ // Create node definitions with proper format
109
+ steps.forEach((step) => {
110
+ const nodeId = getNodeId(step, baseDir);
111
+ const nodeLabel = getNodeLabel(step);
112
+ const nodeStyle = getNodeStyle(step);
113
+ diagram += ` ${nodeId}${nodeLabel}${nodeStyle}\n`;
114
+ });
115
+ // Create connections between nodes
116
+ let connectionsStr = '';
117
+ steps.forEach((sourceStep) => {
118
+ const sourceId = getNodeId(sourceStep, baseDir);
119
+ // Helper function to process emissions if they exist
120
+ function processEmissions(emissionsArray, stepSource, stepsCollection, sourceIdentifier) {
121
+ if (emissionsArray && Array.isArray(emissionsArray)) {
122
+ return generateConnections(emissionsArray, stepSource, stepsCollection, sourceIdentifier, baseDir);
123
+ }
124
+ return '';
125
+ }
126
+ // Semantic variables to clarify which step types support which emission types
127
+ const supportsEmits = (0, guards_1.isApiStep)(sourceStep) || (0, guards_1.isEventStep)(sourceStep) || (0, guards_1.isCronStep)(sourceStep);
128
+ const supportsVirtualEmits = supportsEmits || (0, guards_1.isNoopStep)(sourceStep);
129
+ // Process regular emissions if supported
130
+ if (supportsEmits) {
131
+ const emitConnections = processEmissions(sourceStep.config.emits, sourceStep, steps, sourceId);
132
+ if (emitConnections) {
133
+ connectionsStr += emitConnections + '\n';
134
+ }
135
+ }
136
+ // Process virtual emissions if supported
137
+ if (supportsVirtualEmits) {
138
+ const virtualEmitConnections = processEmissions(sourceStep.config.virtualEmits, sourceStep, steps, sourceId);
139
+ if (virtualEmitConnections) {
140
+ connectionsStr += virtualEmitConnections + '\n';
141
+ }
142
+ }
143
+ });
144
+ // Add connections to the diagram
145
+ diagram += connectionsStr;
146
+ return diagram;
147
+ };
148
+ // Function to save a diagram to a file
149
+ const saveDiagram = (diagramsDir, flowName, diagram) => {
150
+ const filePath = path_1.default.join(diagramsDir, `${flowName}.mmd`);
151
+ fs_1.default.writeFileSync(filePath, diagram);
152
+ };
153
+ // Function to remove a diagram file
154
+ const removeDiagram = (diagramsDir, flowName) => {
155
+ const filePath = path_1.default.join(diagramsDir, `${flowName}.mmd`);
156
+ if (fs_1.default.existsSync(filePath)) {
157
+ fs_1.default.unlinkSync(filePath);
158
+ }
159
+ };
160
+ // Function to generate and save a diagram
161
+ const generateAndSaveDiagram = (diagramsDir, flowName, flow, baseDir) => {
162
+ const diagram = generateFlowDiagram(flowName, flow.steps, baseDir);
163
+ saveDiagram(diagramsDir, flowName, diagram);
164
+ };
165
+ // Main exported function that creates the mermaid generator
166
+ const createMermaidGenerator = (baseDir) => {
167
+ const diagramsDir = path_1.default.join(baseDir, '.mermaid');
168
+ ensureDiagramsDirectory(diagramsDir);
169
+ // Event handlers
170
+ const handleFlowCreated = (flowName, flow) => {
171
+ generateAndSaveDiagram(diagramsDir, flowName, flow, baseDir);
172
+ };
173
+ const handleFlowUpdated = (flowName, flow) => {
174
+ generateAndSaveDiagram(diagramsDir, flowName, flow, baseDir);
175
+ };
176
+ const handleFlowRemoved = (flowName) => {
177
+ removeDiagram(diagramsDir, flowName);
178
+ };
179
+ // Initialize function to hook into LockedData events
180
+ const initialize = (lockedData) => {
181
+ // Hook into flow events
182
+ lockedData.on('flow-created', (flowName) => {
183
+ handleFlowCreated(flowName, lockedData.flows[flowName]);
184
+ });
185
+ lockedData.on('flow-updated', (flowName) => {
186
+ handleFlowUpdated(flowName, lockedData.flows[flowName]);
187
+ });
188
+ lockedData.on('flow-removed', (flowName) => {
189
+ handleFlowRemoved(flowName);
190
+ });
191
+ // Generate diagrams for all existing flows
192
+ if (lockedData.flows && typeof lockedData.flows === 'object') {
193
+ Object.entries(lockedData.flows).forEach(([flowName, flow]) => {
194
+ generateAndSaveDiagram(diagramsDir, flowName, flow, baseDir);
195
+ });
196
+ }
197
+ };
198
+ // Return the public API
199
+ return {
200
+ initialize,
201
+ };
202
+ };
203
+ exports.createMermaidGenerator = createMermaidGenerator;
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const path_1 = __importDefault(require("path"));
7
7
  const zod_1 = require("zod");
8
8
  const zod_to_json_schema_1 = __importDefault(require("zod-to-json-schema"));
9
- const serialize_javascript_1 = __importDefault(require("serialize-javascript"));
10
9
  // Add ts-node registration before dynamic imports
11
10
  // eslint-disable-next-line @typescript-eslint/no-require-imports
12
11
  require('ts-node').register({
@@ -27,7 +26,7 @@ async function getConfig(filePath) {
27
26
  else if (module.config.bodySchema instanceof zod_1.ZodObject) {
28
27
  module.config.bodySchema = (0, zod_to_json_schema_1.default)(module.config.bodySchema);
29
28
  }
30
- process.send?.((0, serialize_javascript_1.default)(module.config));
29
+ process.send?.(module.config);
31
30
  process.exit(0);
32
31
  }
33
32
  catch (error) {
@@ -0,0 +1 @@
1
+ export declare const composeMiddleware: (...middlewares: any[]) => (req: any, ctx: any, handler: () => Promise<any>) => Promise<any>;
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = (...middlewares) => {
3
+ exports.composeMiddleware = void 0;
4
+ /* eslint-disable @typescript-eslint/no-explicit-any */
5
+ const composeMiddleware = (...middlewares) => {
4
6
  return async (req, ctx, handler) => {
5
7
  const composedHandler = middlewares.reduceRight((nextHandler, middleware) => () => middleware(req, ctx, nextHandler), handler);
6
8
  return composedHandler();
7
9
  };
8
10
  };
11
+ exports.composeMiddleware = composeMiddleware;
12
+ /* eslint-enable @typescript-eslint/no-explicit-any */
@@ -7,6 +7,7 @@ const path_1 = __importDefault(require("path"));
7
7
  const logger_1 = require("./logger");
8
8
  const rpc_state_manager_1 = require("./rpc-state-manager");
9
9
  const rpc_1 = require("./rpc");
10
+ const middleware_compose_1 = require("./middleware-compose");
10
11
  // eslint-disable-next-line @typescript-eslint/no-require-imports
11
12
  require('dotenv').config();
12
13
  // Add ts-node registration before dynamic imports
@@ -38,8 +39,12 @@ async function runTypescriptModule(filePath, event) {
38
39
  const emit = async (data) => sender.send('emit', data);
39
40
  const context = { traceId, flows, logger, state, emit };
40
41
  sender.init();
41
- // Call the function with provided arguments
42
- const result = contextInFirstArg ? await module.handler(context) : await module.handler(event.data, context);
42
+ const middlewares = Array.isArray(module.config.middleware) ? module.config.middleware : [];
43
+ const composedMiddleware = (0, middleware_compose_1.composeMiddleware)(...middlewares);
44
+ const handlerFn = () => {
45
+ return contextInFirstArg ? module.handler(context) : module.handler(event.data, context);
46
+ };
47
+ const result = await composedMiddleware(event.data, context, handlerFn);
43
48
  await sender.send('result', result);
44
49
  await sender.close();
45
50
  process.exit(0);
@@ -8,6 +8,7 @@ from logger import Logger
8
8
  from rpc import RpcSender, serialize_for_json
9
9
  from rpc_state_manager import RpcStateManager
10
10
  from typing import Any, Optional
11
+ import functools
11
12
 
12
13
  def parse_args(arg: str) -> Any:
13
14
  from types import SimpleNamespace
@@ -19,16 +20,43 @@ def parse_args(arg: str) -> Any:
19
20
  return arg
20
21
 
21
22
  class Context:
22
- def __init__(self, args: Any, rpc: RpcSender):
23
+ def __init__(self, args: Any, rpc: RpcSender, is_api_handler: bool = False):
23
24
  self.trace_id = args.traceId
24
25
  self.traceId = args.traceId
25
26
  self.flows = args.flows
26
27
  self.rpc = rpc
27
28
  self.state = RpcStateManager(rpc)
28
29
  self.logger = Logger(self.trace_id, self.flows, rpc)
30
+ self._loop = asyncio.get_event_loop()
31
+ self.is_api_handler = is_api_handler
32
+ self._pending_tasks = []
29
33
 
30
34
  async def emit(self, event: Any):
31
- return await self.rpc.send('emit', event)
35
+ if self.is_api_handler:
36
+ self.rpc.send_no_wait('emit', event)
37
+ return None
38
+ else:
39
+ return await self.rpc.send('emit', event)
40
+
41
+ # Add wrapper to handle non-awaited emit coroutine
42
+ def __getattribute__(self, name):
43
+ attr = super().__getattribute__(name)
44
+ if name == 'emit' and asyncio.iscoroutinefunction(attr):
45
+ @functools.wraps(attr)
46
+ def wrapper(*args, **kwargs):
47
+ coro = attr(*args, **kwargs)
48
+ # Check if this is being awaited
49
+ frame = sys._getframe(1)
50
+ if frame.f_code.co_name != '__await__':
51
+ task = asyncio.create_task(coro)
52
+ def handle_exception(t):
53
+ if t.done() and not t.cancelled() and t.exception():
54
+ print(f"Unhandled exception in background task: {t.exception()}", file=sys.stderr)
55
+ task.add_done_callback(handle_exception)
56
+ return task
57
+ return coro
58
+ return wrapper
59
+ return attr
32
60
 
33
61
  async def run_python_module(file_path: str, rpc: RpcSender, args: Any) -> None:
34
62
  try:
@@ -62,6 +90,17 @@ async def run_python_module(file_path: str, rpc: RpcSender, args: Any) -> None:
62
90
 
63
91
  context = Context(args, rpc)
64
92
 
93
+ # Check if this is an API handler
94
+ is_api_handler = False
95
+ if hasattr(module, 'config'):
96
+ if isinstance(module.config, dict) and module.config.get('type') == 'api':
97
+ is_api_handler = True
98
+ elif hasattr(module.config, 'type') and module.config.type == 'api':
99
+ is_api_handler = True
100
+
101
+ # Create context with is_api_handler flag
102
+ context = Context(args, rpc, is_api_handler)
103
+
65
104
  if contextInFirstArg:
66
105
  result = await module.handler(context)
67
106
  else:
@@ -69,9 +108,22 @@ async def run_python_module(file_path: str, rpc: RpcSender, args: Any) -> None:
69
108
  args.data.body = serialize_for_json(args.data.body)
70
109
  result = await module.handler(args.data, context)
71
110
 
111
+ # For API handlers, we want to return immediately without waiting for background tasks
112
+ # This prevents the API from getting stuck
113
+ if not is_api_handler:
114
+ pending = asyncio.all_tasks() - {asyncio.current_task()}
115
+ if pending:
116
+ await asyncio.gather(*pending)
117
+
72
118
  if result:
73
119
  await rpc.send('result', result)
74
120
 
121
+ # For non-API handlers, ensure all pending tasks are completed before closing
122
+ if not is_api_handler:
123
+ pending = asyncio.all_tasks() - {asyncio.current_task()}
124
+ if pending:
125
+ await asyncio.gather(*pending)
126
+
75
127
  rpc.close()
76
128
  rpc.send_no_wait('close', None)
77
129
 
@@ -91,4 +143,4 @@ if __name__ == "__main__":
91
143
  rpc = RpcSender()
92
144
  loop = asyncio.get_event_loop()
93
145
  tasks = asyncio.gather(rpc.init(), run_python_module(file_path, rpc, parse_args(arg)))
94
- loop.run_until_complete(tasks)
146
+ loop.run_until_complete(tasks)
@@ -10,7 +10,15 @@ class RpcStateManager:
10
10
  self._loop = asyncio.get_event_loop()
11
11
 
12
12
  async def get(self, trace_id: str, key: str) -> asyncio.Future[Any]:
13
- return await self.rpc.send('state.get', {'traceId': trace_id, 'key': key})
13
+ result = await self.rpc.send('state.get', {'traceId': trace_id, 'key': key})
14
+
15
+ if result is None:
16
+ return {'data': None}
17
+ elif isinstance(result, dict):
18
+ if 'data' not in result:
19
+ return {'data': result}
20
+
21
+ return result
14
22
 
15
23
  async def set(self, trace_id: str, key: str, value: Any) -> asyncio.Future[None]:
16
24
  future = await self.rpc.send('state.set', {'traceId': trace_id, 'key': key, 'value': value})
@@ -33,9 +41,14 @@ class RpcStateManager:
33
41
  frame = sys._getframe(1)
34
42
  if frame.f_code.co_name != '__await__':
35
43
  # Not being awaited, schedule in background
44
+ # But we need to make sure this task completes before the handler returns
45
+ # So we'll return the task for the caller to await if needed
36
46
  task = asyncio.create_task(coro)
37
- # Optional: Add error handling for the background task
38
- task.add_done_callback(lambda t: t.exception() if t.done() and not t.cancelled() else None)
47
+ # Add error handling for the background task
48
+ def handle_exception(t):
49
+ if t.done() and not t.cancelled() and t.exception():
50
+ print(f"Unhandled exception in background task: {t.exception()}", file=sys.stderr)
51
+ task.add_done_callback(handle_exception)
39
52
  return task
40
53
  # Being awaited, return coroutine as normal
41
54
  return coro
@@ -3,6 +3,22 @@ class RpcStateManager
3
3
  @sender = sender
4
4
  end
5
5
 
6
+ # Support hash-like access with [:method_name] syntax
7
+ def [](method_name)
8
+ case method_name.to_sym
9
+ when :get
10
+ ->(trace_id, key) { get(trace_id, key) }
11
+ when :set
12
+ ->(trace_id, key, value) { set(trace_id, key, value) }
13
+ when :delete
14
+ ->(trace_id, key) { delete(trace_id, key) }
15
+ when :clear
16
+ ->(trace_id) { clear(trace_id) }
17
+ else
18
+ raise NoMethodError, "undefined method `#{method_name}' for #{self.class}"
19
+ end
20
+ end
21
+
6
22
  def get(trace_id, key)
7
23
  # Return promise to match Python/Node behavior
8
24
  @sender.send('state.get', { traceId: trace_id, key: key })
@@ -22,4 +38,4 @@ class RpcStateManager
22
38
  # Return promise to match Python/Node behavior
23
39
  @sender.send('state.clear', { traceId: trace_id })
24
40
  end
25
- end
41
+ end
@@ -20,7 +20,7 @@ class Context
20
20
 
21
21
  def emit(event)
22
22
  # Add type field if not present to match Node.js/Python behavior
23
- event = { type: event[:type] || event['type'], data: event[:data] || event['data'] } unless event.is_a?(String)
23
+ event = { topic: event[:topic] || event['topic'], data: event[:data] || event['data'] } unless event.is_a?(String)
24
24
  promise = @rpc.send('emit', event)
25
25
  promise # Return promise to maintain async pattern
26
26
  end
@@ -8,6 +8,7 @@ const cron_handler_1 = require("./cron-handler");
8
8
  const body_parser_1 = __importDefault(require("body-parser"));
9
9
  const express_1 = __importDefault(require("express"));
10
10
  const http_1 = __importDefault(require("http"));
11
+ const multer_1 = __importDefault(require("multer"));
11
12
  const socket_io_1 = require("socket.io");
12
13
  const flows_endpoint_1 = require("./flows-endpoint");
13
14
  const guards_1 = require("./guards");
@@ -17,13 +18,13 @@ const call_step_file_1 = require("./call-step-file");
17
18
  const LoggerFactory_1 = require("./LoggerFactory");
18
19
  const generate_trace_id_1 = require("./generate-trace-id");
19
20
  const flows_config_endpoint_1 = require("./flows-config-endpoint");
20
- const middleware_composer_1 = __importDefault(require("./middleware-composer"));
21
21
  const createServer = async (lockedData, eventManager, state, config) => {
22
22
  const printer = lockedData.printer;
23
23
  const app = (0, express_1.default)();
24
24
  const server = http_1.default.createServer(app);
25
25
  const io = new socket_io_1.Server(server);
26
26
  const loggerFactory = new LoggerFactory_1.LoggerFactory(config.isVerbose, io);
27
+ const upload = (0, multer_1.default)();
27
28
  const allSteps = [...steps_1.systemSteps, ...lockedData.activeSteps];
28
29
  const cronManager = (0, cron_handler_1.setupCronHandlers)(lockedData, eventManager, state, loggerFactory);
29
30
  const asyncHandler = (step) => {
@@ -37,46 +38,24 @@ const createServer = async (lockedData, eventManager, state, config) => {
37
38
  headers: req.headers,
38
39
  pathParams: req.params,
39
40
  queryParams: req.query,
40
- };
41
- const ctx = {
42
- emit: async (event) => {
43
- await eventManager.emit({
44
- topic: event.topic,
45
- data: event.data,
46
- traceId,
47
- logger,
48
- });
49
- },
50
- traceId,
51
- state,
52
- logger,
53
- };
54
- const finalHandler = async () => {
55
- try {
56
- const result = await (0, call_step_file_1.callStepFile)({
57
- contextInFirstArg: false,
58
- data: request,
59
- step,
60
- printer,
61
- logger,
62
- eventManager,
63
- state,
64
- traceId,
65
- });
66
- if (!result) {
67
- return { status: 500, body: { error: 'Internal server error' } };
68
- }
69
- return result;
70
- }
71
- catch (error) {
72
- logger.error('[API] Internal server error', { error });
73
- console.log(error);
74
- return { status: 500, body: { error: 'Internal server error' } };
75
- }
41
+ files: req.files,
76
42
  };
77
43
  try {
78
- const middleware = step.config.middleware || [];
79
- const result = await (0, middleware_composer_1.default)(...middleware)(request, ctx, finalHandler);
44
+ const data = request;
45
+ const result = await (0, call_step_file_1.callStepFile)({
46
+ contextInFirstArg: false,
47
+ data,
48
+ step,
49
+ printer,
50
+ logger,
51
+ eventManager,
52
+ state,
53
+ traceId,
54
+ });
55
+ if (!result) {
56
+ res.status(500).json({ error: 'Internal server error' });
57
+ return;
58
+ }
80
59
  if (result.headers) {
81
60
  Object.entries(result.headers).forEach(([key, value]) => res.setHeader(key, value));
82
61
  }
@@ -84,7 +63,8 @@ const createServer = async (lockedData, eventManager, state, config) => {
84
63
  res.json(result.body);
85
64
  }
86
65
  catch (error) {
87
- logger.error('[API] Error in middleware chain', { error });
66
+ logger.error('[API] Internal server error', { error });
67
+ console.log(error);
88
68
  res.status(500).json({ error: 'Internal server error' });
89
69
  }
90
70
  };
@@ -95,15 +75,15 @@ const createServer = async (lockedData, eventManager, state, config) => {
95
75
  const addRoute = (step) => {
96
76
  const { method, path } = step.config;
97
77
  logger_1.globalLogger.debug('[API] Registering route', step.config);
98
- const expressHandler = asyncHandler(step);
78
+ const handler = asyncHandler(step);
99
79
  const methods = {
100
- GET: () => router.get(path, expressHandler),
101
- POST: () => router.post(path, expressHandler),
102
- PUT: () => router.put(path, expressHandler),
103
- DELETE: () => router.delete(path, expressHandler),
104
- PATCH: () => router.patch(path, expressHandler),
105
- OPTIONS: () => router.options(path, expressHandler),
106
- HEAD: () => router.head(path, expressHandler),
80
+ GET: () => router.get(path, handler),
81
+ POST: () => router.post(path, upload.any(), handler),
82
+ PUT: () => router.put(path, upload.any(), handler),
83
+ DELETE: () => router.delete(path, handler),
84
+ PATCH: () => router.patch(path, upload.any(), handler),
85
+ OPTIONS: () => router.options(path, handler),
86
+ HEAD: () => router.head(path, handler),
107
87
  };
108
88
  const methodHandler = methods[method];
109
89
  if (!methodHandler) {
@@ -71,6 +71,9 @@ export type ApiRequest = {
71
71
  queryParams: Record<string, string | string[]>;
72
72
  body: Record<string, any>;
73
73
  headers: Record<string, string | string[]>;
74
+ files?: Express.Multer.File[] | {
75
+ [fieldname: string]: Express.Multer.File[];
76
+ };
74
77
  };
75
78
  export type ApiResponse = {
76
79
  status: number;
package/package.json CHANGED
@@ -2,14 +2,15 @@
2
2
  "name": "@motiadev/core",
3
3
  "description": "Core functionality for the Motia framework, providing the foundation for building event-driven workflows.",
4
4
  "main": "dist/index.js",
5
- "version": "0.1.0-beta.10",
5
+ "version": "0.1.0-beta.12",
6
6
  "dependencies": {
7
7
  "body-parser": "^1.20.3",
8
8
  "colors": "^1.4.0",
9
+ "dotenv": "^16.4.7",
9
10
  "express": "^4.21.2",
10
11
  "ioredis": "^5.4.2",
12
+ "multer": "1.4.5-lts.1",
11
13
  "node-cron": "^3.0.3",
12
- "serialize-javascript": "^6.0.2",
13
14
  "socket.io": "^4.8.1",
14
15
  "ts-node": "^10.9.2",
15
16
  "tsconfig-paths": "^4.2.0",
@@ -20,8 +21,8 @@
20
21
  "@types/body-parser": "^1.19.5",
21
22
  "@types/express": "^5.0.0",
22
23
  "@types/jest": "^29.5.14",
24
+ "@types/multer": "^1.4.12",
23
25
  "@types/node-cron": "^3.0.11",
24
- "@types/serialize-javascript": "^5.0.4",
25
26
  "@types/supertest": "^6.0.2",
26
27
  "jest": "^29.7.0",
27
28
  "supertest": "^7.0.0",
@@ -1,3 +0,0 @@
1
- import { ApiMiddleware, ApiRequest, ApiResponse, FlowContext } from './types';
2
- declare const _default: (...middlewares: ApiMiddleware[]) => (req: ApiRequest, ctx: FlowContext, handler: () => Promise<ApiResponse>) => Promise<ApiResponse>;
3
- export default _default;