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

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.
@@ -8,13 +8,16 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const flowsConfigEndpoint = (app, baseDir) => {
10
10
  const configPath = path_1.default.join(baseDir, 'motia-workbench.json');
11
+ const getConfig = () => {
12
+ if (fs_1.default.existsSync(configPath)) {
13
+ return JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
14
+ }
15
+ return {};
16
+ };
11
17
  app.post('/flows/:id/config', (req, res) => {
12
18
  const newFlowConfig = req.body;
13
19
  try {
14
- if (!fs_1.default.existsSync(configPath)) {
15
- fs_1.default.writeFileSync(configPath, JSON.stringify({}, null, 2));
16
- }
17
- const existingConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
20
+ const existingConfig = getConfig();
18
21
  const updatedConfig = {
19
22
  ...existingConfig,
20
23
  };
@@ -28,21 +31,20 @@ const flowsConfigEndpoint = (app, baseDir) => {
28
31
  res.status(200).send({ message: 'Flow config saved successfully' });
29
32
  }
30
33
  catch (error) {
31
- console.error('Error saving flow config:', error);
34
+ console.error('Error saving flow config:', error.message);
32
35
  res.status(500).json({ error: 'Failed to save flow config' });
33
36
  }
34
37
  });
35
38
  app.get('/flows/:id/config', (req, res) => {
36
39
  const { id } = req.params;
37
40
  try {
38
- const file = fs_1.default.readFileSync(configPath, 'utf8');
39
- const allFlowsConfig = JSON.parse(file);
41
+ const allFlowsConfig = getConfig();
40
42
  const flowConfig = allFlowsConfig[id] || {};
41
43
  res.status(200).send(flowConfig);
42
44
  }
43
45
  catch (error) {
44
- console.error('Error reading flow config:', error);
45
- res.status(400).send({});
46
+ console.error('Error reading flow config:', error.message);
47
+ res.status(400).send({ error: 'Failed to read flow config' });
46
48
  }
47
49
  });
48
50
  };
@@ -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 = message;
41
+ config = command === 'node' ? eval('(' + message + ')') : 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,3 @@
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;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = (...middlewares) => {
4
+ return async (req, ctx, handler) => {
5
+ const composedHandler = middlewares.reduceRight((nextHandler, middleware) => () => middleware(req, ctx, nextHandler), handler);
6
+ return composedHandler();
7
+ };
8
+ };
@@ -6,6 +6,7 @@ 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"));
9
10
  // Add ts-node registration before dynamic imports
10
11
  // eslint-disable-next-line @typescript-eslint/no-require-imports
11
12
  require('ts-node').register({
@@ -26,7 +27,7 @@ async function getConfig(filePath) {
26
27
  else if (module.config.bodySchema instanceof zod_1.ZodObject) {
27
28
  module.config.bodySchema = (0, zod_to_json_schema_1.default)(module.config.bodySchema);
28
29
  }
29
- process.send?.(module.config);
30
+ process.send?.((0, serialize_javascript_1.default)(module.config));
30
31
  process.exit(0);
31
32
  }
32
33
  catch (error) {
@@ -1,6 +1,6 @@
1
1
  import time
2
2
  from typing import Any, Dict, Optional
3
- from rpc import RpcSender
3
+ from rpc import RpcSender, serialize_for_json
4
4
 
5
5
  class Logger:
6
6
  def __init__(self, trace_id: str, flows: list[str], rpc: RpcSender):
@@ -18,7 +18,7 @@ class Logger:
18
18
  }
19
19
 
20
20
  if args:
21
- # Convert SimpleNamespace to dict if needed
21
+ # Use our serializer to ensure args are JSON-serializable
22
22
  if hasattr(args, '__dict__'):
23
23
  args = vars(args)
24
24
  elif not isinstance(args, dict):
@@ -5,9 +5,9 @@ import os
5
5
  import asyncio
6
6
  import traceback
7
7
  from logger import Logger
8
- from rpc import RpcSender
8
+ from rpc import RpcSender, serialize_for_json
9
9
  from rpc_state_manager import RpcStateManager
10
- from typing import Any
10
+ from typing import Any, Optional
11
11
 
12
12
  def parse_args(arg: str) -> Any:
13
13
  from types import SimpleNamespace
@@ -65,6 +65,8 @@ async def run_python_module(file_path: str, rpc: RpcSender, args: Any) -> None:
65
65
  if contextInFirstArg:
66
66
  result = await module.handler(context)
67
67
  else:
68
+ if hasattr(args.data, 'body'):
69
+ args.data.body = serialize_for_json(args.data.body)
68
70
  result = await module.handler(args.data, context)
69
71
 
70
72
  if result:
@@ -8,6 +8,20 @@ from typing import Any, Dict, Tuple
8
8
  # get the FD from ENV
9
9
  NODEIPCFD = int(os.environ["NODE_CHANNEL_FD"])
10
10
 
11
+ def serialize_for_json(obj: Any) -> Any:
12
+ """Convert Python objects to JSON-serializable types"""
13
+ if hasattr(obj, '__dict__'):
14
+ return obj.__dict__
15
+ elif hasattr(obj, '_asdict'): # For namedtuples
16
+ return obj._asdict()
17
+ elif isinstance(obj, (list, tuple)):
18
+ return [serialize_for_json(item) for item in obj]
19
+ elif isinstance(obj, dict):
20
+ return {k: serialize_for_json(v) for k, v in obj.items()}
21
+ else:
22
+ # Try to return the object as is, letting json handle basic types
23
+ return obj
24
+
11
25
  class RpcSender:
12
26
  def __init__(self):
13
27
  self.executing = True
@@ -1,19 +1,43 @@
1
1
  from typing import Any
2
2
  import asyncio
3
3
  from rpc import RpcSender
4
+ import functools
5
+ import sys
4
6
 
5
7
  class RpcStateManager:
6
8
  def __init__(self, rpc: RpcSender):
7
9
  self.rpc = rpc
10
+ self._loop = asyncio.get_event_loop()
8
11
 
9
12
  async def get(self, trace_id: str, key: str) -> asyncio.Future[Any]:
10
13
  return await self.rpc.send('state.get', {'traceId': trace_id, 'key': key})
11
14
 
12
15
  async def set(self, trace_id: str, key: str, value: Any) -> asyncio.Future[None]:
13
- return await self.rpc.send('state.set', {'traceId': trace_id, 'key': key, 'value': value})
16
+ future = await self.rpc.send('state.set', {'traceId': trace_id, 'key': key, 'value': value})
17
+ return future
14
18
 
15
19
  async def delete(self, trace_id: str, key: str) -> asyncio.Future[None]:
16
20
  return await self.rpc.send('state.delete', {'traceId': trace_id, 'key': key})
17
21
 
18
22
  async def clear(self, trace_id: str) -> asyncio.Future[None]:
19
23
  return await self.rpc.send('state.clear', {'traceId': trace_id})
24
+
25
+ # Add wrappers to handle non-awaited coroutines
26
+ def __getattribute__(self, name):
27
+ attr = super().__getattribute__(name)
28
+ if name in ('get', 'set', 'delete', 'clear') and asyncio.iscoroutinefunction(attr):
29
+ @functools.wraps(attr)
30
+ def wrapper(*args, **kwargs):
31
+ coro = attr(*args, **kwargs)
32
+ # Check if this is being awaited
33
+ frame = sys._getframe(1)
34
+ if frame.f_code.co_name != '__await__':
35
+ # Not being awaited, schedule in background
36
+ 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)
39
+ return task
40
+ # Being awaited, return coroutine as normal
41
+ return coro
42
+ return wrapper
43
+ return attr
@@ -17,6 +17,7 @@ const call_step_file_1 = require("./call-step-file");
17
17
  const LoggerFactory_1 = require("./LoggerFactory");
18
18
  const generate_trace_id_1 = require("./generate-trace-id");
19
19
  const flows_config_endpoint_1 = require("./flows-config-endpoint");
20
+ const middleware_composer_1 = __importDefault(require("./middleware-composer"));
20
21
  const createServer = async (lockedData, eventManager, state, config) => {
21
22
  const printer = lockedData.printer;
22
23
  const app = (0, express_1.default)();
@@ -37,22 +38,45 @@ const createServer = async (lockedData, eventManager, state, config) => {
37
38
  pathParams: req.params,
38
39
  queryParams: req.query,
39
40
  };
40
- try {
41
- const data = request;
42
- const result = await (0, call_step_file_1.callStepFile)({
43
- contextInFirstArg: false,
44
- data,
45
- step,
46
- printer,
47
- logger,
48
- eventManager,
49
- state,
50
- traceId,
51
- });
52
- if (!result) {
53
- res.status(500).json({ error: 'Internal server error' });
54
- return;
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' } };
55
75
  }
76
+ };
77
+ try {
78
+ const middleware = step.config.middleware || [];
79
+ const result = await (0, middleware_composer_1.default)(...middleware)(request, ctx, finalHandler);
56
80
  if (result.headers) {
57
81
  Object.entries(result.headers).forEach(([key, value]) => res.setHeader(key, value));
58
82
  }
@@ -60,8 +84,7 @@ const createServer = async (lockedData, eventManager, state, config) => {
60
84
  res.json(result.body);
61
85
  }
62
86
  catch (error) {
63
- logger.error('[API] Internal server error', { error });
64
- console.log(error);
87
+ logger.error('[API] Error in middleware chain', { error });
65
88
  res.status(500).json({ error: 'Internal server error' });
66
89
  }
67
90
  };
@@ -72,15 +95,15 @@ const createServer = async (lockedData, eventManager, state, config) => {
72
95
  const addRoute = (step) => {
73
96
  const { method, path } = step.config;
74
97
  logger_1.globalLogger.debug('[API] Registering route', step.config);
75
- const handler = asyncHandler(step);
98
+ const expressHandler = asyncHandler(step);
76
99
  const methods = {
77
- GET: () => router.get(path, handler),
78
- POST: () => router.post(path, handler),
79
- PUT: () => router.put(path, handler),
80
- DELETE: () => router.delete(path, handler),
81
- PATCH: () => router.patch(path, handler),
82
- OPTIONS: () => router.options(path, handler),
83
- HEAD: () => router.head(path, handler),
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),
84
107
  };
85
108
  const methodHandler = methods[method];
86
109
  if (!methodHandler) {
@@ -65,6 +65,7 @@ const eventSchema = zod_1.z
65
65
  virtualEmits: emits.optional(),
66
66
  input: zod_1.z.union([jsonSchema, zod_1.z.null()]).optional(),
67
67
  flows: zod_1.z.array(zod_1.z.string()).optional(),
68
+ includeFiles: zod_1.z.array(zod_1.z.string()).optional(),
68
69
  })
69
70
  .strict();
70
71
  const apiSchema = zod_1.z
@@ -79,6 +80,8 @@ const apiSchema = zod_1.z
79
80
  virtualSubscribes: zod_1.z.array(zod_1.z.string()).optional(),
80
81
  flows: zod_1.z.array(zod_1.z.string()).optional(),
81
82
  bodySchema: zod_1.z.union([jsonSchema, zod_1.z.null()]).optional(),
83
+ includeFiles: zod_1.z.array(zod_1.z.string()).optional(),
84
+ middleware: zod_1.z.array(zod_1.z.any()).optional(),
82
85
  })
83
86
  .strict();
84
87
  const cronSchema = zod_1.z
@@ -90,6 +93,7 @@ const cronSchema = zod_1.z
90
93
  virtualEmits: emits.optional(),
91
94
  emits: emits,
92
95
  flows: zod_1.z.array(zod_1.z.string()).optional(),
96
+ includeFiles: zod_1.z.array(zod_1.z.string()).optional(),
93
97
  })
