@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.
- package/dist/src/flows-config-endpoint.js +11 -9
- package/dist/src/get-step-config.js +1 -1
- package/dist/src/middleware-composer.d.ts +3 -0
- package/dist/src/middleware-composer.js +8 -0
- package/dist/src/node/get-config.js +2 -1
- package/dist/src/python/logger.py +2 -2
- package/dist/src/python/python-runner.py +4 -2
- package/dist/src/python/rpc.py +14 -0
- package/dist/src/python/rpc_state_manager.py +25 -1
- package/dist/src/server.js +48 -25
- package/dist/src/step-validator.js +4 -0
- package/dist/src/types.d.ts +17 -0
- package/package.json +3 -1
|
@@ -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
|
-
|
|
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
|
|
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,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
|
-
#
|
|
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:
|
package/dist/src/python/rpc.py
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/src/server.js
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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]
|
|
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
|
|
98
|
+
const expressHandler = asyncHandler(step);
|
|
76
99
|
const methods = {
|
|
77
|
-
GET: () => router.get(path,
|
|
78
|
-
POST: () => router.post(path,
|
|
79
|
-
PUT: () => router.put(path,
|
|
80
|
-
DELETE: () => router.delete(path,
|
|
81
|
-
PATCH: () => router.patch(path,
|
|
82
|
-
OPTIONS: () => router.options(path,
|
|
83
|
-
HEAD: () => router.head(path,
|
|
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) => {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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",
|