@meetploy/cli 1.6.0 → 1.8.0
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/dashboard-dist/assets/main-Bsiap3cJ.css +1 -0
- package/dist/dashboard-dist/assets/main-X-CWS305.js +176 -0
- package/dist/dashboard-dist/index.html +13 -0
- package/dist/dev.d.ts +1 -2
- package/dist/dev.js +1065 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4298 -150
- package/package.json +22 -5
- package/dist/commands/build.d.ts +0 -7
- package/dist/commands/build.js +0 -94
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/types.d.ts +0 -7
- package/dist/commands/types.js +0 -145
- package/dist/commands/types.js.map +0 -1
- package/dist/dev.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/dev.js
CHANGED
|
@@ -1,2 +1,1065 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import 'child_process';
|
|
3
|
+
import { readFile, existsSync, readFileSync, mkdirSync } from 'fs';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import 'esbuild';
|
|
7
|
+
import 'chokidar';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import { parse } from 'yaml';
|
|
10
|
+
import { serve } from '@hono/node-server';
|
|
11
|
+
import { Hono } from 'hono';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
import 'os';
|
|
14
|
+
import Database from 'better-sqlite3';
|
|
15
|
+
|
|
16
|
+
createRequire(import.meta.url);
|
|
17
|
+
promisify(readFile);
|
|
18
|
+
function readPloyConfigSync(projectDir, configPath) {
|
|
19
|
+
const configFile = configPath;
|
|
20
|
+
const fullPath = join(projectDir, configFile);
|
|
21
|
+
if (!existsSync(fullPath)) {
|
|
22
|
+
throw new Error(`Config file not found: ${fullPath}`);
|
|
23
|
+
}
|
|
24
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
25
|
+
return parse(content);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ../emulator/dist/config/ploy-config.js
|
|
29
|
+
function readPloyConfig(projectDir, configPath) {
|
|
30
|
+
const config = readPloyConfigSync(projectDir, configPath);
|
|
31
|
+
if (!config.kind) {
|
|
32
|
+
throw new Error(`Missing required field 'kind' in ${configPath}`);
|
|
33
|
+
}
|
|
34
|
+
if (config.kind !== "dynamic" && config.kind !== "worker") {
|
|
35
|
+
throw new Error(`Invalid kind '${config.kind}' in ${configPath}. Must be 'dynamic' or 'worker'`);
|
|
36
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
}
|
|
39
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
40
|
+
var __dirname = dirname(__filename);
|
|
41
|
+
function findDashboardDistPath() {
|
|
42
|
+
const possiblePaths = [
|
|
43
|
+
join(__dirname, "..", "dashboard-dist"),
|
|
44
|
+
join(__dirname, "..", "..", "src", "dashboard-dist")
|
|
45
|
+
];
|
|
46
|
+
for (const p of possiblePaths) {
|
|
47
|
+
if (existsSync(p)) {
|
|
48
|
+
return p;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
var MIME_TYPES = {
|
|
54
|
+
".html": "text/html",
|
|
55
|
+
".js": "application/javascript",
|
|
56
|
+
".css": "text/css",
|
|
57
|
+
".json": "application/json",
|
|
58
|
+
".png": "image/png",
|
|
59
|
+
".jpg": "image/jpeg",
|
|
60
|
+
".svg": "image/svg+xml",
|
|
61
|
+
".ico": "image/x-icon",
|
|
62
|
+
".woff": "font/woff",
|
|
63
|
+
".woff2": "font/woff2"
|
|
64
|
+
};
|
|
65
|
+
function getMimeType(filePath) {
|
|
66
|
+
const ext = filePath.substring(filePath.lastIndexOf("."));
|
|
67
|
+
return MIME_TYPES[ext] || "application/octet-stream";
|
|
68
|
+
}
|
|
69
|
+
function createDashboardRoutes(app, dbManager2, config) {
|
|
70
|
+
const dashboardDistPath = findDashboardDistPath();
|
|
71
|
+
const hasDashboard = dashboardDistPath !== null;
|
|
72
|
+
function getDbResourceName(bindingName) {
|
|
73
|
+
return config.db?.[bindingName] ?? null;
|
|
74
|
+
}
|
|
75
|
+
app.get("/api/config", (c) => {
|
|
76
|
+
return c.json({
|
|
77
|
+
db: config.db,
|
|
78
|
+
queue: config.queue,
|
|
79
|
+
workflow: config.workflow
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
app.post("/api/db/:binding/query", async (c) => {
|
|
83
|
+
const binding = c.req.param("binding");
|
|
84
|
+
const resourceName = getDbResourceName(binding);
|
|
85
|
+
if (!resourceName) {
|
|
86
|
+
return c.json({ error: `Database binding '${binding}' not found` }, 404);
|
|
87
|
+
}
|
|
88
|
+
const body = await c.req.json();
|
|
89
|
+
const { query } = body;
|
|
90
|
+
if (!query) {
|
|
91
|
+
return c.json({ error: "Query is required" }, 400);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const db = dbManager2.getD1Database(resourceName);
|
|
95
|
+
const startTime = Date.now();
|
|
96
|
+
const stmt = db.prepare(query);
|
|
97
|
+
const isSelect = query.trim().toUpperCase().startsWith("SELECT");
|
|
98
|
+
let results = [];
|
|
99
|
+
let changes = 0;
|
|
100
|
+
if (isSelect) {
|
|
101
|
+
results = stmt.all();
|
|
102
|
+
} else {
|
|
103
|
+
const info = stmt.run();
|
|
104
|
+
changes = info.changes;
|
|
105
|
+
}
|
|
106
|
+
const duration = Date.now() - startTime;
|
|
107
|
+
return c.json({
|
|
108
|
+
results,
|
|
109
|
+
success: true,
|
|
110
|
+
meta: {
|
|
111
|
+
duration,
|
|
112
|
+
rows_read: results.length,
|
|
113
|
+
rows_written: changes
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return c.json({
|
|
118
|
+
results: [],
|
|
119
|
+
success: false,
|
|
120
|
+
error: err instanceof Error ? err.message : String(err),
|
|
121
|
+
meta: { duration: 0, rows_read: 0, rows_written: 0 }
|
|
122
|
+
}, 400);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
app.get("/api/db/:binding/tables", (c) => {
|
|
126
|
+
const binding = c.req.param("binding");
|
|
127
|
+
const resourceName = getDbResourceName(binding);
|
|
128
|
+
if (!resourceName) {
|
|
129
|
+
return c.json({ error: `Database binding '${binding}' not found` }, 404);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const db = dbManager2.getD1Database(resourceName);
|
|
133
|
+
const tables = db.prepare(`SELECT name FROM sqlite_master
|
|
134
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%'
|
|
135
|
+
ORDER BY name`).all();
|
|
136
|
+
return c.json({ tables });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
app.get("/api/db/:binding/tables/:tableName", (c) => {
|
|
142
|
+
const binding = c.req.param("binding");
|
|
143
|
+
const resourceName = getDbResourceName(binding);
|
|
144
|
+
if (!resourceName) {
|
|
145
|
+
return c.json({ error: `Database binding '${binding}' not found` }, 404);
|
|
146
|
+
}
|
|
147
|
+
const tableName = c.req.param("tableName");
|
|
148
|
+
const limit = parseInt(c.req.query("limit") || "50", 10);
|
|
149
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
150
|
+
try {
|
|
151
|
+
const db = dbManager2.getD1Database(resourceName);
|
|
152
|
+
const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
|
|
153
|
+
const columns = columnsResult.map((col) => col.name);
|
|
154
|
+
const countResult = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get();
|
|
155
|
+
const total = countResult.count;
|
|
156
|
+
const data = db.prepare(`SELECT * FROM "${tableName}" LIMIT ? OFFSET ?`).all(limit, offset);
|
|
157
|
+
return c.json({ data, columns, total });
|
|
158
|
+
} catch (err) {
|
|
159
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
app.get("/api/db/:binding/schema", (c) => {
|
|
163
|
+
const binding = c.req.param("binding");
|
|
164
|
+
const resourceName = getDbResourceName(binding);
|
|
165
|
+
if (!resourceName) {
|
|
166
|
+
return c.json({ error: `Database binding '${binding}' not found` }, 404);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const db = dbManager2.getD1Database(resourceName);
|
|
170
|
+
const tablesResult = db.prepare(`SELECT name FROM sqlite_master
|
|
171
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
172
|
+
ORDER BY name`).all();
|
|
173
|
+
const tables = tablesResult.map((table) => {
|
|
174
|
+
const columnsResult = db.prepare(`PRAGMA table_info("${table.name}")`).all();
|
|
175
|
+
return {
|
|
176
|
+
name: table.name,
|
|
177
|
+
columns: columnsResult.map((col) => ({
|
|
178
|
+
name: col.name,
|
|
179
|
+
type: col.type,
|
|
180
|
+
notNull: col.notnull === 1,
|
|
181
|
+
primaryKey: col.pk === 1
|
|
182
|
+
}))
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
return c.json({ tables });
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
app.get("/api/queue/:binding/metrics", (c) => {
|
|
191
|
+
const binding = c.req.param("binding");
|
|
192
|
+
const queueName = config.queue?.[binding];
|
|
193
|
+
if (!queueName) {
|
|
194
|
+
return c.json({ error: "Queue not found" }, 404);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const db = dbManager2.emulatorDb;
|
|
198
|
+
const metrics = {
|
|
199
|
+
pending: 0,
|
|
200
|
+
processing: 0,
|
|
201
|
+
acknowledged: 0,
|
|
202
|
+
failed: 0,
|
|
203
|
+
deadLetter: 0
|
|
204
|
+
};
|
|
205
|
+
const statusCounts = db.prepare(`SELECT status, COUNT(*) as count
|
|
206
|
+
FROM queue_messages
|
|
207
|
+
WHERE queue_name = ?
|
|
208
|
+
GROUP BY status`).all(queueName);
|
|
209
|
+
for (const row of statusCounts) {
|
|
210
|
+
if (row.status === "pending") {
|
|
211
|
+
metrics.pending = row.count;
|
|
212
|
+
} else if (row.status === "processing") {
|
|
213
|
+
metrics.processing = row.count;
|
|
214
|
+
} else if (row.status === "acknowledged") {
|
|
215
|
+
metrics.acknowledged = row.count;
|
|
216
|
+
} else if (row.status === "failed") {
|
|
217
|
+
metrics.failed = row.count;
|
|
218
|
+
} else if (row.status === "dead_letter") {
|
|
219
|
+
metrics.deadLetter = row.count;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return c.json({ metrics });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
app.get("/api/queue/:binding/messages", (c) => {
|
|
228
|
+
const binding = c.req.param("binding");
|
|
229
|
+
const queueName = config.queue?.[binding];
|
|
230
|
+
const limit = parseInt(c.req.query("limit") || "10", 10);
|
|
231
|
+
if (!queueName) {
|
|
232
|
+
return c.json({ error: "Queue not found" }, 404);
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const db = dbManager2.emulatorDb;
|
|
236
|
+
const messages = db.prepare(`SELECT id, status, payload, attempt, created_at
|
|
237
|
+
FROM queue_messages
|
|
238
|
+
WHERE queue_name = ?
|
|
239
|
+
ORDER BY created_at DESC
|
|
240
|
+
LIMIT ?`).all(queueName, limit);
|
|
241
|
+
return c.json({
|
|
242
|
+
messages: messages.map((m) => ({
|
|
243
|
+
id: m.id,
|
|
244
|
+
status: m.status.toUpperCase(),
|
|
245
|
+
payload: JSON.parse(m.payload),
|
|
246
|
+
attempt: m.attempt,
|
|
247
|
+
createdAt: new Date(m.created_at * 1e3).toISOString()
|
|
248
|
+
}))
|
|
249
|
+
});
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
app.get("/api/workflow/:binding/executions", (c) => {
|
|
255
|
+
const binding = c.req.param("binding");
|
|
256
|
+
const workflowConfig = config.workflow?.[binding];
|
|
257
|
+
const limit = parseInt(c.req.query("limit") || "20", 10);
|
|
258
|
+
if (!workflowConfig) {
|
|
259
|
+
return c.json({ error: "Workflow not found" }, 404);
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const db = dbManager2.emulatorDb;
|
|
263
|
+
const workflowName = workflowConfig;
|
|
264
|
+
const executions = db.prepare(`SELECT
|
|
265
|
+
e.id,
|
|
266
|
+
e.workflow_name,
|
|
267
|
+
e.status,
|
|
268
|
+
e.error,
|
|
269
|
+
e.started_at,
|
|
270
|
+
e.completed_at,
|
|
271
|
+
e.created_at,
|
|
272
|
+
(SELECT COUNT(*) FROM workflow_steps WHERE execution_id = e.id) as steps_count,
|
|
273
|
+
(SELECT COUNT(*) FROM workflow_steps WHERE execution_id = e.id AND status = 'completed') as steps_completed
|
|
274
|
+
FROM workflow_executions e
|
|
275
|
+
WHERE e.workflow_name = ?
|
|
276
|
+
ORDER BY e.created_at DESC
|
|
277
|
+
LIMIT ?`).all(workflowName, limit);
|
|
278
|
+
return c.json({
|
|
279
|
+
executions: executions.map((e) => ({
|
|
280
|
+
id: e.id,
|
|
281
|
+
status: e.status.toUpperCase(),
|
|
282
|
+
startedAt: e.started_at ? new Date(e.started_at * 1e3).toISOString() : null,
|
|
283
|
+
completedAt: e.completed_at ? new Date(e.completed_at * 1e3).toISOString() : null,
|
|
284
|
+
durationMs: e.started_at && e.completed_at ? (e.completed_at - e.started_at) * 1e3 : null,
|
|
285
|
+
stepsCount: e.steps_count,
|
|
286
|
+
stepsCompleted: e.steps_completed,
|
|
287
|
+
errorMessage: e.error,
|
|
288
|
+
createdAt: new Date(e.created_at * 1e3).toISOString()
|
|
289
|
+
}))
|
|
290
|
+
});
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
app.get("/api/workflow/:binding/executions/:executionId", (c) => {
|
|
296
|
+
const binding = c.req.param("binding");
|
|
297
|
+
const executionId = c.req.param("executionId");
|
|
298
|
+
const workflowConfig = config.workflow?.[binding];
|
|
299
|
+
if (!workflowConfig) {
|
|
300
|
+
return c.json({ error: "Workflow not found" }, 404);
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const db = dbManager2.emulatorDb;
|
|
304
|
+
const execution = db.prepare(`SELECT id, workflow_name, status, error, started_at, completed_at, created_at
|
|
305
|
+
FROM workflow_executions
|
|
306
|
+
WHERE id = ?`).get(executionId);
|
|
307
|
+
if (!execution) {
|
|
308
|
+
return c.json({ error: "Execution not found" }, 404);
|
|
309
|
+
}
|
|
310
|
+
const steps = db.prepare(`SELECT id, step_name, step_index, status, output, error, duration_ms, created_at
|
|
311
|
+
FROM workflow_steps
|
|
312
|
+
WHERE execution_id = ?
|
|
313
|
+
ORDER BY step_index`).all(executionId);
|
|
314
|
+
return c.json({
|
|
315
|
+
execution: {
|
|
316
|
+
id: execution.id,
|
|
317
|
+
status: execution.status.toUpperCase(),
|
|
318
|
+
startedAt: execution.started_at ? new Date(execution.started_at * 1e3).toISOString() : null,
|
|
319
|
+
completedAt: execution.completed_at ? new Date(execution.completed_at * 1e3).toISOString() : null,
|
|
320
|
+
durationMs: execution.started_at && execution.completed_at ? (execution.completed_at - execution.started_at) * 1e3 : null,
|
|
321
|
+
stepsCount: steps.length,
|
|
322
|
+
stepsCompleted: steps.filter((s) => s.status === "completed").length,
|
|
323
|
+
errorMessage: execution.error,
|
|
324
|
+
createdAt: new Date(execution.created_at * 1e3).toISOString()
|
|
325
|
+
},
|
|
326
|
+
steps: steps.map((s) => ({
|
|
327
|
+
id: String(s.id),
|
|
328
|
+
stepName: s.step_name,
|
|
329
|
+
stepIndex: s.step_index,
|
|
330
|
+
status: s.status.toUpperCase(),
|
|
331
|
+
output: s.output ? JSON.parse(s.output) : null,
|
|
332
|
+
errorMessage: s.error,
|
|
333
|
+
durationMs: s.duration_ms,
|
|
334
|
+
createdAt: new Date(s.created_at * 1e3).toISOString()
|
|
335
|
+
}))
|
|
336
|
+
});
|
|
337
|
+
} catch (err) {
|
|
338
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
if (hasDashboard) {
|
|
342
|
+
app.get("/assets/*", (c) => {
|
|
343
|
+
const path = c.req.path;
|
|
344
|
+
const filePath = join(dashboardDistPath, path);
|
|
345
|
+
if (!existsSync(filePath)) {
|
|
346
|
+
return c.notFound();
|
|
347
|
+
}
|
|
348
|
+
const content = readFileSync(filePath);
|
|
349
|
+
const mimeType = getMimeType(filePath);
|
|
350
|
+
return new Response(content, {
|
|
351
|
+
headers: {
|
|
352
|
+
"Content-Type": mimeType,
|
|
353
|
+
"Cache-Control": "public, max-age=31536000"
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
app.get("*", (c) => {
|
|
358
|
+
if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/db") || c.req.path.startsWith("/queue") || c.req.path.startsWith("/workflow")) {
|
|
359
|
+
return c.notFound();
|
|
360
|
+
}
|
|
361
|
+
const indexPath = join(dashboardDistPath, "index.html");
|
|
362
|
+
if (!existsSync(indexPath)) {
|
|
363
|
+
return c.text("Dashboard not found. Run 'pnpm build' in packages/dev-dashboard.", 404);
|
|
364
|
+
}
|
|
365
|
+
const content = readFileSync(indexPath, "utf-8");
|
|
366
|
+
return c.html(content);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ../emulator/dist/services/db-service.js
|
|
372
|
+
function createDbHandler(getDatabase) {
|
|
373
|
+
return async (c) => {
|
|
374
|
+
const startTime = Date.now();
|
|
375
|
+
try {
|
|
376
|
+
const body = await c.req.json();
|
|
377
|
+
const { bindingName, method, query, params, statements } = body;
|
|
378
|
+
const db = getDatabase(bindingName);
|
|
379
|
+
if (method === "prepare" && query) {
|
|
380
|
+
const stmt = db.prepare(query);
|
|
381
|
+
const isSelect = query.trim().toUpperCase().startsWith("SELECT");
|
|
382
|
+
let results;
|
|
383
|
+
let changes = 0;
|
|
384
|
+
if (isSelect) {
|
|
385
|
+
results = params?.length ? stmt.all(...params) : stmt.all();
|
|
386
|
+
} else {
|
|
387
|
+
const info = params?.length ? stmt.run(...params) : stmt.run();
|
|
388
|
+
changes = info.changes;
|
|
389
|
+
results = [];
|
|
390
|
+
}
|
|
391
|
+
const duration = Date.now() - startTime;
|
|
392
|
+
return c.json({
|
|
393
|
+
results,
|
|
394
|
+
success: true,
|
|
395
|
+
meta: {
|
|
396
|
+
duration,
|
|
397
|
+
rows_read: results.length,
|
|
398
|
+
rows_written: changes
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (method === "exec" && query) {
|
|
403
|
+
db.exec(query);
|
|
404
|
+
const duration = Date.now() - startTime;
|
|
405
|
+
return c.json({
|
|
406
|
+
results: [],
|
|
407
|
+
success: true,
|
|
408
|
+
meta: {
|
|
409
|
+
duration,
|
|
410
|
+
rows_read: 0,
|
|
411
|
+
rows_written: 0
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (method === "batch" && statements) {
|
|
416
|
+
const results = [];
|
|
417
|
+
const transaction = db.transaction(() => {
|
|
418
|
+
for (const stmt of statements) {
|
|
419
|
+
const prepared = db.prepare(stmt.query);
|
|
420
|
+
const isSelect = stmt.query.trim().toUpperCase().startsWith("SELECT");
|
|
421
|
+
if (isSelect) {
|
|
422
|
+
const rows = stmt.params?.length ? prepared.all(...stmt.params) : prepared.all();
|
|
423
|
+
results.push({
|
|
424
|
+
results: rows,
|
|
425
|
+
success: true,
|
|
426
|
+
meta: {
|
|
427
|
+
duration: 0,
|
|
428
|
+
rows_read: rows.length,
|
|
429
|
+
rows_written: 0
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
} else {
|
|
433
|
+
const info = stmt.params?.length ? prepared.run(...stmt.params) : prepared.run();
|
|
434
|
+
results.push({
|
|
435
|
+
results: [],
|
|
436
|
+
success: true,
|
|
437
|
+
meta: {
|
|
438
|
+
duration: 0,
|
|
439
|
+
rows_read: 0,
|
|
440
|
+
rows_written: info.changes
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
transaction();
|
|
447
|
+
return c.json(results);
|
|
448
|
+
}
|
|
449
|
+
if (method === "dump") {
|
|
450
|
+
const buffer = db.serialize();
|
|
451
|
+
return new Response(new Uint8Array(buffer), {
|
|
452
|
+
headers: {
|
|
453
|
+
"Content-Type": "application/octet-stream"
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return c.json({ error: "Invalid method" }, 400);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
460
|
+
return c.json({ error: message }, 500);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function createQueueHandlers(db) {
|
|
465
|
+
const sendHandler = async (c) => {
|
|
466
|
+
try {
|
|
467
|
+
const body = await c.req.json();
|
|
468
|
+
const { queueName, payload, delaySeconds = 0 } = body;
|
|
469
|
+
const id = randomUUID();
|
|
470
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
471
|
+
const visibleAt = now + delaySeconds;
|
|
472
|
+
db.prepare(`INSERT INTO queue_messages (id, queue_name, payload, visible_at)
|
|
473
|
+
VALUES (?, ?, ?, ?)`).run(id, queueName, JSON.stringify(payload), visibleAt);
|
|
474
|
+
return c.json({ success: true, messageId: id });
|
|
475
|
+
} catch (err) {
|
|
476
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
477
|
+
return c.json({ success: false, error: message }, 500);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const batchSendHandler = async (c) => {
|
|
481
|
+
try {
|
|
482
|
+
const body = await c.req.json();
|
|
483
|
+
const { queueName, messages } = body;
|
|
484
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
485
|
+
const messageIds = [];
|
|
486
|
+
const insert = db.prepare(`INSERT INTO queue_messages (id, queue_name, payload, visible_at)
|
|
487
|
+
VALUES (?, ?, ?, ?)`);
|
|
488
|
+
const transaction = db.transaction(() => {
|
|
489
|
+
for (const msg of messages) {
|
|
490
|
+
const id = randomUUID();
|
|
491
|
+
const visibleAt = now + (msg.delaySeconds || 0);
|
|
492
|
+
insert.run(id, queueName, JSON.stringify(msg.payload), visibleAt);
|
|
493
|
+
messageIds.push(id);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
transaction();
|
|
497
|
+
return c.json({ success: true, messageIds });
|
|
498
|
+
} catch (err) {
|
|
499
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
500
|
+
return c.json({ success: false, error: message }, 500);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const receiveHandler = async (c) => {
|
|
504
|
+
try {
|
|
505
|
+
const body = await c.req.json();
|
|
506
|
+
const { queueName, maxMessages = 10, visibilityTimeoutSeconds = 30 } = body;
|
|
507
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
508
|
+
const deliveryId = randomUUID();
|
|
509
|
+
const newVisibleAt = now + visibilityTimeoutSeconds;
|
|
510
|
+
const rows = db.prepare(`SELECT id, queue_name, payload, attempt
|
|
511
|
+
FROM queue_messages
|
|
512
|
+
WHERE queue_name = ? AND status = 'pending' AND visible_at <= ?
|
|
513
|
+
ORDER BY visible_at ASC
|
|
514
|
+
LIMIT ?`).all(queueName, now, maxMessages);
|
|
515
|
+
if (rows.length === 0) {
|
|
516
|
+
return c.json({ success: true, messages: [] });
|
|
517
|
+
}
|
|
518
|
+
const ids = rows.map((r) => r.id);
|
|
519
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
520
|
+
db.prepare(`UPDATE queue_messages
|
|
521
|
+
SET status = 'processing', delivery_id = ?, visible_at = ?, attempt = attempt + 1, updated_at = ?
|
|
522
|
+
WHERE id IN (${placeholders})`).run(deliveryId, newVisibleAt, now, ...ids);
|
|
523
|
+
const messages = rows.map((row) => ({
|
|
524
|
+
id: row.id,
|
|
525
|
+
payload: JSON.parse(row.payload),
|
|
526
|
+
attempt: row.attempt + 1,
|
|
527
|
+
deliveryId
|
|
528
|
+
}));
|
|
529
|
+
return c.json({ success: true, messages });
|
|
530
|
+
} catch (err) {
|
|
531
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
532
|
+
return c.json({ success: false, messages: [], error: message }, 500);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
const ackHandler = async (c) => {
|
|
536
|
+
try {
|
|
537
|
+
const body = await c.req.json();
|
|
538
|
+
const { messageId, deliveryId } = body;
|
|
539
|
+
const result = db.prepare(`DELETE FROM queue_messages
|
|
540
|
+
WHERE id = ? AND delivery_id = ?`).run(messageId, deliveryId);
|
|
541
|
+
if (result.changes === 0) {
|
|
542
|
+
return c.json({ success: false, error: "Message not found or already processed" }, 404);
|
|
543
|
+
}
|
|
544
|
+
return c.json({ success: true });
|
|
545
|
+
} catch (err) {
|
|
546
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
547
|
+
return c.json({ success: false, error: message }, 500);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
const retryHandler = async (c) => {
|
|
551
|
+
try {
|
|
552
|
+
const body = await c.req.json();
|
|
553
|
+
const { messageId, deliveryId } = body;
|
|
554
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
555
|
+
const backoffSeconds = 5;
|
|
556
|
+
const result = db.prepare(`UPDATE queue_messages
|
|
557
|
+
SET status = 'pending', delivery_id = NULL, visible_at = ?, updated_at = ?
|
|
558
|
+
WHERE id = ? AND delivery_id = ?`).run(now + backoffSeconds, now, messageId, deliveryId);
|
|
559
|
+
if (result.changes === 0) {
|
|
560
|
+
return c.json({ success: false, error: "Message not found or already processed" }, 404);
|
|
561
|
+
}
|
|
562
|
+
return c.json({ success: true });
|
|
563
|
+
} catch (err) {
|
|
564
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
565
|
+
return c.json({ success: false, error: message }, 500);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
return {
|
|
569
|
+
sendHandler,
|
|
570
|
+
batchSendHandler,
|
|
571
|
+
receiveHandler,
|
|
572
|
+
ackHandler,
|
|
573
|
+
retryHandler
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function createWorkflowHandlers(db, workerUrl) {
|
|
577
|
+
const triggerHandler = async (c) => {
|
|
578
|
+
try {
|
|
579
|
+
const body = await c.req.json();
|
|
580
|
+
const { workflowName, input } = body;
|
|
581
|
+
const id = randomUUID();
|
|
582
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
583
|
+
db.prepare(`INSERT INTO workflow_executions (id, workflow_name, status, input, started_at, created_at)
|
|
584
|
+
VALUES (?, ?, 'running', ?, ?, ?)`).run(id, workflowName, JSON.stringify(input), now, now);
|
|
585
|
+
if (workerUrl) {
|
|
586
|
+
fetch(workerUrl, {
|
|
587
|
+
method: "POST",
|
|
588
|
+
headers: {
|
|
589
|
+
"Content-Type": "application/json",
|
|
590
|
+
"X-Ploy-Workflow-Execution": "true",
|
|
591
|
+
"X-Ploy-Workflow-Name": workflowName,
|
|
592
|
+
"X-Ploy-Execution-Id": id
|
|
593
|
+
},
|
|
594
|
+
body: JSON.stringify(input)
|
|
595
|
+
}).catch(() => {
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return c.json({ success: true, executionId: id });
|
|
599
|
+
} catch (err) {
|
|
600
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
601
|
+
return c.json({ success: false, error: message }, 500);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const statusHandler = async (c) => {
|
|
605
|
+
try {
|
|
606
|
+
const body = await c.req.json();
|
|
607
|
+
const { executionId } = body;
|
|
608
|
+
const execution = db.prepare(`SELECT id, workflow_name, status, input, output, error, started_at, completed_at, created_at
|
|
609
|
+
FROM workflow_executions WHERE id = ?`).get(executionId);
|
|
610
|
+
if (!execution) {
|
|
611
|
+
return c.json({ success: false, error: "Execution not found" }, 404);
|
|
612
|
+
}
|
|
613
|
+
const steps = db.prepare(`SELECT step_name, step_index, status, output, error, duration_ms
|
|
614
|
+
FROM workflow_steps WHERE execution_id = ? ORDER BY step_index`).all(executionId);
|
|
615
|
+
return c.json({
|
|
616
|
+
success: true,
|
|
617
|
+
execution: {
|
|
618
|
+
id: execution.id,
|
|
619
|
+
workflowName: execution.workflow_name,
|
|
620
|
+
status: execution.status,
|
|
621
|
+
input: execution.input ? JSON.parse(execution.input) : null,
|
|
622
|
+
output: execution.output ? JSON.parse(execution.output) : null,
|
|
623
|
+
error: execution.error,
|
|
624
|
+
startedAt: execution.started_at ? new Date(execution.started_at * 1e3).toISOString() : null,
|
|
625
|
+
completedAt: execution.completed_at ? new Date(execution.completed_at * 1e3).toISOString() : null,
|
|
626
|
+
steps: steps.map((s) => ({
|
|
627
|
+
stepName: s.step_name,
|
|
628
|
+
stepIndex: s.step_index,
|
|
629
|
+
status: s.status,
|
|
630
|
+
output: s.output ? JSON.parse(s.output) : null,
|
|
631
|
+
error: s.error,
|
|
632
|
+
durationMs: s.duration_ms
|
|
633
|
+
}))
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
} catch (err) {
|
|
637
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
638
|
+
return c.json({ success: false, error: message }, 500);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const cancelHandler = async (c) => {
|
|
642
|
+
try {
|
|
643
|
+
const body = await c.req.json();
|
|
644
|
+
const { executionId } = body;
|
|
645
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
646
|
+
const result = db.prepare(`UPDATE workflow_executions
|
|
647
|
+
SET status = 'cancelled', completed_at = ?
|
|
648
|
+
WHERE id = ? AND status IN ('pending', 'running')`).run(now, executionId);
|
|
649
|
+
if (result.changes === 0) {
|
|
650
|
+
return c.json({ success: false, error: "Execution not found or already completed" }, 404);
|
|
651
|
+
}
|
|
652
|
+
return c.json({ success: true });
|
|
653
|
+
} catch (err) {
|
|
654
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
655
|
+
return c.json({ success: false, error: message }, 500);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
const stepStartHandler = async (c) => {
|
|
659
|
+
try {
|
|
660
|
+
const body = await c.req.json();
|
|
661
|
+
const { executionId, stepName, stepIndex } = body;
|
|
662
|
+
const existing = db.prepare(`SELECT status, output FROM workflow_steps
|
|
663
|
+
WHERE execution_id = ? AND step_name = ? AND step_index = ?`).get(executionId, stepName, stepIndex);
|
|
664
|
+
if (existing && existing.status === "completed") {
|
|
665
|
+
return c.json({
|
|
666
|
+
success: true,
|
|
667
|
+
alreadyCompleted: true,
|
|
668
|
+
output: existing.output ? JSON.parse(existing.output) : null
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
672
|
+
if (existing) {
|
|
673
|
+
db.prepare(`UPDATE workflow_steps SET status = 'running' WHERE execution_id = ? AND step_name = ?`).run(executionId, stepName);
|
|
674
|
+
} else {
|
|
675
|
+
db.prepare(`INSERT INTO workflow_steps (execution_id, step_name, step_index, status, created_at)
|
|
676
|
+
VALUES (?, ?, ?, 'running', ?)`).run(executionId, stepName, stepIndex, now);
|
|
677
|
+
}
|
|
678
|
+
return c.json({ success: true, alreadyCompleted: false });
|
|
679
|
+
} catch (err) {
|
|
680
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
681
|
+
return c.json({ success: false, error: message }, 500);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
const stepCompleteHandler = async (c) => {
|
|
685
|
+
try {
|
|
686
|
+
const body = await c.req.json();
|
|
687
|
+
const { executionId, stepName, output, durationMs } = body;
|
|
688
|
+
db.prepare(`UPDATE workflow_steps
|
|
689
|
+
SET status = 'completed', output = ?, duration_ms = ?
|
|
690
|
+
WHERE execution_id = ? AND step_name = ?`).run(JSON.stringify(output), durationMs, executionId, stepName);
|
|
691
|
+
return c.json({ success: true });
|
|
692
|
+
} catch (err) {
|
|
693
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
694
|
+
return c.json({ success: false, error: message }, 500);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const stepFailHandler = async (c) => {
|
|
698
|
+
try {
|
|
699
|
+
const body = await c.req.json();
|
|
700
|
+
const { executionId, stepName, error: error2, durationMs } = body;
|
|
701
|
+
db.prepare(`UPDATE workflow_steps
|
|
702
|
+
SET status = 'failed', error = ?, duration_ms = ?
|
|
703
|
+
WHERE execution_id = ? AND step_name = ?`).run(error2, durationMs, executionId, stepName);
|
|
704
|
+
return c.json({ success: true });
|
|
705
|
+
} catch (err) {
|
|
706
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
707
|
+
return c.json({ success: false, error: message }, 500);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
const completeHandler = async (c) => {
|
|
711
|
+
try {
|
|
712
|
+
const body = await c.req.json();
|
|
713
|
+
const { executionId, output } = body;
|
|
714
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
715
|
+
db.prepare(`UPDATE workflow_executions
|
|
716
|
+
SET status = 'completed', output = ?, completed_at = ?
|
|
717
|
+
WHERE id = ?`).run(JSON.stringify(output), now, executionId);
|
|
718
|
+
return c.json({ success: true });
|
|
719
|
+
} catch (err) {
|
|
720
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
721
|
+
return c.json({ success: false, error: message }, 500);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
const failHandler = async (c) => {
|
|
725
|
+
try {
|
|
726
|
+
const body = await c.req.json();
|
|
727
|
+
const { executionId, error: error2 } = body;
|
|
728
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
729
|
+
db.prepare(`UPDATE workflow_executions
|
|
730
|
+
SET status = 'failed', error = ?, completed_at = ?
|
|
731
|
+
WHERE id = ?`).run(error2, now, executionId);
|
|
732
|
+
return c.json({ success: true });
|
|
733
|
+
} catch (err) {
|
|
734
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
735
|
+
return c.json({ success: false, error: message }, 500);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
return {
|
|
739
|
+
triggerHandler,
|
|
740
|
+
statusHandler,
|
|
741
|
+
cancelHandler,
|
|
742
|
+
stepStartHandler,
|
|
743
|
+
stepCompleteHandler,
|
|
744
|
+
stepFailHandler,
|
|
745
|
+
completeHandler,
|
|
746
|
+
failHandler
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ../emulator/dist/services/mock-server.js
|
|
751
|
+
var DEFAULT_MOCK_SERVER_PORT = 4003;
|
|
752
|
+
async function startMockServer(dbManager2, config, options = {}) {
|
|
753
|
+
const app = new Hono();
|
|
754
|
+
if (config.db) {
|
|
755
|
+
const dbHandler = createDbHandler(dbManager2.getD1Database);
|
|
756
|
+
app.post("/db", dbHandler);
|
|
757
|
+
}
|
|
758
|
+
if (config.queue) {
|
|
759
|
+
const queueHandlers = createQueueHandlers(dbManager2.emulatorDb);
|
|
760
|
+
app.post("/queue/send", queueHandlers.sendHandler);
|
|
761
|
+
app.post("/queue/batch-send", queueHandlers.batchSendHandler);
|
|
762
|
+
app.post("/queue/receive", queueHandlers.receiveHandler);
|
|
763
|
+
app.post("/queue/ack", queueHandlers.ackHandler);
|
|
764
|
+
app.post("/queue/retry", queueHandlers.retryHandler);
|
|
765
|
+
}
|
|
766
|
+
if (config.workflow) {
|
|
767
|
+
const workflowHandlers = createWorkflowHandlers(dbManager2.emulatorDb, options.workerUrl);
|
|
768
|
+
app.post("/workflow/trigger", workflowHandlers.triggerHandler);
|
|
769
|
+
app.post("/workflow/status", workflowHandlers.statusHandler);
|
|
770
|
+
app.post("/workflow/cancel", workflowHandlers.cancelHandler);
|
|
771
|
+
app.post("/workflow/step/start", workflowHandlers.stepStartHandler);
|
|
772
|
+
app.post("/workflow/step/complete", workflowHandlers.stepCompleteHandler);
|
|
773
|
+
app.post("/workflow/step/fail", workflowHandlers.stepFailHandler);
|
|
774
|
+
app.post("/workflow/complete", workflowHandlers.completeHandler);
|
|
775
|
+
app.post("/workflow/fail", workflowHandlers.failHandler);
|
|
776
|
+
}
|
|
777
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
778
|
+
if (options.dashboardEnabled !== false) {
|
|
779
|
+
createDashboardRoutes(app, dbManager2, config);
|
|
780
|
+
}
|
|
781
|
+
const serverPort = options.port ?? DEFAULT_MOCK_SERVER_PORT;
|
|
782
|
+
return await new Promise((resolve) => {
|
|
783
|
+
const server = serve({
|
|
784
|
+
fetch: app.fetch,
|
|
785
|
+
port: serverPort
|
|
786
|
+
}, (info) => {
|
|
787
|
+
resolve({
|
|
788
|
+
port: info.port,
|
|
789
|
+
close: () => new Promise((res) => {
|
|
790
|
+
server.close(() => res());
|
|
791
|
+
})
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
function getDataDir(projectDir) {
|
|
797
|
+
return join(projectDir, ".ploy");
|
|
798
|
+
}
|
|
799
|
+
function ensureDir(dir) {
|
|
800
|
+
mkdirSync(dir, { recursive: true });
|
|
801
|
+
}
|
|
802
|
+
function ensureDataDir(projectDir) {
|
|
803
|
+
const dataDir = getDataDir(projectDir);
|
|
804
|
+
ensureDir(dataDir);
|
|
805
|
+
ensureDir(join(dataDir, "db"));
|
|
806
|
+
return dataDir;
|
|
807
|
+
}
|
|
808
|
+
var EMULATOR_SCHEMA = `
|
|
809
|
+
-- Queue messages table
|
|
810
|
+
CREATE TABLE IF NOT EXISTS queue_messages (
|
|
811
|
+
id TEXT PRIMARY KEY,
|
|
812
|
+
queue_name TEXT NOT NULL,
|
|
813
|
+
payload TEXT NOT NULL,
|
|
814
|
+
status TEXT DEFAULT 'pending',
|
|
815
|
+
attempt INTEGER DEFAULT 0,
|
|
816
|
+
delivery_id TEXT,
|
|
817
|
+
visible_at INTEGER NOT NULL,
|
|
818
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
819
|
+
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
CREATE INDEX IF NOT EXISTS idx_queue_messages_status
|
|
823
|
+
ON queue_messages(queue_name, status, visible_at);
|
|
824
|
+
|
|
825
|
+
-- Workflow executions table
|
|
826
|
+
CREATE TABLE IF NOT EXISTS workflow_executions (
|
|
827
|
+
id TEXT PRIMARY KEY,
|
|
828
|
+
workflow_name TEXT NOT NULL,
|
|
829
|
+
status TEXT DEFAULT 'pending',
|
|
830
|
+
input TEXT,
|
|
831
|
+
output TEXT,
|
|
832
|
+
error TEXT,
|
|
833
|
+
started_at INTEGER,
|
|
834
|
+
completed_at INTEGER,
|
|
835
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_executions_status
|
|
839
|
+
ON workflow_executions(workflow_name, status);
|
|
840
|
+
|
|
841
|
+
-- Workflow steps table
|
|
842
|
+
CREATE TABLE IF NOT EXISTS workflow_steps (
|
|
843
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
844
|
+
execution_id TEXT NOT NULL,
|
|
845
|
+
step_name TEXT NOT NULL,
|
|
846
|
+
step_index INTEGER NOT NULL,
|
|
847
|
+
status TEXT DEFAULT 'pending',
|
|
848
|
+
output TEXT,
|
|
849
|
+
error TEXT,
|
|
850
|
+
duration_ms INTEGER,
|
|
851
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
852
|
+
FOREIGN KEY (execution_id) REFERENCES workflow_executions(id)
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_steps_execution
|
|
856
|
+
ON workflow_steps(execution_id, step_index);
|
|
857
|
+
`;
|
|
858
|
+
function initializeDatabases(projectDir) {
|
|
859
|
+
const dataDir = ensureDataDir(projectDir);
|
|
860
|
+
const d1Databases = /* @__PURE__ */ new Map();
|
|
861
|
+
const emulatorDb = new Database(join(dataDir, "emulator.db"));
|
|
862
|
+
emulatorDb.pragma("journal_mode = WAL");
|
|
863
|
+
emulatorDb.exec(EMULATOR_SCHEMA);
|
|
864
|
+
function getD1Database(bindingName) {
|
|
865
|
+
let db = d1Databases.get(bindingName);
|
|
866
|
+
if (!db) {
|
|
867
|
+
db = new Database(join(dataDir, "db", `${bindingName}.db`));
|
|
868
|
+
db.pragma("journal_mode = WAL");
|
|
869
|
+
d1Databases.set(bindingName, db);
|
|
870
|
+
}
|
|
871
|
+
return db;
|
|
872
|
+
}
|
|
873
|
+
function close() {
|
|
874
|
+
emulatorDb.close();
|
|
875
|
+
for (const db of d1Databases.values()) {
|
|
876
|
+
db.close();
|
|
877
|
+
}
|
|
878
|
+
d1Databases.clear();
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
emulatorDb,
|
|
882
|
+
getD1Database,
|
|
883
|
+
close
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// ../emulator/dist/nextjs-dev.js
|
|
888
|
+
var PLOY_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("__ploy-context__");
|
|
889
|
+
function createDevD1(databaseId, apiUrl) {
|
|
890
|
+
return {
|
|
891
|
+
async dump() {
|
|
892
|
+
const response = await fetch(`${apiUrl}/db`, {
|
|
893
|
+
method: "POST",
|
|
894
|
+
headers: { "Content-Type": "application/json" },
|
|
895
|
+
body: JSON.stringify({
|
|
896
|
+
databaseId,
|
|
897
|
+
method: "dump"
|
|
898
|
+
})
|
|
899
|
+
});
|
|
900
|
+
if (!response.ok) {
|
|
901
|
+
throw new Error(`DB dump failed: ${await response.text()}`);
|
|
902
|
+
}
|
|
903
|
+
return await response.arrayBuffer();
|
|
904
|
+
},
|
|
905
|
+
prepare(query) {
|
|
906
|
+
let boundParams = [];
|
|
907
|
+
const stmt = {
|
|
908
|
+
bind(...values) {
|
|
909
|
+
boundParams = values;
|
|
910
|
+
return stmt;
|
|
911
|
+
},
|
|
912
|
+
async run() {
|
|
913
|
+
const response = await fetch(`${apiUrl}/db`, {
|
|
914
|
+
method: "POST",
|
|
915
|
+
headers: { "Content-Type": "application/json" },
|
|
916
|
+
body: JSON.stringify({
|
|
917
|
+
databaseId,
|
|
918
|
+
method: "prepare",
|
|
919
|
+
query,
|
|
920
|
+
params: boundParams
|
|
921
|
+
})
|
|
922
|
+
});
|
|
923
|
+
if (!response.ok) {
|
|
924
|
+
throw new Error(`DB query failed: ${await response.text()}`);
|
|
925
|
+
}
|
|
926
|
+
return await response.json();
|
|
927
|
+
},
|
|
928
|
+
async all() {
|
|
929
|
+
return await stmt.run();
|
|
930
|
+
},
|
|
931
|
+
async first(colName) {
|
|
932
|
+
const result = await stmt.run();
|
|
933
|
+
if (result.results.length === 0) {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
const firstRow = result.results[0];
|
|
937
|
+
if (colName) {
|
|
938
|
+
return firstRow[colName] ?? null;
|
|
939
|
+
}
|
|
940
|
+
return firstRow;
|
|
941
|
+
},
|
|
942
|
+
async raw() {
|
|
943
|
+
const result = await stmt.run();
|
|
944
|
+
if (result.results.length === 0) {
|
|
945
|
+
return [];
|
|
946
|
+
}
|
|
947
|
+
const keys = Object.keys(result.results[0]);
|
|
948
|
+
return result.results.map((row) => keys.map((k) => row[k]));
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
return stmt;
|
|
952
|
+
},
|
|
953
|
+
async exec(query) {
|
|
954
|
+
const response = await fetch(`${apiUrl}/db`, {
|
|
955
|
+
method: "POST",
|
|
956
|
+
headers: { "Content-Type": "application/json" },
|
|
957
|
+
body: JSON.stringify({
|
|
958
|
+
databaseId,
|
|
959
|
+
method: "exec",
|
|
960
|
+
query
|
|
961
|
+
})
|
|
962
|
+
});
|
|
963
|
+
if (!response.ok) {
|
|
964
|
+
throw new Error(`DB exec failed: ${await response.text()}`);
|
|
965
|
+
}
|
|
966
|
+
return await response.json();
|
|
967
|
+
},
|
|
968
|
+
async batch(statements) {
|
|
969
|
+
const stmts = statements.map((s) => {
|
|
970
|
+
const data = s.__db_data;
|
|
971
|
+
return data || s;
|
|
972
|
+
});
|
|
973
|
+
const response = await fetch(`${apiUrl}/db`, {
|
|
974
|
+
method: "POST",
|
|
975
|
+
headers: { "Content-Type": "application/json" },
|
|
976
|
+
body: JSON.stringify({
|
|
977
|
+
databaseId,
|
|
978
|
+
method: "batch",
|
|
979
|
+
statements: stmts
|
|
980
|
+
})
|
|
981
|
+
});
|
|
982
|
+
if (!response.ok) {
|
|
983
|
+
throw new Error(`DB batch failed: ${await response.text()}`);
|
|
984
|
+
}
|
|
985
|
+
return await response.json();
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
var mockServer = null;
|
|
990
|
+
var dbManager = null;
|
|
991
|
+
async function initPloyForDev(config) {
|
|
992
|
+
if (process.env.NODE_ENV !== "development") {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (globalThis.__PLOY_DEV_INITIALIZED__) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
globalThis.__PLOY_DEV_INITIALIZED__ = true;
|
|
999
|
+
const configPath = config?.configPath || "./ploy.yaml";
|
|
1000
|
+
const projectDir = process.cwd();
|
|
1001
|
+
let ployConfig;
|
|
1002
|
+
try {
|
|
1003
|
+
ployConfig = readPloyConfig(projectDir, configPath);
|
|
1004
|
+
} catch {
|
|
1005
|
+
if (config?.bindings?.db) {
|
|
1006
|
+
ployConfig = { db: config.bindings.db };
|
|
1007
|
+
} else {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (config?.bindings?.db) {
|
|
1012
|
+
ployConfig = { ...ployConfig, db: config.bindings.db };
|
|
1013
|
+
}
|
|
1014
|
+
if (!ployConfig.db || Object.keys(ployConfig.db).length === 0) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
ensureDataDir(projectDir);
|
|
1018
|
+
dbManager = initializeDatabases(projectDir);
|
|
1019
|
+
mockServer = await startMockServer(dbManager, ployConfig, {});
|
|
1020
|
+
const apiUrl = `http://localhost:${mockServer.port}`;
|
|
1021
|
+
const env = {};
|
|
1022
|
+
for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
|
|
1023
|
+
env[bindingName] = createDevD1(databaseId, apiUrl);
|
|
1024
|
+
}
|
|
1025
|
+
const context = {
|
|
1026
|
+
env,
|
|
1027
|
+
cf: void 0,
|
|
1028
|
+
ctx: void 0
|
|
1029
|
+
};
|
|
1030
|
+
globalThis.__PLOY_DEV_CONTEXT__ = context;
|
|
1031
|
+
Object.defineProperty(globalThis, PLOY_CONTEXT_SYMBOL, {
|
|
1032
|
+
get() {
|
|
1033
|
+
return context;
|
|
1034
|
+
},
|
|
1035
|
+
configurable: true
|
|
1036
|
+
});
|
|
1037
|
+
const bindingNames = Object.keys(env);
|
|
1038
|
+
console.log(`[Ploy] Development context initialized with bindings: ${bindingNames.join(", ")}`);
|
|
1039
|
+
console.log(`[Ploy] Mock server running at ${apiUrl}`);
|
|
1040
|
+
const cleanup = async () => {
|
|
1041
|
+
if (mockServer) {
|
|
1042
|
+
await mockServer.close();
|
|
1043
|
+
mockServer = null;
|
|
1044
|
+
}
|
|
1045
|
+
if (dbManager) {
|
|
1046
|
+
dbManager.close();
|
|
1047
|
+
dbManager = null;
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
process.on("exit", () => {
|
|
1051
|
+
if (dbManager) {
|
|
1052
|
+
dbManager.close();
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
process.on("SIGINT", async () => {
|
|
1056
|
+
await cleanup();
|
|
1057
|
+
process.exit(0);
|
|
1058
|
+
});
|
|
1059
|
+
process.on("SIGTERM", async () => {
|
|
1060
|
+
await cleanup();
|
|
1061
|
+
process.exit(0);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
export { initPloyForDev };
|