@meetploy/cli 1.20.0 → 1.22.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/README.md +203 -0
- package/dist/dashboard-dist/assets/main-DKimSd85.js +354 -0
- package/dist/dashboard-dist/assets/main-kKaL2zMb.css +1 -0
- package/dist/dashboard-dist/index.html +2 -2
- package/dist/dev.js +246 -41
- package/dist/index.js +320 -62
- package/package.json +1 -1
- package/dist/dashboard-dist/assets/main-5Kt9I_hM.css +0 -1
- package/dist/dashboard-dist/assets/main-CZRtZPnP.js +0 -354
package/dist/index.js
CHANGED
|
@@ -118,6 +118,15 @@ var init_package_manager = __esm({
|
|
|
118
118
|
"../tools/dist/package-manager.js"() {
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
|
+
function getWorkflowName(value) {
|
|
122
|
+
return typeof value === "string" ? value : value.name;
|
|
123
|
+
}
|
|
124
|
+
function getWorkflowTimeout(value) {
|
|
125
|
+
return typeof value === "string" ? void 0 : value.timeout;
|
|
126
|
+
}
|
|
127
|
+
function getWorkflowStepTimeout(value) {
|
|
128
|
+
return typeof value === "string" ? void 0 : value.step_timeout;
|
|
129
|
+
}
|
|
121
130
|
function getCompatibilityFlags(config) {
|
|
122
131
|
if (!config.compatibility_flags || config.compatibility_flags.length === 0) {
|
|
123
132
|
return DEFAULT_COMPATIBILITY_FLAGS;
|
|
@@ -163,6 +172,47 @@ function validateCronBindings(bindings, configFile) {
|
|
|
163
172
|
}
|
|
164
173
|
}
|
|
165
174
|
}
|
|
175
|
+
function validateWorkflowBindings(bindings, configFile) {
|
|
176
|
+
if (bindings === void 0) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const [bindingName, value] of Object.entries(bindings)) {
|
|
180
|
+
if (!BINDING_NAME_REGEX.test(bindingName)) {
|
|
181
|
+
throw new Error(`Invalid workflow binding name '${bindingName}' in ${configFile}. Binding names must be uppercase with underscores (e.g., ORDER_FLOW)`);
|
|
182
|
+
}
|
|
183
|
+
if (typeof value === "string") {
|
|
184
|
+
if (!RESOURCE_NAME_REGEX.test(value)) {
|
|
185
|
+
throw new Error(`Invalid workflow resource name '${value}' for binding '${bindingName}' in ${configFile}. Resource names must be lowercase with underscores (e.g., order_processing)`);
|
|
186
|
+
}
|
|
187
|
+
} else if (typeof value === "object" && value !== null) {
|
|
188
|
+
if (typeof value.name !== "string") {
|
|
189
|
+
throw new Error(`Workflow binding '${bindingName}' in ${configFile} must have a 'name' field`);
|
|
190
|
+
}
|
|
191
|
+
if (!RESOURCE_NAME_REGEX.test(value.name)) {
|
|
192
|
+
throw new Error(`Invalid workflow resource name '${value.name}' for binding '${bindingName}' in ${configFile}. Resource names must be lowercase with underscores (e.g., order_processing)`);
|
|
193
|
+
}
|
|
194
|
+
if (value.timeout !== void 0 && (typeof value.timeout !== "number" || !Number.isFinite(value.timeout) || value.timeout <= 0)) {
|
|
195
|
+
throw new Error(`'timeout' for workflow binding '${bindingName}' in ${configFile} must be a positive number (seconds)`);
|
|
196
|
+
}
|
|
197
|
+
if (value.timeout !== void 0 && value.timeout > MAX_WORKFLOW_TIMEOUT_S) {
|
|
198
|
+
throw new Error(`'timeout' for workflow binding '${bindingName}' in ${configFile} exceeds maximum of ${MAX_WORKFLOW_TIMEOUT_S} seconds (60 minutes)`);
|
|
199
|
+
}
|
|
200
|
+
if (value.step_timeout !== void 0) {
|
|
201
|
+
if (typeof value.step_timeout !== "number" || !Number.isFinite(value.step_timeout) || value.step_timeout <= 0) {
|
|
202
|
+
throw new Error(`'step_timeout' for workflow binding '${bindingName}' in ${configFile} must be a positive number (seconds)`);
|
|
203
|
+
}
|
|
204
|
+
if (value.step_timeout > MAX_WORKFLOW_TIMEOUT_S) {
|
|
205
|
+
throw new Error(`'step_timeout' for workflow binding '${bindingName}' in ${configFile} exceeds maximum of ${MAX_WORKFLOW_TIMEOUT_S} seconds (60 minutes)`);
|
|
206
|
+
}
|
|
207
|
+
if (value.timeout !== void 0 && value.step_timeout > value.timeout) {
|
|
208
|
+
throw new Error(`'step_timeout' for workflow binding '${bindingName}' in ${configFile} cannot exceed its 'timeout' of ${value.timeout} seconds`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
throw new Error(`Workflow binding '${bindingName}' in ${configFile} must be a string or object with 'name'`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
166
216
|
function validateRelativePath(path, fieldName, configFile) {
|
|
167
217
|
if (path === void 0) {
|
|
168
218
|
return void 0;
|
|
@@ -217,7 +267,7 @@ function validatePloyConfig(config, configFile = "ploy.yaml", options = {}) {
|
|
|
217
267
|
validateBindings(config.cache, "cache", configFile);
|
|
218
268
|
validateBindings(config.state, "state", configFile);
|
|
219
269
|
validateBindings(config.fs, "fs", configFile);
|
|
220
|
-
|
|
270
|
+
validateWorkflowBindings(config.workflow, configFile);
|
|
221
271
|
validateCronBindings(config.cron, configFile);
|
|
222
272
|
validateBindings(config.timer, "timer", configFile);
|
|
223
273
|
if (config.ai !== void 0 && typeof config.ai !== "boolean") {
|
|
@@ -357,10 +407,13 @@ function getWorkerEntryPoint(projectDir, config) {
|
|
|
357
407
|
}
|
|
358
408
|
throw new Error("Could not find worker entry point. Specify 'main' in ploy.yaml");
|
|
359
409
|
}
|
|
360
|
-
var readFileAsync, DEFAULT_COMPATIBILITY_FLAGS, DEFAULT_COMPATIBILITY_DATE, BINDING_NAME_REGEX, RESOURCE_NAME_REGEX, CRON_EXPRESSION_REGEX;
|
|
410
|
+
var readFileAsync, MAX_WORKFLOW_TIMEOUT_S, DEFAULT_WORKFLOW_TIMEOUT_S, DEFAULT_STEP_TIMEOUT_S, DEFAULT_COMPATIBILITY_FLAGS, DEFAULT_COMPATIBILITY_DATE, BINDING_NAME_REGEX, RESOURCE_NAME_REGEX, CRON_EXPRESSION_REGEX;
|
|
361
411
|
var init_ploy_config = __esm({
|
|
362
412
|
"../tools/dist/ploy-config.js"() {
|
|
363
413
|
readFileAsync = promisify(readFile$1);
|
|
414
|
+
MAX_WORKFLOW_TIMEOUT_S = 60 * 60;
|
|
415
|
+
DEFAULT_WORKFLOW_TIMEOUT_S = 10 * 60;
|
|
416
|
+
DEFAULT_STEP_TIMEOUT_S = 2 * 60;
|
|
364
417
|
DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat_v2"];
|
|
365
418
|
DEFAULT_COMPATIBILITY_DATE = "2025-04-02";
|
|
366
419
|
BINDING_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
|
|
@@ -374,12 +427,18 @@ var cli_exports = {};
|
|
|
374
427
|
__export(cli_exports, {
|
|
375
428
|
DEFAULT_COMPATIBILITY_DATE: () => DEFAULT_COMPATIBILITY_DATE,
|
|
376
429
|
DEFAULT_COMPATIBILITY_FLAGS: () => DEFAULT_COMPATIBILITY_FLAGS,
|
|
430
|
+
DEFAULT_STEP_TIMEOUT_S: () => DEFAULT_STEP_TIMEOUT_S,
|
|
431
|
+
DEFAULT_WORKFLOW_TIMEOUT_S: () => DEFAULT_WORKFLOW_TIMEOUT_S,
|
|
432
|
+
MAX_WORKFLOW_TIMEOUT_S: () => MAX_WORKFLOW_TIMEOUT_S,
|
|
377
433
|
detectMonorepo: () => detectMonorepo,
|
|
378
434
|
getAddDevDependencyCommand: () => getAddDevDependencyCommand,
|
|
379
435
|
getCompatibilityDate: () => getCompatibilityDate,
|
|
380
436
|
getCompatibilityFlags: () => getCompatibilityFlags,
|
|
381
437
|
getRunCommand: () => getRunCommand,
|
|
382
438
|
getWorkerEntryPoint: () => getWorkerEntryPoint,
|
|
439
|
+
getWorkflowName: () => getWorkflowName,
|
|
440
|
+
getWorkflowStepTimeout: () => getWorkflowStepTimeout,
|
|
441
|
+
getWorkflowTimeout: () => getWorkflowTimeout,
|
|
383
442
|
hasBindings: () => hasBindings,
|
|
384
443
|
isPnpmWorkspace: () => isPnpmWorkspace,
|
|
385
444
|
loadDotEnvSync: () => loadDotEnvSync,
|
|
@@ -398,6 +457,26 @@ var init_cli = __esm({
|
|
|
398
457
|
}
|
|
399
458
|
});
|
|
400
459
|
|
|
460
|
+
// ../emulator/dist/config/ploy-config.js
|
|
461
|
+
function readPloyConfig2(projectDir, configPath) {
|
|
462
|
+
const config = readPloyConfigSync(projectDir, configPath);
|
|
463
|
+
if (!config.kind) {
|
|
464
|
+
throw new Error(`Missing required field 'kind' in ${configPath ?? "ploy.yaml"}`);
|
|
465
|
+
}
|
|
466
|
+
if (config.kind !== "dynamic" && config.kind !== "worker") {
|
|
467
|
+
throw new Error(`Invalid kind '${config.kind}' in ${configPath ?? "ploy.yaml"}. Must be 'dynamic' or 'worker'`);
|
|
468
|
+
}
|
|
469
|
+
return config;
|
|
470
|
+
}
|
|
471
|
+
function getWorkerEntryPoint2(projectDir, config) {
|
|
472
|
+
return getWorkerEntryPoint(projectDir, config);
|
|
473
|
+
}
|
|
474
|
+
var init_ploy_config2 = __esm({
|
|
475
|
+
"../emulator/dist/config/ploy-config.js"() {
|
|
476
|
+
init_cli();
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
401
480
|
// ../emulator/dist/runtime/cache-runtime.js
|
|
402
481
|
var CACHE_RUNTIME_CODE;
|
|
403
482
|
var init_cache_runtime = __esm({
|
|
@@ -458,10 +537,10 @@ export function initializeCache(cacheName: string, serviceUrl: string): CacheBin
|
|
|
458
537
|
}
|
|
459
538
|
});
|
|
460
539
|
|
|
461
|
-
// ../shared/dist/
|
|
540
|
+
// ../shared/dist/db-runtime.js
|
|
462
541
|
var DB_RUNTIME_CODE, DB_RUNTIME_CODE_PRODUCTION;
|
|
463
|
-
var
|
|
464
|
-
"../shared/dist/
|
|
542
|
+
var init_db_runtime = __esm({
|
|
543
|
+
"../shared/dist/db-runtime.js"() {
|
|
465
544
|
DB_RUNTIME_CODE = `
|
|
466
545
|
interface DBResult {
|
|
467
546
|
results: unknown[];
|
|
@@ -481,14 +560,14 @@ interface DBPreparedStatement {
|
|
|
481
560
|
raw: (options?: { columnNames?: boolean }) => Promise<unknown[][]>;
|
|
482
561
|
}
|
|
483
562
|
|
|
484
|
-
interface
|
|
563
|
+
interface Database {
|
|
485
564
|
prepare: (query: string) => DBPreparedStatement;
|
|
486
565
|
dump: () => Promise<ArrayBuffer>;
|
|
487
566
|
batch: (statements: DBPreparedStatement[]) => Promise<DBResult[]>;
|
|
488
567
|
exec: (query: string) => Promise<DBResult>;
|
|
489
568
|
}
|
|
490
569
|
|
|
491
|
-
export function initializeDB(bindingName: string, serviceUrl: string):
|
|
570
|
+
export function initializeDB(bindingName: string, serviceUrl: string): Database {
|
|
492
571
|
return {
|
|
493
572
|
prepare(query: string): DBPreparedStatement {
|
|
494
573
|
let boundParams: unknown[] = [];
|
|
@@ -760,7 +839,7 @@ var init_url_validation = __esm({
|
|
|
760
839
|
// ../shared/dist/index.js
|
|
761
840
|
var init_dist = __esm({
|
|
762
841
|
"../shared/dist/index.js"() {
|
|
763
|
-
|
|
842
|
+
init_db_runtime();
|
|
764
843
|
init_error();
|
|
765
844
|
init_health_check();
|
|
766
845
|
init_trace_event();
|
|
@@ -771,7 +850,7 @@ var init_dist = __esm({
|
|
|
771
850
|
});
|
|
772
851
|
|
|
773
852
|
// ../emulator/dist/runtime/db-runtime.js
|
|
774
|
-
var
|
|
853
|
+
var init_db_runtime2 = __esm({
|
|
775
854
|
"../emulator/dist/runtime/db-runtime.js"() {
|
|
776
855
|
init_dist();
|
|
777
856
|
}
|
|
@@ -1333,7 +1412,8 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
|
|
|
1333
1412
|
}
|
|
1334
1413
|
if (config.workflow) {
|
|
1335
1414
|
imports.push('import { initializeWorkflow, createStepContext, executeWorkflow } from "__ploy_workflow_runtime__";');
|
|
1336
|
-
for (const [bindingName,
|
|
1415
|
+
for (const [bindingName, value] of Object.entries(config.workflow)) {
|
|
1416
|
+
const workflowName = getWorkflowName(value);
|
|
1337
1417
|
bindings.push(` ${bindingName}: initializeWorkflow("${workflowName}", "${mockServiceUrl}"),`);
|
|
1338
1418
|
}
|
|
1339
1419
|
}
|
|
@@ -1608,8 +1688,9 @@ async function bundleWorker(options) {
|
|
|
1608
1688
|
var NODE_BUILTINS;
|
|
1609
1689
|
var init_bundler = __esm({
|
|
1610
1690
|
"../emulator/dist/bundler/bundler.js"() {
|
|
1691
|
+
init_ploy_config2();
|
|
1611
1692
|
init_cache_runtime();
|
|
1612
|
-
|
|
1693
|
+
init_db_runtime2();
|
|
1613
1694
|
init_fs_runtime();
|
|
1614
1695
|
init_queue_runtime();
|
|
1615
1696
|
init_state_runtime();
|
|
@@ -1671,6 +1752,9 @@ function log(message) {
|
|
|
1671
1752
|
function success(message) {
|
|
1672
1753
|
console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.green}[ploy]${COLORS.reset} ${message}`);
|
|
1673
1754
|
}
|
|
1755
|
+
function warn(message) {
|
|
1756
|
+
console.log(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.yellow}[ploy]${COLORS.reset} ${message}`);
|
|
1757
|
+
}
|
|
1674
1758
|
function error(message) {
|
|
1675
1759
|
console.error(`${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.red}[ploy]${COLORS.reset} ${message}`);
|
|
1676
1760
|
}
|
|
@@ -1802,26 +1886,6 @@ var init_env = __esm({
|
|
|
1802
1886
|
init_cli();
|
|
1803
1887
|
}
|
|
1804
1888
|
});
|
|
1805
|
-
|
|
1806
|
-
// ../emulator/dist/config/ploy-config.js
|
|
1807
|
-
function readPloyConfig2(projectDir, configPath) {
|
|
1808
|
-
const config = readPloyConfigSync(projectDir, configPath);
|
|
1809
|
-
if (!config.kind) {
|
|
1810
|
-
throw new Error(`Missing required field 'kind' in ${configPath ?? "ploy.yaml"}`);
|
|
1811
|
-
}
|
|
1812
|
-
if (config.kind !== "dynamic" && config.kind !== "worker") {
|
|
1813
|
-
throw new Error(`Invalid kind '${config.kind}' in ${configPath ?? "ploy.yaml"}. Must be 'dynamic' or 'worker'`);
|
|
1814
|
-
}
|
|
1815
|
-
return config;
|
|
1816
|
-
}
|
|
1817
|
-
function getWorkerEntryPoint2(projectDir, config) {
|
|
1818
|
-
return getWorkerEntryPoint(projectDir, config);
|
|
1819
|
-
}
|
|
1820
|
-
var init_ploy_config2 = __esm({
|
|
1821
|
-
"../emulator/dist/config/ploy-config.js"() {
|
|
1822
|
-
init_cli();
|
|
1823
|
-
}
|
|
1824
|
-
});
|
|
1825
1889
|
function generateWorkerdConfig(options) {
|
|
1826
1890
|
const { port, mockServicePort } = options;
|
|
1827
1891
|
const services = [
|
|
@@ -2566,7 +2630,7 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2566
2630
|
return c.json({ error: "Query is required" }, 400);
|
|
2567
2631
|
}
|
|
2568
2632
|
try {
|
|
2569
|
-
const db = dbManager2.
|
|
2633
|
+
const db = dbManager2.getDatabase(resourceName);
|
|
2570
2634
|
const startTime = Date.now();
|
|
2571
2635
|
const stmt = db.prepare(query);
|
|
2572
2636
|
const isSelect = query.trim().toUpperCase().startsWith("SELECT");
|
|
@@ -2604,7 +2668,7 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2604
2668
|
return c.json({ error: `Database binding '${binding}' not found` }, 404);
|
|
2605
2669
|
}
|
|
2606
2670
|
try {
|
|
2607
|
-
const db = dbManager2.
|
|
2671
|
+
const db = dbManager2.getDatabase(resourceName);
|
|
2608
2672
|
const tables = db.prepare(`SELECT name FROM sqlite_master
|
|
2609
2673
|
WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%'
|
|
2610
2674
|
ORDER BY name`).all();
|
|
@@ -2623,7 +2687,7 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2623
2687
|
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
2624
2688
|
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
2625
2689
|
try {
|
|
2626
|
-
const db = dbManager2.
|
|
2690
|
+
const db = dbManager2.getDatabase(resourceName);
|
|
2627
2691
|
const columnsResult = db.prepare(`PRAGMA table_info("${tableName}")`).all();
|
|
2628
2692
|
const columns = columnsResult.map((col) => col.name);
|
|
2629
2693
|
const countResult = db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`).get();
|
|
@@ -2641,7 +2705,7 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2641
2705
|
return c.json({ error: `Database binding '${binding}' not found` }, 404);
|
|
2642
2706
|
}
|
|
2643
2707
|
try {
|
|
2644
|
-
const db = dbManager2.
|
|
2708
|
+
const db = dbManager2.getDatabase(resourceName);
|
|
2645
2709
|
const tablesResult = db.prepare(`SELECT name FROM sqlite_master
|
|
2646
2710
|
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
2647
2711
|
ORDER BY name`).all();
|
|
@@ -2794,13 +2858,50 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2794
2858
|
app.get("/api/workflow/:binding/executions", (c) => {
|
|
2795
2859
|
const binding = c.req.param("binding");
|
|
2796
2860
|
const workflowConfig = config.workflow?.[binding];
|
|
2797
|
-
const limit = parseInt(c.req.query("limit") ?? "20", 10);
|
|
2861
|
+
const limit = Math.min(Math.max(1, parseInt(c.req.query("limit") ?? "20", 10)), 100);
|
|
2862
|
+
const offset = Math.max(0, parseInt(c.req.query("offset") ?? "0", 10));
|
|
2863
|
+
const statusFilter = c.req.query("status")?.toLowerCase() ?? null;
|
|
2864
|
+
const sinceRaw = c.req.query("since") ?? null;
|
|
2798
2865
|
if (!workflowConfig) {
|
|
2799
2866
|
return c.json({ error: "Workflow not found" }, 404);
|
|
2800
2867
|
}
|
|
2868
|
+
const validStatuses = /* @__PURE__ */ new Set([
|
|
2869
|
+
"pending",
|
|
2870
|
+
"running",
|
|
2871
|
+
"completed",
|
|
2872
|
+
"failed",
|
|
2873
|
+
"cancelled"
|
|
2874
|
+
]);
|
|
2801
2875
|
try {
|
|
2802
2876
|
const db = dbManager2.emulatorDb;
|
|
2803
|
-
const workflowName = workflowConfig;
|
|
2877
|
+
const workflowName = getWorkflowName(workflowConfig);
|
|
2878
|
+
let sinceTs = null;
|
|
2879
|
+
if (sinceRaw) {
|
|
2880
|
+
const match = sinceRaw.match(/^(\d+)(s|m|h|d)?$/);
|
|
2881
|
+
if (match) {
|
|
2882
|
+
const val = parseInt(match[1], 10);
|
|
2883
|
+
const unit = match[2] ?? "s";
|
|
2884
|
+
const multipliers = {
|
|
2885
|
+
s: 1,
|
|
2886
|
+
m: 60,
|
|
2887
|
+
h: 3600,
|
|
2888
|
+
d: 86400
|
|
2889
|
+
};
|
|
2890
|
+
sinceTs = Math.floor(Date.now() / 1e3) - val * (multipliers[unit] ?? 1);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
const conditions = ["e.workflow_name = ?"];
|
|
2894
|
+
const queryArgs = [workflowName];
|
|
2895
|
+
if (statusFilter && validStatuses.has(statusFilter)) {
|
|
2896
|
+
conditions.push("e.status = ?");
|
|
2897
|
+
queryArgs.push(statusFilter);
|
|
2898
|
+
}
|
|
2899
|
+
if (sinceTs !== null) {
|
|
2900
|
+
conditions.push("e.created_at >= ?");
|
|
2901
|
+
queryArgs.push(sinceTs);
|
|
2902
|
+
}
|
|
2903
|
+
const where = conditions.join(" AND ");
|
|
2904
|
+
const countRow = db.prepare(`SELECT COUNT(*) as count FROM workflow_executions e WHERE ${where}`).get(...queryArgs);
|
|
2804
2905
|
const executions = db.prepare(`SELECT
|
|
2805
2906
|
e.id,
|
|
2806
2907
|
e.workflow_name,
|
|
@@ -2812,9 +2913,9 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2812
2913
|
(SELECT COUNT(*) FROM workflow_steps WHERE execution_id = e.id) as steps_count,
|
|
2813
2914
|
(SELECT COUNT(*) FROM workflow_steps WHERE execution_id = e.id AND status = 'completed') as steps_completed
|
|
2814
2915
|
FROM workflow_executions e
|
|
2815
|
-
WHERE
|
|
2916
|
+
WHERE ${where}
|
|
2816
2917
|
ORDER BY e.created_at DESC
|
|
2817
|
-
LIMIT ?`).all(
|
|
2918
|
+
LIMIT ? OFFSET ?`).all(...queryArgs, limit, offset);
|
|
2818
2919
|
return c.json({
|
|
2819
2920
|
executions: executions.map((e) => ({
|
|
2820
2921
|
id: e.id,
|
|
@@ -2826,7 +2927,10 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2826
2927
|
stepsCompleted: e.steps_completed,
|
|
2827
2928
|
errorMessage: e.error,
|
|
2828
2929
|
createdAt: new Date(e.created_at * 1e3).toISOString()
|
|
2829
|
-
}))
|
|
2930
|
+
})),
|
|
2931
|
+
total: countRow.count,
|
|
2932
|
+
limit,
|
|
2933
|
+
offset
|
|
2830
2934
|
});
|
|
2831
2935
|
} catch (err) {
|
|
2832
2936
|
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
@@ -3060,6 +3164,7 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
3060
3164
|
var __filename, __dirname, MIME_TYPES;
|
|
3061
3165
|
var init_dashboard_routes = __esm({
|
|
3062
3166
|
"../emulator/dist/services/dashboard-routes.js"() {
|
|
3167
|
+
init_ploy_config2();
|
|
3063
3168
|
__filename = fileURLToPath(import.meta.url);
|
|
3064
3169
|
__dirname = dirname(__filename);
|
|
3065
3170
|
MIME_TYPES = {
|
|
@@ -3742,7 +3847,7 @@ function createWorkflowHandlers(db, workerUrl) {
|
|
|
3742
3847
|
}
|
|
3743
3848
|
const now = Math.floor(Date.now() / 1e3);
|
|
3744
3849
|
if (existing) {
|
|
3745
|
-
db.prepare(`UPDATE workflow_steps SET status = 'running' WHERE execution_id = ? AND step_name = ?`).run(executionId, stepName);
|
|
3850
|
+
db.prepare(`UPDATE workflow_steps SET status = 'running', error = NULL, created_at = ? WHERE execution_id = ? AND step_name = ? AND step_index = ?`).run(now, executionId, stepName, stepIndex);
|
|
3746
3851
|
} else {
|
|
3747
3852
|
db.prepare(`INSERT INTO workflow_steps (execution_id, step_name, step_index, status, created_at)
|
|
3748
3853
|
VALUES (?, ?, ?, 'running', ?)`).run(executionId, stepName, stepIndex, now);
|
|
@@ -3756,10 +3861,16 @@ function createWorkflowHandlers(db, workerUrl) {
|
|
|
3756
3861
|
const stepCompleteHandler = async (c) => {
|
|
3757
3862
|
try {
|
|
3758
3863
|
const body = await c.req.json();
|
|
3759
|
-
const { executionId, stepName, output, durationMs } = body;
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3864
|
+
const { executionId, stepName, stepIndex, output, durationMs } = body;
|
|
3865
|
+
if (stepIndex !== void 0) {
|
|
3866
|
+
db.prepare(`UPDATE workflow_steps
|
|
3867
|
+
SET status = 'completed', output = ?, duration_ms = ?
|
|
3868
|
+
WHERE execution_id = ? AND step_name = ? AND step_index = ?`).run(JSON.stringify(output), durationMs, executionId, stepName, stepIndex);
|
|
3869
|
+
} else {
|
|
3870
|
+
db.prepare(`UPDATE workflow_steps
|
|
3871
|
+
SET status = 'completed', output = ?, duration_ms = ?
|
|
3872
|
+
WHERE execution_id = ? AND step_name = ?`).run(JSON.stringify(output), durationMs, executionId, stepName);
|
|
3873
|
+
}
|
|
3763
3874
|
return c.json({ success: true });
|
|
3764
3875
|
} catch (err) {
|
|
3765
3876
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3769,10 +3880,16 @@ function createWorkflowHandlers(db, workerUrl) {
|
|
|
3769
3880
|
const stepFailHandler = async (c) => {
|
|
3770
3881
|
try {
|
|
3771
3882
|
const body = await c.req.json();
|
|
3772
|
-
const { executionId, stepName, error: error2, durationMs } = body;
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3883
|
+
const { executionId, stepName, stepIndex, error: error2, durationMs } = body;
|
|
3884
|
+
if (stepIndex !== void 0) {
|
|
3885
|
+
db.prepare(`UPDATE workflow_steps
|
|
3886
|
+
SET status = 'failed', error = ?, duration_ms = ?
|
|
3887
|
+
WHERE execution_id = ? AND step_name = ? AND step_index = ?`).run(error2, durationMs, executionId, stepName, stepIndex);
|
|
3888
|
+
} else {
|
|
3889
|
+
db.prepare(`UPDATE workflow_steps
|
|
3890
|
+
SET status = 'failed', error = ?, duration_ms = ?
|
|
3891
|
+
WHERE execution_id = ? AND step_name = ?`).run(error2, durationMs, executionId, stepName);
|
|
3892
|
+
}
|
|
3776
3893
|
return c.json({ success: true });
|
|
3777
3894
|
} catch (err) {
|
|
3778
3895
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3818,14 +3935,124 @@ function createWorkflowHandlers(db, workerUrl) {
|
|
|
3818
3935
|
failHandler
|
|
3819
3936
|
};
|
|
3820
3937
|
}
|
|
3938
|
+
function startWorkflowTimeoutEnforcement(db, intervalMs = 5e3, timeouts) {
|
|
3939
|
+
let isRunning = false;
|
|
3940
|
+
return setInterval(() => void (async () => {
|
|
3941
|
+
if (isRunning) {
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
isRunning = true;
|
|
3945
|
+
try {
|
|
3946
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3947
|
+
const allTimeouts = [
|
|
3948
|
+
timeouts.defaultTimeoutS,
|
|
3949
|
+
...Object.values(timeouts.perWorkflow)
|
|
3950
|
+
];
|
|
3951
|
+
const minTimeoutS = Math.min(...allTimeouts);
|
|
3952
|
+
const queryThreshold = now - minTimeoutS;
|
|
3953
|
+
const timedOutCandidates = db.prepare(`SELECT id, workflow_name, started_at FROM workflow_executions
|
|
3954
|
+
WHERE status = 'running' AND started_at < ?`).all(queryThreshold);
|
|
3955
|
+
for (const execution of timedOutCandidates) {
|
|
3956
|
+
const timeoutS = timeouts.perWorkflow[execution.workflow_name] ?? timeouts.defaultTimeoutS;
|
|
3957
|
+
const threshold = now - timeoutS;
|
|
3958
|
+
if (execution.started_at >= threshold) {
|
|
3959
|
+
continue;
|
|
3960
|
+
}
|
|
3961
|
+
db.prepare(`UPDATE workflow_executions
|
|
3962
|
+
SET status = 'failed', error = ?, completed_at = ?
|
|
3963
|
+
WHERE id = ? AND status = 'running'`).run(`Workflow exceeded timeout of ${timeoutS}s`, now, execution.id);
|
|
3964
|
+
}
|
|
3965
|
+
const allStepTimeouts = [
|
|
3966
|
+
timeouts.defaultStepTimeoutS,
|
|
3967
|
+
...Object.values(timeouts.perWorkflowStep)
|
|
3968
|
+
];
|
|
3969
|
+
const minStepTimeoutS = Math.min(...allStepTimeouts);
|
|
3970
|
+
const stepQueryThreshold = now - minStepTimeoutS;
|
|
3971
|
+
const timedOutSteps = db.prepare(`SELECT ws.id, ws.execution_id, ws.step_name, ws.step_index, ws.created_at, we.workflow_name
|
|
3972
|
+
FROM workflow_steps ws
|
|
3973
|
+
JOIN workflow_executions we ON we.id = ws.execution_id
|
|
3974
|
+
WHERE ws.status = 'running' AND ws.created_at < ?`).all(stepQueryThreshold);
|
|
3975
|
+
for (const step of timedOutSteps) {
|
|
3976
|
+
const stepTimeoutS = timeouts.perWorkflowStep[step.workflow_name] ?? timeouts.defaultStepTimeoutS;
|
|
3977
|
+
const stepThreshold = now - stepTimeoutS;
|
|
3978
|
+
if (step.created_at >= stepThreshold) {
|
|
3979
|
+
continue;
|
|
3980
|
+
}
|
|
3981
|
+
db.transaction(() => {
|
|
3982
|
+
db.prepare(`UPDATE workflow_steps
|
|
3983
|
+
SET status = 'failed', error = ?
|
|
3984
|
+
WHERE id = ? AND status = 'running'`).run(`Step exceeded timeout of ${stepTimeoutS}s`, step.id);
|
|
3985
|
+
db.prepare(`UPDATE workflow_executions
|
|
3986
|
+
SET status = 'failed', error = ?, completed_at = ?
|
|
3987
|
+
WHERE id = ? AND status = 'running'`).run(`Step '${step.step_name}' exceeded timeout of ${stepTimeoutS}s`, now, step.execution_id);
|
|
3988
|
+
})();
|
|
3989
|
+
}
|
|
3990
|
+
} catch (err) {
|
|
3991
|
+
warn(`Failed to enforce workflow timeouts: ${err instanceof Error ? err.message : String(err)}`);
|
|
3992
|
+
} finally {
|
|
3993
|
+
isRunning = false;
|
|
3994
|
+
}
|
|
3995
|
+
})(), intervalMs);
|
|
3996
|
+
}
|
|
3997
|
+
function startStalledWorkflowRecovery(db, workerUrl, intervalMs = 5e3, stallTimeoutS = DEFAULT_STALL_TIMEOUT_S) {
|
|
3998
|
+
let isRunning = false;
|
|
3999
|
+
return setInterval(() => void (async () => {
|
|
4000
|
+
if (isRunning) {
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
isRunning = true;
|
|
4004
|
+
try {
|
|
4005
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4006
|
+
const queryThreshold = now - stallTimeoutS;
|
|
4007
|
+
const stalledExecutions = db.prepare(`SELECT id, workflow_name, input, started_at FROM workflow_executions
|
|
4008
|
+
WHERE status = 'running' AND started_at < ?`).all(queryThreshold);
|
|
4009
|
+
for (const execution of stalledExecutions) {
|
|
4010
|
+
let input = null;
|
|
4011
|
+
if (execution.input) {
|
|
4012
|
+
try {
|
|
4013
|
+
input = JSON.parse(execution.input);
|
|
4014
|
+
} catch {
|
|
4015
|
+
db.prepare(`UPDATE workflow_executions SET status = 'failed', error = 'Malformed workflow input payload', completed_at = ? WHERE id = ?`).run(now, execution.id);
|
|
4016
|
+
continue;
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
db.prepare(`UPDATE workflow_steps SET status = 'pending', error = NULL
|
|
4020
|
+
WHERE execution_id = ? AND status = 'running'`).run(execution.id);
|
|
4021
|
+
try {
|
|
4022
|
+
const response = await fetch(workerUrl, {
|
|
4023
|
+
method: "POST",
|
|
4024
|
+
headers: {
|
|
4025
|
+
"Content-Type": "application/json",
|
|
4026
|
+
"X-Ploy-Workflow-Execution": "true",
|
|
4027
|
+
"X-Ploy-Workflow-Name": execution.workflow_name,
|
|
4028
|
+
"X-Ploy-Execution-Id": execution.id
|
|
4029
|
+
},
|
|
4030
|
+
body: JSON.stringify(input)
|
|
4031
|
+
});
|
|
4032
|
+
if (response.ok) {
|
|
4033
|
+
db.prepare(`UPDATE workflow_executions SET started_at = ? WHERE id = ?`).run(Math.floor(Date.now() / 1e3), execution.id);
|
|
4034
|
+
}
|
|
4035
|
+
} catch {
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
} catch (err) {
|
|
4039
|
+
warn(`Failed to recover stalled workflows: ${err instanceof Error ? err.message : String(err)}`);
|
|
4040
|
+
} finally {
|
|
4041
|
+
isRunning = false;
|
|
4042
|
+
}
|
|
4043
|
+
})(), intervalMs);
|
|
4044
|
+
}
|
|
4045
|
+
var DEFAULT_STALL_TIMEOUT_S;
|
|
3821
4046
|
var init_workflow_service = __esm({
|
|
3822
4047
|
"../emulator/dist/services/workflow-service.js"() {
|
|
4048
|
+
init_logger();
|
|
4049
|
+
DEFAULT_STALL_TIMEOUT_S = 100 * 60;
|
|
3823
4050
|
}
|
|
3824
4051
|
});
|
|
3825
4052
|
async function startMockServer(dbManager2, config, options = {}) {
|
|
3826
4053
|
const app = new Hono();
|
|
3827
4054
|
if (config.db) {
|
|
3828
|
-
const dbHandler = createDbHandler(dbManager2.
|
|
4055
|
+
const dbHandler = createDbHandler(dbManager2.getDatabase);
|
|
3829
4056
|
app.post("/db", dbHandler);
|
|
3830
4057
|
}
|
|
3831
4058
|
if (config.queue) {
|
|
@@ -3836,6 +4063,8 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
3836
4063
|
app.post("/queue/ack", queueHandlers.ackHandler);
|
|
3837
4064
|
app.post("/queue/retry", queueHandlers.retryHandler);
|
|
3838
4065
|
}
|
|
4066
|
+
let stalledRecoveryInterval;
|
|
4067
|
+
let timeoutEnforcementInterval;
|
|
3839
4068
|
if (config.workflow) {
|
|
3840
4069
|
const workflowHandlers = createWorkflowHandlers(dbManager2.emulatorDb, options.workerUrl);
|
|
3841
4070
|
app.post("/workflow/trigger", workflowHandlers.triggerHandler);
|
|
@@ -3846,6 +4075,28 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
3846
4075
|
app.post("/workflow/step/fail", workflowHandlers.stepFailHandler);
|
|
3847
4076
|
app.post("/workflow/complete", workflowHandlers.completeHandler);
|
|
3848
4077
|
app.post("/workflow/fail", workflowHandlers.failHandler);
|
|
4078
|
+
const perWorkflow = {};
|
|
4079
|
+
const perWorkflowStep = {};
|
|
4080
|
+
for (const [, value] of Object.entries(config.workflow)) {
|
|
4081
|
+
const workflowName = getWorkflowName(value);
|
|
4082
|
+
const timeout = getWorkflowTimeout(value);
|
|
4083
|
+
const stepTimeout = getWorkflowStepTimeout(value);
|
|
4084
|
+
if (timeout !== void 0) {
|
|
4085
|
+
perWorkflow[workflowName] = timeout;
|
|
4086
|
+
}
|
|
4087
|
+
if (stepTimeout !== void 0) {
|
|
4088
|
+
perWorkflowStep[workflowName] = stepTimeout;
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
timeoutEnforcementInterval = startWorkflowTimeoutEnforcement(dbManager2.emulatorDb, 5e3, {
|
|
4092
|
+
perWorkflow,
|
|
4093
|
+
perWorkflowStep,
|
|
4094
|
+
defaultTimeoutS: DEFAULT_WORKFLOW_TIMEOUT_S,
|
|
4095
|
+
defaultStepTimeoutS: DEFAULT_STEP_TIMEOUT_S
|
|
4096
|
+
});
|
|
4097
|
+
if (options.workerUrl) {
|
|
4098
|
+
stalledRecoveryInterval = startStalledWorkflowRecovery(dbManager2.emulatorDb, options.workerUrl, 5e3, DEFAULT_STALL_TIMEOUT_S);
|
|
4099
|
+
}
|
|
3849
4100
|
}
|
|
3850
4101
|
if (config.cache) {
|
|
3851
4102
|
const cacheHandlers = createCacheHandlers(dbManager2.emulatorDb);
|
|
@@ -3893,6 +4144,12 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
3893
4144
|
resolve({
|
|
3894
4145
|
port: info.port,
|
|
3895
4146
|
close: () => new Promise((res) => {
|
|
4147
|
+
if (stalledRecoveryInterval) {
|
|
4148
|
+
clearInterval(stalledRecoveryInterval);
|
|
4149
|
+
}
|
|
4150
|
+
if (timeoutEnforcementInterval) {
|
|
4151
|
+
clearInterval(timeoutEnforcementInterval);
|
|
4152
|
+
}
|
|
3896
4153
|
server.close(() => {
|
|
3897
4154
|
res();
|
|
3898
4155
|
});
|
|
@@ -3904,6 +4161,7 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
3904
4161
|
var DEFAULT_MOCK_SERVER_PORT;
|
|
3905
4162
|
var init_mock_server = __esm({
|
|
3906
4163
|
"../emulator/dist/services/mock-server.js"() {
|
|
4164
|
+
init_ploy_config2();
|
|
3907
4165
|
init_paths();
|
|
3908
4166
|
init_auth_service();
|
|
3909
4167
|
init_cache_service();
|
|
@@ -3980,29 +4238,29 @@ var init_sqlite_db = __esm({
|
|
|
3980
4238
|
});
|
|
3981
4239
|
function initializeDatabases(projectDir) {
|
|
3982
4240
|
const dataDir = ensureDataDir(projectDir);
|
|
3983
|
-
const
|
|
4241
|
+
const databases = /* @__PURE__ */ new Map();
|
|
3984
4242
|
const emulatorDb = openDatabase(join(dataDir, "emulator.db"));
|
|
3985
4243
|
emulatorDb.pragma("journal_mode = WAL");
|
|
3986
4244
|
emulatorDb.exec(EMULATOR_SCHEMA);
|
|
3987
|
-
function
|
|
3988
|
-
let db =
|
|
4245
|
+
function getDatabase(bindingName) {
|
|
4246
|
+
let db = databases.get(bindingName);
|
|
3989
4247
|
if (!db) {
|
|
3990
4248
|
db = openDatabase(join(dataDir, "db", `${bindingName}.db`));
|
|
3991
4249
|
db.pragma("journal_mode = WAL");
|
|
3992
|
-
|
|
4250
|
+
databases.set(bindingName, db);
|
|
3993
4251
|
}
|
|
3994
4252
|
return db;
|
|
3995
4253
|
}
|
|
3996
4254
|
function close() {
|
|
3997
4255
|
emulatorDb.close();
|
|
3998
|
-
for (const db of
|
|
4256
|
+
for (const db of databases.values()) {
|
|
3999
4257
|
db.close();
|
|
4000
4258
|
}
|
|
4001
|
-
|
|
4259
|
+
databases.clear();
|
|
4002
4260
|
}
|
|
4003
4261
|
return {
|
|
4004
4262
|
emulatorDb,
|
|
4005
|
-
|
|
4263
|
+
getDatabase,
|
|
4006
4264
|
close
|
|
4007
4265
|
};
|
|
4008
4266
|
}
|
|
@@ -4477,7 +4735,7 @@ var init_emulator = __esm({
|
|
|
4477
4735
|
});
|
|
4478
4736
|
|
|
4479
4737
|
// ../emulator/dist/nextjs-dev.js
|
|
4480
|
-
function
|
|
4738
|
+
function createDevDB(databaseId, apiUrl) {
|
|
4481
4739
|
return {
|
|
4482
4740
|
async dump() {
|
|
4483
4741
|
const response = await fetch(`${apiUrl}/db`, {
|
|
@@ -4642,7 +4900,7 @@ async function initPloyForDev(config) {
|
|
|
4642
4900
|
const env2 = {};
|
|
4643
4901
|
if (hasDbBindings2 && ployConfig2.db) {
|
|
4644
4902
|
for (const [bindingName, databaseId] of Object.entries(ployConfig2.db)) {
|
|
4645
|
-
env2[bindingName] =
|
|
4903
|
+
env2[bindingName] = createDevDB(databaseId, cliMockServerUrl);
|
|
4646
4904
|
}
|
|
4647
4905
|
}
|
|
4648
4906
|
if (hasAuthConfig2) {
|
|
@@ -4694,7 +4952,7 @@ async function initPloyForDev(config) {
|
|
|
4694
4952
|
const env = {};
|
|
4695
4953
|
if (hasDbBindings && ployConfig.db) {
|
|
4696
4954
|
for (const [bindingName, databaseId] of Object.entries(ployConfig.db)) {
|
|
4697
|
-
env[bindingName] =
|
|
4955
|
+
env[bindingName] = createDevDB(databaseId, apiUrl);
|
|
4698
4956
|
}
|
|
4699
4957
|
}
|
|
4700
4958
|
if (hasAuthConfig) {
|
|
@@ -4827,7 +5085,7 @@ var init_dist2 = __esm({
|
|
|
4827
5085
|
init_emulator();
|
|
4828
5086
|
init_nextjs_dev();
|
|
4829
5087
|
init_dev_dashboard();
|
|
4830
|
-
|
|
5088
|
+
init_db_runtime2();
|
|
4831
5089
|
}
|
|
4832
5090
|
});
|
|
4833
5091
|
var CONFIG_DIR = join(homedir(), ".ploy");
|
|
@@ -6316,7 +6574,7 @@ ${varProps}
|
|
|
6316
6574
|
}
|
|
6317
6575
|
if (config.db) {
|
|
6318
6576
|
for (const bindingName of Object.keys(config.db)) {
|
|
6319
|
-
properties.push(` ${bindingName}:
|
|
6577
|
+
properties.push(` ${bindingName}: Database;`);
|
|
6320
6578
|
}
|
|
6321
6579
|
}
|
|
6322
6580
|
if (config.queue) {
|