@ryanfw/prompt-orchestration-pipeline 0.0.1 → 0.4.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 +415 -24
- package/package.json +46 -8
- package/src/api/files.js +48 -0
- package/src/api/index.js +149 -53
- package/src/api/validators/seed.js +141 -0
- package/src/cli/index.js +444 -29
- package/src/cli/run-orchestrator.js +39 -0
- package/src/cli/update-pipeline-json.js +47 -0
- package/src/components/DAGGrid.jsx +649 -0
- package/src/components/JobCard.jsx +96 -0
- package/src/components/JobDetail.jsx +159 -0
- package/src/components/JobTable.jsx +202 -0
- package/src/components/Layout.jsx +134 -0
- package/src/components/TaskFilePane.jsx +570 -0
- package/src/components/UploadSeed.jsx +239 -0
- package/src/components/ui/badge.jsx +20 -0
- package/src/components/ui/button.jsx +43 -0
- package/src/components/ui/card.jsx +20 -0
- package/src/components/ui/focus-styles.css +60 -0
- package/src/components/ui/progress.jsx +26 -0
- package/src/components/ui/select.jsx +27 -0
- package/src/components/ui/separator.jsx +6 -0
- package/src/config/paths.js +99 -0
- package/src/core/config.js +270 -9
- package/src/core/file-io.js +202 -0
- package/src/core/module-loader.js +157 -0
- package/src/core/orchestrator.js +275 -294
- package/src/core/pipeline-runner.js +95 -41
- package/src/core/progress.js +66 -0
- package/src/core/status-writer.js +331 -0
- package/src/core/task-runner.js +719 -73
- package/src/core/validation.js +120 -1
- package/src/lib/utils.js +6 -0
- package/src/llm/README.md +139 -30
- package/src/llm/index.js +222 -72
- package/src/pages/PipelineDetail.jsx +111 -0
- package/src/pages/PromptPipelineDashboard.jsx +223 -0
- package/src/providers/deepseek.js +3 -15
- package/src/ui/client/adapters/job-adapter.js +258 -0
- package/src/ui/client/bootstrap.js +120 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +619 -0
- package/src/ui/client/hooks/useJobList.js +50 -0
- package/src/ui/client/hooks/useJobListWithUpdates.js +335 -0
- package/src/ui/client/hooks/useTicker.js +26 -0
- package/src/ui/client/index.css +31 -0
- package/src/ui/client/index.html +18 -0
- package/src/ui/client/main.jsx +38 -0
- package/src/ui/config-bridge.browser.js +149 -0
- package/src/ui/config-bridge.js +149 -0
- package/src/ui/config-bridge.node.js +310 -0
- package/src/ui/dist/assets/index-CxcrauYR.js +22702 -0
- package/src/ui/dist/assets/style-D6K_oQ12.css +62 -0
- package/src/ui/dist/index.html +19 -0
- package/src/ui/endpoints/job-endpoints.js +300 -0
- package/src/ui/file-reader.js +216 -0
- package/src/ui/job-change-detector.js +83 -0
- package/src/ui/job-index.js +231 -0
- package/src/ui/job-reader.js +274 -0
- package/src/ui/job-scanner.js +188 -0
- package/src/ui/public/app.js +3 -1
- package/src/ui/server.js +1636 -59
- package/src/ui/sse-enhancer.js +149 -0
- package/src/ui/sse.js +204 -0
- package/src/ui/state-snapshot.js +252 -0
- package/src/ui/transformers/list-transformer.js +347 -0
- package/src/ui/transformers/status-transformer.js +307 -0
- package/src/ui/watcher.js +61 -7
- package/src/utils/dag.js +101 -0
- package/src/utils/duration.js +126 -0
- package/src/utils/id-generator.js +30 -0
- package/src/utils/jobs.js +7 -0
- package/src/utils/pipelines.js +44 -0
- package/src/utils/task-files.js +271 -0
- package/src/utils/ui.jsx +76 -0
- package/src/ui/public/index.html +0 -53
- package/src/ui/public/style.css +0 -341
package/src/api/index.js
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { startOrchestrator } from "../core/orchestrator.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
-
import {
|
|
4
|
+
import { validateSeed } from "./validators/seed.js";
|
|
5
|
+
import { atomicWrite, cleanupOnFailure } from "./files.js";
|
|
6
|
+
import { getPipelineConfig } from "../core/config.js";
|
|
7
|
+
import {
|
|
8
|
+
getPendingSeedPath,
|
|
9
|
+
resolvePipelinePaths,
|
|
10
|
+
getJobDirectoryPath,
|
|
11
|
+
getJobMetadataPath,
|
|
12
|
+
getJobPipelinePath,
|
|
13
|
+
} from "../config/paths.js";
|
|
14
|
+
import { generateJobId } from "../utils/id-generator.js";
|
|
5
15
|
|
|
6
16
|
// Pure functional utilities
|
|
7
17
|
const createPaths = (config) => {
|
|
8
|
-
const {
|
|
9
|
-
rootDir,
|
|
10
|
-
dataDir = "pipeline-data",
|
|
11
|
-
configDir = "pipeline-config",
|
|
12
|
-
} = config;
|
|
18
|
+
const { rootDir, dataDir = "pipeline-data" } = config;
|
|
13
19
|
return {
|
|
14
20
|
pending: path.join(rootDir, dataDir, "pending"),
|
|
15
21
|
current: path.join(rootDir, dataDir, "current"),
|
|
16
22
|
complete: path.join(rootDir, dataDir, "complete"),
|
|
17
|
-
pipeline: path.join(rootDir, configDir, "pipeline.json"),
|
|
18
|
-
tasks: path.join(rootDir, configDir, "tasks"),
|
|
19
23
|
};
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
const validateConfig = (options = {}) => ({
|
|
23
27
|
rootDir: options.rootDir || process.cwd(),
|
|
24
28
|
dataDir: options.dataDir || "pipeline-data",
|
|
25
|
-
configDir: options.configDir || "pipeline-config",
|
|
26
29
|
autoStart: options.autoStart ?? true,
|
|
27
30
|
ui: options.ui ?? false,
|
|
28
31
|
uiPort: options.uiPort || 3000,
|
|
@@ -36,22 +39,10 @@ const ensureDirectories = async (paths) => {
|
|
|
36
39
|
}
|
|
37
40
|
};
|
|
38
41
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
definition.__path = pipelinePath;
|
|
44
|
-
return definition;
|
|
45
|
-
} catch (error) {
|
|
46
|
-
if (error.code === "ENOENT") {
|
|
47
|
-
throw new Error(`Pipeline definition not found at ${pipelinePath}`);
|
|
48
|
-
}
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const createOrchestrator = (paths, pipelineDefinition) =>
|
|
54
|
-
new Orchestrator({ paths, pipelineDefinition });
|
|
42
|
+
const createOrchestrator = (paths, pipelineDefinition, rootDir) =>
|
|
43
|
+
// Accept an explicit rootDir (project root) to avoid passing a subpath.
|
|
44
|
+
// Pipeline is mandatory - see docs/plans/multi-pipeline-backend-plan-PR2.md
|
|
45
|
+
startOrchestrator({ dataDir: rootDir || paths.pending });
|
|
55
46
|
|
|
56
47
|
// Main API functions
|
|
57
48
|
export const createPipelineOrchestrator = async (options = {}) => {
|
|
@@ -59,40 +50,39 @@ export const createPipelineOrchestrator = async (options = {}) => {
|
|
|
59
50
|
const paths = createPaths(config);
|
|
60
51
|
|
|
61
52
|
await ensureDirectories(paths);
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
|
|
54
|
+
// Pass config.rootDir as the orchestrator dataDir root so the orchestrator resolves
|
|
55
|
+
// pipeline-data/... correctly (avoids duplicate path segments).
|
|
56
|
+
const orchestrator = await createOrchestrator(
|
|
57
|
+
paths,
|
|
58
|
+
null, // unused - pipeline definition will be loaded per-job
|
|
59
|
+
config.rootDir
|
|
60
|
+
);
|
|
64
61
|
|
|
65
62
|
let uiServer = null;
|
|
66
63
|
|
|
67
64
|
const state = {
|
|
68
65
|
config,
|
|
69
66
|
paths,
|
|
70
|
-
pipelineDefinition,
|
|
67
|
+
pipelineDefinition: undefined, // TODO: multi-pipeline UI will provide per-slug previews from snapshots
|
|
71
68
|
orchestrator,
|
|
72
69
|
uiServer,
|
|
73
70
|
};
|
|
74
71
|
|
|
75
|
-
// Auto-start if configured
|
|
76
|
-
|
|
77
|
-
await orchestrator.start();
|
|
78
|
-
}
|
|
72
|
+
// Auto-start if configured (startOrchestrator handles this automatically)
|
|
73
|
+
// No need to call orchestrator.start() as startOrchestrator auto-starts when autoStart=true
|
|
79
74
|
|
|
80
75
|
// Start UI if configured
|
|
81
76
|
if (config.ui) {
|
|
82
|
-
const {
|
|
77
|
+
const { startServer } = await import("../ui/server.js");
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
getStatus: (jobName) => getStatus(state, jobName),
|
|
88
|
-
listJobs: (status) => listJobs(state, status),
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
uiServer = createUIServer(uiApi);
|
|
92
|
-
uiServer.listen(config.uiPort, () => {
|
|
93
|
-
console.log(`Pipeline UI available at http://localhost:${config.uiPort}`);
|
|
79
|
+
uiServer = await startServer({
|
|
80
|
+
dataDir: config.rootDir,
|
|
81
|
+
port: config.uiPort,
|
|
94
82
|
});
|
|
83
|
+
|
|
95
84
|
state.uiServer = uiServer;
|
|
85
|
+
console.log(`Pipeline UI available at ${uiServer.url}`);
|
|
96
86
|
}
|
|
97
87
|
|
|
98
88
|
return state;
|
|
@@ -100,13 +90,120 @@ export const createPipelineOrchestrator = async (options = {}) => {
|
|
|
100
90
|
|
|
101
91
|
// Job management functions
|
|
102
92
|
export const submitJob = async (state, seed) => {
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
throw new Error(
|
|
94
|
+
"submitJob is deprecated. Use submitJobWithValidation instead for ID-only job submission."
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Submit a job with comprehensive validation and atomic writes
|
|
100
|
+
* @param {Object} options - Options object
|
|
101
|
+
* @param {string} options.dataDir - Base data directory
|
|
102
|
+
* @param {Object} options.seedObject - Seed object to submit
|
|
103
|
+
* @returns {Promise<Object>} Result object with success status
|
|
104
|
+
*/
|
|
105
|
+
export const submitJobWithValidation = async ({ dataDir, seedObject }) => {
|
|
106
|
+
let partialFiles = [];
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Validate the seed object
|
|
110
|
+
const validatedSeed = await validateSeed(
|
|
111
|
+
JSON.stringify(seedObject),
|
|
112
|
+
dataDir
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Generate a random job ID
|
|
116
|
+
const jobId = generateJobId();
|
|
117
|
+
|
|
118
|
+
// Get the paths
|
|
119
|
+
const paths = resolvePipelinePaths(dataDir);
|
|
120
|
+
const pendingPath = getPendingSeedPath(dataDir, jobId);
|
|
121
|
+
const currentJobDir = getJobDirectoryPath(dataDir, jobId, "current");
|
|
122
|
+
const jobMetadataPath = getJobMetadataPath(dataDir, jobId, "current");
|
|
123
|
+
const jobPipelinePath = getJobPipelinePath(dataDir, jobId, "current");
|
|
124
|
+
|
|
125
|
+
// Ensure directories exist
|
|
126
|
+
await fs.mkdir(paths.pending, { recursive: true });
|
|
127
|
+
await fs.mkdir(currentJobDir, { recursive: true });
|
|
128
|
+
|
|
129
|
+
// Create job metadata
|
|
130
|
+
const jobMetadata = {
|
|
131
|
+
id: jobId,
|
|
132
|
+
name: validatedSeed.name,
|
|
133
|
+
pipeline: validatedSeed.pipeline, // Include pipeline slug
|
|
134
|
+
createdAt: new Date().toISOString(),
|
|
135
|
+
status: "pending",
|
|
136
|
+
};
|
|
105
137
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
138
|
+
// Read pipeline configuration for snapshot
|
|
139
|
+
let pipelineSnapshot = null;
|
|
140
|
+
try {
|
|
141
|
+
// Compute snapshot path from the seed-derived slug
|
|
142
|
+
const pipelineSlug = validatedSeed.pipeline;
|
|
143
|
+
const { pipelineJsonPath } = getPipelineConfig(pipelineSlug);
|
|
144
|
+
const pipelineContent = await fs.readFile(pipelineJsonPath, "utf8");
|
|
145
|
+
pipelineSnapshot = JSON.parse(pipelineContent);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Handle unknown pipeline slug
|
|
148
|
+
if (
|
|
149
|
+
error.message.includes("Unknown pipeline") ||
|
|
150
|
+
error.message.includes("not found")
|
|
151
|
+
) {
|
|
152
|
+
const errorMessage = "Unknown pipeline slug: " + validatedSeed.pipeline;
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
message: errorMessage,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// If pipeline config doesn't exist, create a minimal snapshot
|
|
159
|
+
pipelineSnapshot = {
|
|
160
|
+
tasks: [],
|
|
161
|
+
name: validatedSeed.pipeline,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Write files atomically
|
|
166
|
+
partialFiles.push(pendingPath);
|
|
167
|
+
await atomicWrite(pendingPath, JSON.stringify(validatedSeed, null, 2));
|
|
168
|
+
|
|
169
|
+
partialFiles.push(jobMetadataPath);
|
|
170
|
+
await atomicWrite(jobMetadataPath, JSON.stringify(jobMetadata, null, 2));
|
|
171
|
+
|
|
172
|
+
partialFiles.push(jobPipelinePath);
|
|
173
|
+
await atomicWrite(
|
|
174
|
+
jobPipelinePath,
|
|
175
|
+
JSON.stringify(pipelineSnapshot, null, 2)
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
success: true,
|
|
180
|
+
jobId,
|
|
181
|
+
jobName: validatedSeed.name,
|
|
182
|
+
message: "Seed file uploaded successfully",
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// Clean up any partial files on failure
|
|
186
|
+
for (const filePath of partialFiles) {
|
|
187
|
+
try {
|
|
188
|
+
await cleanupOnFailure(filePath);
|
|
189
|
+
} catch (cleanupError) {
|
|
190
|
+
// Ignore cleanup errors
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Map validation errors to appropriate error messages
|
|
195
|
+
let errorMessage = error.message;
|
|
196
|
+
if (error.message.includes("Invalid JSON")) {
|
|
197
|
+
errorMessage = "Invalid JSON";
|
|
198
|
+
} else if (error.message.includes("required")) {
|
|
199
|
+
errorMessage = "Required fields missing";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
message: errorMessage,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
110
207
|
};
|
|
111
208
|
|
|
112
209
|
export const getStatus = async (state, jobName) => {
|
|
@@ -166,13 +263,13 @@ export const listJobs = async (state, status = "all") => {
|
|
|
166
263
|
|
|
167
264
|
// Control functions
|
|
168
265
|
export const start = async (state) => {
|
|
169
|
-
|
|
266
|
+
// startOrchestrator already starts automatically, no need to call start
|
|
170
267
|
return state;
|
|
171
268
|
};
|
|
172
269
|
|
|
173
270
|
export const stop = async (state) => {
|
|
174
271
|
if (state.uiServer) {
|
|
175
|
-
await
|
|
272
|
+
await state.uiServer.close();
|
|
176
273
|
}
|
|
177
274
|
await state.orchestrator.stop();
|
|
178
275
|
return state;
|
|
@@ -215,6 +312,5 @@ export const PipelineOrchestrator = {
|
|
|
215
312
|
|
|
216
313
|
// Export the original functions for direct functional usage
|
|
217
314
|
export { runPipeline } from "../core/task-runner.js";
|
|
218
|
-
export { selectModel } from "../core/task-runner.js";
|
|
219
315
|
|
|
220
316
|
export default PipelineOrchestrator;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed validation utilities
|
|
3
|
+
* @module api/validators/seed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { getPipelineConfig } from "../../core/config.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate JSON string and parse it
|
|
12
|
+
* @param {string} jsonString - JSON string to validate
|
|
13
|
+
* @returns {Object} Parsed JSON object
|
|
14
|
+
* @throws {Error} With message containing "Invalid JSON" on parse failure
|
|
15
|
+
*/
|
|
16
|
+
function validateAndParseJson(jsonString) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(jsonString);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new Error("Invalid JSON");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate required fields in seed object
|
|
26
|
+
* @param {Object} seedObject - Seed object to validate
|
|
27
|
+
* @returns {Object} Validated seed object
|
|
28
|
+
* @throws {Error} With message containing "required" if fields are missing
|
|
29
|
+
*/
|
|
30
|
+
function validateRequiredFields(seedObject) {
|
|
31
|
+
if (
|
|
32
|
+
!seedObject.name ||
|
|
33
|
+
typeof seedObject.name !== "string" ||
|
|
34
|
+
seedObject.name.trim() === ""
|
|
35
|
+
) {
|
|
36
|
+
throw new Error("name field is required");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!seedObject.data || typeof seedObject.data !== "object") {
|
|
40
|
+
throw new Error("data field is required");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
!seedObject.pipeline ||
|
|
45
|
+
typeof seedObject.pipeline !== "string" ||
|
|
46
|
+
seedObject.pipeline.trim() === ""
|
|
47
|
+
) {
|
|
48
|
+
throw new Error("pipeline field is required");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return seedObject;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate name format (alphanumeric + -/_ allowed)
|
|
56
|
+
* @param {string} name - Name to validate
|
|
57
|
+
* @returns {string} Validated name
|
|
58
|
+
* @throws {Error} With message containing "required" if format is invalid
|
|
59
|
+
*/
|
|
60
|
+
function validateNameFormat(name) {
|
|
61
|
+
// Allow display-friendly names: required non-empty string, trim, length cap
|
|
62
|
+
if (!name || typeof name !== "string" || name.trim() === "") {
|
|
63
|
+
throw new Error("name field is required");
|
|
64
|
+
}
|
|
65
|
+
const trimmed = name.trim();
|
|
66
|
+
if (trimmed.length > 120) {
|
|
67
|
+
throw new Error("name must be 120 characters or less");
|
|
68
|
+
}
|
|
69
|
+
// Allow spaces and common punctuation for better UX
|
|
70
|
+
// Still disallow control characters and path traversal patterns
|
|
71
|
+
const dangerousPattern = /[\x00-\x1f\x7f-\x9f]/;
|
|
72
|
+
if (dangerousPattern.test(trimmed)) {
|
|
73
|
+
throw new Error("name must contain only printable characters");
|
|
74
|
+
}
|
|
75
|
+
return trimmed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a job with the given name already exists
|
|
80
|
+
* @param {string} baseDir - Base data directory
|
|
81
|
+
* @param {string} jobName - Job name to check
|
|
82
|
+
* @returns {Promise<boolean>} True if duplicate exists
|
|
83
|
+
*/
|
|
84
|
+
async function checkDuplicateJob(baseDir, jobName) {
|
|
85
|
+
const { getPendingSeedPath, getCurrentSeedPath, getCompleteSeedPath } =
|
|
86
|
+
await import("../../config/paths.js");
|
|
87
|
+
|
|
88
|
+
const paths = [
|
|
89
|
+
getPendingSeedPath(baseDir, jobName),
|
|
90
|
+
getCurrentSeedPath(baseDir, jobName),
|
|
91
|
+
getCompleteSeedPath(baseDir, jobName),
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
for (const filePath of paths) {
|
|
95
|
+
try {
|
|
96
|
+
await fs.access(filePath);
|
|
97
|
+
return true; // File exists, duplicate found
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// File doesn't exist, continue checking
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false; // No duplicates found
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Comprehensive seed validation
|
|
108
|
+
* @param {string} jsonString - JSON string to validate
|
|
109
|
+
* @param {string} baseDir - Base data directory for duplicate checking
|
|
110
|
+
* @returns {Promise<Object>} Validated seed object
|
|
111
|
+
* @throws {Error} With appropriate error message
|
|
112
|
+
*/
|
|
113
|
+
async function validateSeed(jsonString, baseDir) {
|
|
114
|
+
// Step 1: Validate and parse JSON
|
|
115
|
+
const seedObject = validateAndParseJson(jsonString);
|
|
116
|
+
|
|
117
|
+
// Step 2: Validate required fields
|
|
118
|
+
const validatedObject = validateRequiredFields(seedObject);
|
|
119
|
+
|
|
120
|
+
// Step 3: Validate name format
|
|
121
|
+
validateNameFormat(validatedObject.name);
|
|
122
|
+
|
|
123
|
+
// Step 4: Validate pipeline slug against registry
|
|
124
|
+
getPipelineConfig(validatedObject.pipeline);
|
|
125
|
+
|
|
126
|
+
// Step 5: Check for duplicates
|
|
127
|
+
const isDuplicate = await checkDuplicateJob(baseDir, validatedObject.name);
|
|
128
|
+
if (isDuplicate) {
|
|
129
|
+
throw new Error("Job with this name already exists");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return validatedObject;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export {
|
|
136
|
+
validateAndParseJson,
|
|
137
|
+
validateRequiredFields,
|
|
138
|
+
validateNameFormat,
|
|
139
|
+
checkDuplicateJob,
|
|
140
|
+
validateSeed,
|
|
141
|
+
};
|