94
98
  .strict();
95
99
  const validateStep = (step) => {
@@ -32,6 +32,11 @@ export type EventConfig<TInput extends ZodObject<any> = any> = {
32
32
  virtualEmits?: Emit[];
33
33
  input: TInput;
34
34
  flows?: string[];
35
+ /**
36
+ * Files to include in the step bundle.
37
+ * Needs to be relative to the step file.
38
+ */
39
+ includeFiles?: string[];
35
40
  };
36
41
  export type NoopConfig = {
37
42
  type: 'noop';
@@ -42,6 +47,7 @@ export type NoopConfig = {
42
47
  flows?: string[];
43
48
  };
44
49
  export type ApiRouteMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
50
+ export type ApiMiddleware = (req: ApiRequest, ctx: FlowContext, next: () => Promise<ApiResponse>) => Promise<ApiResponse>;
45
51
  export type ApiRouteConfig = {
46
52
  type: 'api';
47
53
  name: string;
@@ -52,7 +58,13 @@ export type ApiRouteConfig = {
52
58
  virtualEmits?: Emit[];
53
59
  virtualSubscribes?: string[];
54
60
  flows?: string[];
61
+ middleware?: ApiMiddleware[];
55
62
  bodySchema?: ZodObject<any>;
63
+ /**
64
+ * Files to include in the step bundle.
65
+ * Needs to be relative to the step file.
66
+ */
67
+ includeFiles?: string[];
56
68
  };
57
69
  export type ApiRequest = {
58
70
  pathParams: Record<string, string>;
@@ -74,6 +86,11 @@ export type CronConfig = {
74
86
  virtualEmits?: Emit[];
75
87
  emits: Emit[];
76
88
  flows?: string[];
89
+ /**
90
+ * Files to include in the step bundle.
91
+ * Needs to be relative to the step file.
92
+ */
93
+ includeFiles?: string[];
77
94
  };
78
95
  export type CronHandler = (ctx: FlowContext) => Promise<void>;
79
96
  export type StepHandler<T> = T extends EventConfig<any> ? EventHandler<T['input']> : T extends ApiRouteConfig ? ApiRouteHandler : T extends CronConfig ? CronHandler : never;
package/package.json CHANGED
@@ -2,13 +2,14 @@
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",
5
+ "version": "0.1.0-beta.10",
6
6
  "dependencies": {
7
7
  "body-parser": "^1.20.3",
8
8
  "colors": "^1.4.0",
9
9
  "express": "^4.21.2",
10
10
  "ioredis": "^5.4.2",
11
11
  "node-cron": "^3.0.3",
12
+ "serialize-javascript": "^6.0.2",
12
13
  "socket.io": "^4.8.1",
13
14
  "ts-node": "^10.9.2",
14
15
  "tsconfig-paths": "^4.2.0",
@@ -20,6 +21,7 @@
20
21
  "@types/express": "^5.0.0",
21
22
  "@types/jest": "^29.5.14",
22
23
  "@types/node-cron": "^3.0.11",
24
+ "@types/serialize-javascript": "^5.0.4",
23
25
  "@types/supertest": "^6.0.2",
24
26
  "jest": "^29.7.0",
25
27
  "supertest": "^7.0.0",