@openfn/project 0.1.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/index.d.ts +44 -0
- package/dist/index.js +562 -0
- package/package.json +45 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as l from '@openfn/lexicon';
|
|
2
|
+
|
|
3
|
+
type FileFormats = 'yaml' | 'json';
|
|
4
|
+
type RepoOptions = {
|
|
5
|
+
/**default workflow root when serializing to fs (relative to openfn.yaml) */
|
|
6
|
+
workflowRoot?: string;
|
|
7
|
+
formats: {
|
|
8
|
+
openfn: FileFormats;
|
|
9
|
+
workflow: FileFormats;
|
|
10
|
+
project: FileFormats;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
declare class Project {
|
|
14
|
+
/** project name */
|
|
15
|
+
name?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
history: string[];
|
|
18
|
+
workflows: l.Workflow[];
|
|
19
|
+
options: any;
|
|
20
|
+
meta: any;
|
|
21
|
+
openfn?: l.ProjectConfig;
|
|
22
|
+
repo?: Required<RepoOptions>;
|
|
23
|
+
static from(type: 'state', data: any, options: Partial<l.ProjectConfig>): Project;
|
|
24
|
+
static from(type: 'fs', options: {
|
|
25
|
+
root: string;
|
|
26
|
+
}): Project;
|
|
27
|
+
static from(type: 'path', data: any): Project;
|
|
28
|
+
static diff(a: Project, b: Project): void;
|
|
29
|
+
constructor(data: l.Project, repoConfig?: RepoOptions);
|
|
30
|
+
serialize(type?: 'json' | 'yaml' | 'fs' | 'state', options?: any): any;
|
|
31
|
+
getVersionHash(): void;
|
|
32
|
+
merge(project: Project, options: any): void;
|
|
33
|
+
getWorkflow(id: string): l.Workflow | undefined;
|
|
34
|
+
getIdentifier(): string;
|
|
35
|
+
compare(proj: Project): void;
|
|
36
|
+
getUUID(workflow: string | Workflow, stepId: string, otherStep?: string): any;
|
|
37
|
+
}
|
|
38
|
+
declare class Workflow {
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare function yamlToJson(y: string): any;
|
|
42
|
+
declare function jsonToYaml(json: string | JSONObject): string;
|
|
43
|
+
|
|
44
|
+
export { Project as default, jsonToYaml, yamlToJson };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/serialize/index.ts
|
|
8
|
+
var serialize_exports = {};
|
|
9
|
+
__export(serialize_exports, {
|
|
10
|
+
fs: () => to_fs_default,
|
|
11
|
+
json: () => to_json_default,
|
|
12
|
+
state: () => to_app_state_default
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// src/serialize/to-json.ts
|
|
16
|
+
function to_json_default(project) {
|
|
17
|
+
return {
|
|
18
|
+
// There must be a better way to do this?
|
|
19
|
+
// Do we just serialize all public fields?
|
|
20
|
+
name: project.name,
|
|
21
|
+
description: project.description,
|
|
22
|
+
repo: project.repo,
|
|
23
|
+
meta: project.meta,
|
|
24
|
+
workflows: project.workflows,
|
|
25
|
+
collections: project.collections,
|
|
26
|
+
credentials: project.credentials,
|
|
27
|
+
openfn: project.openfn,
|
|
28
|
+
options: project.options
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/util/yaml.ts
|
|
33
|
+
import yaml from "yaml";
|
|
34
|
+
function yamlToJson(y) {
|
|
35
|
+
const doc = yaml.parseDocument(y);
|
|
36
|
+
return doc.toJS();
|
|
37
|
+
}
|
|
38
|
+
function jsonToYaml(json) {
|
|
39
|
+
if (typeof json === "string") {
|
|
40
|
+
json = JSON.parse(json);
|
|
41
|
+
}
|
|
42
|
+
const doc = new yaml.Document(json);
|
|
43
|
+
return yaml.stringify(doc, null, 2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/serialize/to-app-state.ts
|
|
47
|
+
import { randomUUID } from "node:crypto";
|
|
48
|
+
function to_app_state_default(project, options = {}) {
|
|
49
|
+
const { projectId: id, endpoint, env, ...rest } = project.openfn;
|
|
50
|
+
const state = {
|
|
51
|
+
id,
|
|
52
|
+
name: project.name,
|
|
53
|
+
description: project.description,
|
|
54
|
+
project_credentials: project.credentials,
|
|
55
|
+
collections: project.collections,
|
|
56
|
+
...rest,
|
|
57
|
+
...project.options,
|
|
58
|
+
workflows: project.workflows.map(mapWorkflow)
|
|
59
|
+
};
|
|
60
|
+
const shouldReturnYaml = options.format === "yaml" || !options.format && project.repo.formats.project === "yaml";
|
|
61
|
+
if (shouldReturnYaml) {
|
|
62
|
+
return jsonToYaml(state);
|
|
63
|
+
}
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
var mapWorkflow = (workflow) => {
|
|
67
|
+
const wfState = {
|
|
68
|
+
name: workflow.name,
|
|
69
|
+
...workflow.openfn,
|
|
70
|
+
jobs: [],
|
|
71
|
+
triggers: [],
|
|
72
|
+
edges: []
|
|
73
|
+
};
|
|
74
|
+
const lookup = workflow.steps.reduce((obj, next) => {
|
|
75
|
+
if (!next.openfn?.id) {
|
|
76
|
+
next.openfn ??= {};
|
|
77
|
+
next.openfn.id = randomUUID();
|
|
78
|
+
}
|
|
79
|
+
obj[next.id] = next.openfn.id;
|
|
80
|
+
return obj;
|
|
81
|
+
}, {});
|
|
82
|
+
workflow.steps.forEach((s) => {
|
|
83
|
+
let isTrigger;
|
|
84
|
+
let node;
|
|
85
|
+
if (s.type && !s.expression) {
|
|
86
|
+
isTrigger = true;
|
|
87
|
+
node = {
|
|
88
|
+
type: s.type,
|
|
89
|
+
...s.openfn
|
|
90
|
+
};
|
|
91
|
+
wfState.triggers.push(node);
|
|
92
|
+
} else {
|
|
93
|
+
node = {
|
|
94
|
+
name: s.name,
|
|
95
|
+
body: s.expression,
|
|
96
|
+
adaptor: s.adaptor,
|
|
97
|
+
...s.openfn
|
|
98
|
+
};
|
|
99
|
+
wfState.jobs.push(node);
|
|
100
|
+
}
|
|
101
|
+
Object.keys(s.next ?? {}).forEach((next) => {
|
|
102
|
+
const rules = s.next[next];
|
|
103
|
+
const e = {
|
|
104
|
+
id: rules.openfn?.id ?? randomUUID(),
|
|
105
|
+
target_job_id: lookup[next],
|
|
106
|
+
enabled: !rules.disabled
|
|
107
|
+
};
|
|
108
|
+
if (isTrigger) {
|
|
109
|
+
e.source_trigger_id = node.id;
|
|
110
|
+
} else {
|
|
111
|
+
e.source_job_id = node.id;
|
|
112
|
+
}
|
|
113
|
+
if (rules.condition === true) {
|
|
114
|
+
e.condition_type = "always";
|
|
115
|
+
} else if (rules.condition === false) {
|
|
116
|
+
e.condition_type = "never";
|
|
117
|
+
} else if (typeof rules.condition === "string") {
|
|
118
|
+
}
|
|
119
|
+
wfState.edges.push(e);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
return wfState;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/serialize/to-fs.ts
|
|
126
|
+
import nodepath from "path";
|
|
127
|
+
var stringify = (json) => JSON.stringify(json, null, 2);
|
|
128
|
+
function to_fs_default(project) {
|
|
129
|
+
const files = {};
|
|
130
|
+
const { path: path2, content } = extractRepoConfig(project);
|
|
131
|
+
files[path2] = content;
|
|
132
|
+
for (const wf of project.workflows) {
|
|
133
|
+
const { path: path3, content: content2 } = extractWorkflow(project, wf.id);
|
|
134
|
+
files[path3] = content2;
|
|
135
|
+
for (const s of wf.steps) {
|
|
136
|
+
const result = extractStep(project, wf.id, s.id);
|
|
137
|
+
if (result) {
|
|
138
|
+
const { path: path4, content: content3 } = result;
|
|
139
|
+
files[path4] = content3;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return files;
|
|
144
|
+
}
|
|
145
|
+
var extractWorkflow = (project, workflowId2) => {
|
|
146
|
+
const format = project.repo.formats.workflow;
|
|
147
|
+
const workflow = project.getWorkflow(workflowId2);
|
|
148
|
+
if (!workflow) {
|
|
149
|
+
throw new Error(`workflow not found: ${workflowId2}`);
|
|
150
|
+
}
|
|
151
|
+
const root = project.repo?.workflowRoot ?? "workflows/";
|
|
152
|
+
const path2 = nodepath.join(root, workflow.id, workflow.id);
|
|
153
|
+
const wf = {
|
|
154
|
+
id: workflow.id,
|
|
155
|
+
name: workflow.name,
|
|
156
|
+
options: workflow.options,
|
|
157
|
+
steps: workflow.steps.map((step) => {
|
|
158
|
+
const { openfn, expression, ...mapped } = step;
|
|
159
|
+
if (expression) {
|
|
160
|
+
mapped.expression = `./${step.id}.js`;
|
|
161
|
+
}
|
|
162
|
+
return mapped;
|
|
163
|
+
})
|
|
164
|
+
};
|
|
165
|
+
return handleOutput(wf, path2, format);
|
|
166
|
+
};
|
|
167
|
+
var extractStep = (project, workflowId2, stepId) => {
|
|
168
|
+
const workflow = project.getWorkflow(workflowId2);
|
|
169
|
+
if (!workflow) {
|
|
170
|
+
throw new Error(`workflow not found: ${workflowId2}`);
|
|
171
|
+
}
|
|
172
|
+
const step = workflow.steps.find((s) => s.id === stepId);
|
|
173
|
+
if (!step) {
|
|
174
|
+
throw new Error(`step not found: ${stepId}`);
|
|
175
|
+
}
|
|
176
|
+
if (step.expression) {
|
|
177
|
+
const root = project.config?.workflowRoot ?? "workflows/";
|
|
178
|
+
const path2 = nodepath.join(root, `${workflow.id}/${step.id}.js`);
|
|
179
|
+
const content = step.expression;
|
|
180
|
+
return { path: path2, content };
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var extractRepoConfig = (project) => {
|
|
184
|
+
const format = project.repo.formats.openfn;
|
|
185
|
+
const config = {
|
|
186
|
+
name: project.name,
|
|
187
|
+
...project.repo,
|
|
188
|
+
project: project.openfn ?? {}
|
|
189
|
+
};
|
|
190
|
+
return handleOutput(config, "openfn", format);
|
|
191
|
+
};
|
|
192
|
+
var handleOutput = (data, filePath, format) => {
|
|
193
|
+
const path2 = `${filePath}.${format}`;
|
|
194
|
+
let content;
|
|
195
|
+
if (format === "json") {
|
|
196
|
+
content = stringify(data, null, 2);
|
|
197
|
+
} else if (format === "yaml") {
|
|
198
|
+
content = jsonToYaml(data);
|
|
199
|
+
} else {
|
|
200
|
+
throw new Error(`Unrecognised format: ${format}`);
|
|
201
|
+
}
|
|
202
|
+
return { path: path2, content };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/parse/from-app-state.ts
|
|
206
|
+
function slugify(text) {
|
|
207
|
+
return text.replace(/\W/g, " ").trim().replace(/\s+/g, "-").toLowerCase();
|
|
208
|
+
}
|
|
209
|
+
var from_app_state_default = (state, config) => {
|
|
210
|
+
if (config.format === "yaml") {
|
|
211
|
+
state = yamlToJson(state);
|
|
212
|
+
} else if (typeof state === "string") {
|
|
213
|
+
state = JSON.parse(state);
|
|
214
|
+
}
|
|
215
|
+
const {
|
|
216
|
+
id,
|
|
217
|
+
name,
|
|
218
|
+
description,
|
|
219
|
+
workflows,
|
|
220
|
+
project_credentials: credentials,
|
|
221
|
+
collections,
|
|
222
|
+
inserted_at,
|
|
223
|
+
updated_at,
|
|
224
|
+
...options
|
|
225
|
+
} = state;
|
|
226
|
+
const proj = {
|
|
227
|
+
name,
|
|
228
|
+
// TODO do we need to slug this or anything?
|
|
229
|
+
description,
|
|
230
|
+
collections,
|
|
231
|
+
credentials,
|
|
232
|
+
options
|
|
233
|
+
};
|
|
234
|
+
const repoConfig = {};
|
|
235
|
+
proj.openfn = {
|
|
236
|
+
projectId: id,
|
|
237
|
+
endpoint: config.endpoint,
|
|
238
|
+
env: config.env,
|
|
239
|
+
inserted_at,
|
|
240
|
+
updated_at
|
|
241
|
+
};
|
|
242
|
+
proj.meta = {
|
|
243
|
+
fetched_at: config.fetchedAt
|
|
244
|
+
};
|
|
245
|
+
proj.workflows = state.workflows.map(mapWorkflow2);
|
|
246
|
+
return new Project(proj, repoConfig);
|
|
247
|
+
};
|
|
248
|
+
var mapTriggerEdgeCondition = (edge) => {
|
|
249
|
+
const e = {
|
|
250
|
+
disabled: !edge.enabled
|
|
251
|
+
};
|
|
252
|
+
if (edge.condition_type === "always") {
|
|
253
|
+
e.condition = true;
|
|
254
|
+
} else if (edge.condition_type === "never") {
|
|
255
|
+
e.condition = false;
|
|
256
|
+
} else {
|
|
257
|
+
e.condition = edge.condition_expression;
|
|
258
|
+
}
|
|
259
|
+
e.openfn = {
|
|
260
|
+
id: edge.id
|
|
261
|
+
};
|
|
262
|
+
return e;
|
|
263
|
+
};
|
|
264
|
+
var mapWorkflow2 = (workflow) => {
|
|
265
|
+
const { jobs, edges, triggers, name, ...remoteProps } = workflow;
|
|
266
|
+
const mapped = {
|
|
267
|
+
id: slugify(workflow.name),
|
|
268
|
+
name: workflow.name,
|
|
269
|
+
steps: [],
|
|
270
|
+
openfn: remoteProps
|
|
271
|
+
};
|
|
272
|
+
workflow.triggers.forEach((trigger) => {
|
|
273
|
+
const { type, ...otherProps } = trigger;
|
|
274
|
+
const connectedEdges = edges.filter(
|
|
275
|
+
(e) => e.source_trigger_id === trigger.id
|
|
276
|
+
);
|
|
277
|
+
mapped.steps.push({
|
|
278
|
+
id: "trigger",
|
|
279
|
+
type,
|
|
280
|
+
openfn: otherProps,
|
|
281
|
+
next: connectedEdges.reduce((obj, edge) => {
|
|
282
|
+
const target = jobs.find((j) => j.id === edge.target_job_id);
|
|
283
|
+
if (!target) {
|
|
284
|
+
throw new Error(`Failed to find ${edge.target_job_id}`);
|
|
285
|
+
}
|
|
286
|
+
obj[slugify(target.name)] = mapTriggerEdgeCondition(edge);
|
|
287
|
+
return obj;
|
|
288
|
+
}, {})
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
workflow.jobs.forEach((step) => {
|
|
292
|
+
const outboundEdges = edges.filter(
|
|
293
|
+
(e) => e.source_job_id === step.id || e.source_trigger_id === step.id
|
|
294
|
+
);
|
|
295
|
+
const { body: expression, name: name2, adaptor, ...remoteProps2 } = step;
|
|
296
|
+
const s = {
|
|
297
|
+
id: slugify(name2),
|
|
298
|
+
name: name2,
|
|
299
|
+
expression,
|
|
300
|
+
adaptor,
|
|
301
|
+
openfn: remoteProps2
|
|
302
|
+
};
|
|
303
|
+
if (outboundEdges.length) {
|
|
304
|
+
s.next = outboundEdges.reduce((next, edge) => {
|
|
305
|
+
const target = jobs.find((j) => j.id === edge.target_job_id);
|
|
306
|
+
next[slugify(target.name)] = mapTriggerEdgeCondition(edge);
|
|
307
|
+
return next;
|
|
308
|
+
}, {});
|
|
309
|
+
}
|
|
310
|
+
mapped.steps.push(s);
|
|
311
|
+
});
|
|
312
|
+
return mapped;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/parse/from-fs.ts
|
|
316
|
+
import fs from "node:fs/promises";
|
|
317
|
+
import path from "node:path";
|
|
318
|
+
import { glob } from "glob";
|
|
319
|
+
|
|
320
|
+
// src/util/get-identifier.ts
|
|
321
|
+
var get_identifier_default = (config = {}) => {
|
|
322
|
+
const endpoint = config.endpoint || "local";
|
|
323
|
+
const name = config.env ?? "main";
|
|
324
|
+
let host;
|
|
325
|
+
try {
|
|
326
|
+
host = new URL(endpoint).hostname;
|
|
327
|
+
} catch (e) {
|
|
328
|
+
host = endpoint;
|
|
329
|
+
}
|
|
330
|
+
return `${name}@${host}`;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/parse/from-fs.ts
|
|
334
|
+
var parseProject = async (options = {}) => {
|
|
335
|
+
const { root } = options;
|
|
336
|
+
const proj = {};
|
|
337
|
+
let config;
|
|
338
|
+
try {
|
|
339
|
+
const file = await fs.readFile(
|
|
340
|
+
path.resolve(path.join(root, "openfn.yaml")),
|
|
341
|
+
"utf8"
|
|
342
|
+
);
|
|
343
|
+
config = yamlToJson(file);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
try {
|
|
346
|
+
const file = await fs.readFile(
|
|
347
|
+
path.join(root || ".", "openfn.json"),
|
|
348
|
+
"utf8"
|
|
349
|
+
);
|
|
350
|
+
config = JSON.parse(file);
|
|
351
|
+
} catch (e2) {
|
|
352
|
+
console.log(e2);
|
|
353
|
+
throw e2;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
let state;
|
|
357
|
+
const identifier = get_identifier_default({
|
|
358
|
+
endpoint: config.project?.endpoint,
|
|
359
|
+
env: config.project?.env
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
const format = config.formats.project ?? "yaml";
|
|
363
|
+
const statePath = path.join(root, ".projects", `${identifier}.${format}`);
|
|
364
|
+
const stateFile = await fs.readFile(statePath, "utf8");
|
|
365
|
+
state = from_app_state_default(stateFile, { format });
|
|
366
|
+
} catch (e) {
|
|
367
|
+
console.warn(`Failed to find state file for ${identifier}`);
|
|
368
|
+
}
|
|
369
|
+
const { project: openfn, ...repo } = config;
|
|
370
|
+
proj.openfn = openfn;
|
|
371
|
+
const workflowDir = config.workflowRoot ?? "workflows";
|
|
372
|
+
const fileType = config.formats?.workflow ?? "yaml";
|
|
373
|
+
const pattern = `${root}/${workflowDir}/*/*.${fileType}`;
|
|
374
|
+
const candidateWfs = await glob(pattern, {
|
|
375
|
+
ignore: ["**node_modules/**", "**tmp**"]
|
|
376
|
+
});
|
|
377
|
+
const workflows = [];
|
|
378
|
+
for (const filePath of candidateWfs) {
|
|
379
|
+
const candidate = await fs.readFile(filePath, "utf-8");
|
|
380
|
+
try {
|
|
381
|
+
const wf = fileType === "yaml" ? yamlToJson(candidate) : JSON.parse(candidate);
|
|
382
|
+
if (wf.id && Array.isArray(wf.steps)) {
|
|
383
|
+
const wfState = (state && state.getWorkflow(wf.id)) ?? {};
|
|
384
|
+
wf.openfn = {
|
|
385
|
+
id: wfState.openfn?.id ?? null
|
|
386
|
+
// TODO do we need to transfer more stuff?
|
|
387
|
+
};
|
|
388
|
+
console.log("Loading workflow at ", filePath);
|
|
389
|
+
for (const step of wf.steps) {
|
|
390
|
+
if (step.expression && step.expression.endsWith(".js")) {
|
|
391
|
+
const dir = path.dirname(filePath);
|
|
392
|
+
const exprPath = path.join(dir, step.expression);
|
|
393
|
+
try {
|
|
394
|
+
console.debug(`Loaded expression from ${exprPath}`);
|
|
395
|
+
step.expression = await fs.readFile(exprPath, "utf-8");
|
|
396
|
+
} catch (e) {
|
|
397
|
+
console.error(`Error loading expression from ${exprPath}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const uuid = state?.getUUID(wf.id, step.id) ?? null;
|
|
401
|
+
step.openfn = { id: uuid };
|
|
402
|
+
for (const target in step.next || {}) {
|
|
403
|
+
if (typeof step.next[target] === "boolean") {
|
|
404
|
+
const bool = step.next[target];
|
|
405
|
+
step.next[target] = { condition: bool };
|
|
406
|
+
}
|
|
407
|
+
const uuid2 = state?.getUUID(wf.id, step.id, target) ?? null;
|
|
408
|
+
step.next[target].openfn = { id: uuid2 };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
workflows.push(wf);
|
|
412
|
+
}
|
|
413
|
+
} catch (e) {
|
|
414
|
+
console.log(e);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
proj.workflows = workflows;
|
|
419
|
+
return new Project(proj, repo);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// src/util/uuid.ts
|
|
423
|
+
var getUuidForStep = (project, workflow, stepId) => {
|
|
424
|
+
const wf = typeof workflow === "string" ? project.getWorkflow(workflow) : workflow;
|
|
425
|
+
if (!wf) {
|
|
426
|
+
throw new Error(`Workflow "${workflow} not found in project ${project.id}`);
|
|
427
|
+
}
|
|
428
|
+
for (const step of wf.steps) {
|
|
429
|
+
if (step.id === stepId) {
|
|
430
|
+
return step.openfn?.id ?? null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
};
|
|
435
|
+
var getUuidForEdge = (project, workflow, from, to) => {
|
|
436
|
+
const wf = typeof workflow === "string" ? project.getWorkflow(workflow) : workflow;
|
|
437
|
+
if (!wf) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`Workflow "${workflowId} not found in project ${project.id}`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
for (const step of wf.steps) {
|
|
443
|
+
if (step.id === from) {
|
|
444
|
+
for (const edge in step.next) {
|
|
445
|
+
if (edge === to) {
|
|
446
|
+
return step.next[edge].openfn?.id ?? null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/Project.ts
|
|
456
|
+
var setConfigDefaults = (config = {}) => ({
|
|
457
|
+
workflowRoot: config.workflowRoot ?? "workflows",
|
|
458
|
+
formats: {
|
|
459
|
+
// TODO change these maybe
|
|
460
|
+
openfn: config.formats?.openfn ?? "yaml",
|
|
461
|
+
project: config.formats?.project ?? "yaml",
|
|
462
|
+
workflow: config.formats?.workflow ?? "yaml"
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
var Project = class {
|
|
466
|
+
// what schema version is this?
|
|
467
|
+
// And how are we tracking this?
|
|
468
|
+
// version;
|
|
469
|
+
/** project name */
|
|
470
|
+
name;
|
|
471
|
+
description;
|
|
472
|
+
// array of version shas
|
|
473
|
+
history = [];
|
|
474
|
+
workflows;
|
|
475
|
+
// option strings saved by the app
|
|
476
|
+
// these are all (?) unused clientside
|
|
477
|
+
options;
|
|
478
|
+
// local metadata used by the CLI
|
|
479
|
+
// This stuff is not synced back to lightning
|
|
480
|
+
meta;
|
|
481
|
+
// this contains meta about the connected openfn project
|
|
482
|
+
openfn;
|
|
483
|
+
// repo configuration options
|
|
484
|
+
// these should be shared across projects
|
|
485
|
+
// and saved to an openfn.yaml file
|
|
486
|
+
repo;
|
|
487
|
+
static from(type, data, options) {
|
|
488
|
+
if (type === "state") {
|
|
489
|
+
return from_app_state_default(data, options);
|
|
490
|
+
}
|
|
491
|
+
if (type === "fs") {
|
|
492
|
+
return parseProject(data, options);
|
|
493
|
+
}
|
|
494
|
+
throw new Error(`Didn't recognize type ${type}`);
|
|
495
|
+
}
|
|
496
|
+
// Diff two projects
|
|
497
|
+
static diff(a, b) {
|
|
498
|
+
}
|
|
499
|
+
// env is excluded because it's not really part of the project
|
|
500
|
+
// uh maybe
|
|
501
|
+
// maybe this second arg is config - like env, branch rules, serialisation rules
|
|
502
|
+
// stuff that's external to the actual project and managed by the repo
|
|
503
|
+
constructor(data, repoConfig = {}) {
|
|
504
|
+
this.repo = setConfigDefaults(repoConfig);
|
|
505
|
+
this.id = data.id;
|
|
506
|
+
this.name = data.name;
|
|
507
|
+
this.description = data.description;
|
|
508
|
+
this.openfn = data.openfn;
|
|
509
|
+
this.options = data.options;
|
|
510
|
+
this.workflows = data.workflows;
|
|
511
|
+
this.collections = data.collections;
|
|
512
|
+
this.credentials = data.credentials;
|
|
513
|
+
this.meta = data.meta;
|
|
514
|
+
}
|
|
515
|
+
serialize(type = "json", options) {
|
|
516
|
+
if (type in serialize_exports) {
|
|
517
|
+
return serialize_exports[type](this, options);
|
|
518
|
+
}
|
|
519
|
+
throw new Error(`Cannot serialize ${type}`);
|
|
520
|
+
}
|
|
521
|
+
// would like a better name for this
|
|
522
|
+
// stamp? id? sha?
|
|
523
|
+
// this builds a version string for the current state
|
|
524
|
+
getVersionHash() {
|
|
525
|
+
}
|
|
526
|
+
// take a second project and merge its data into this one
|
|
527
|
+
// Throws if there's a conflict, unless force is true
|
|
528
|
+
// It's basically an overwrite
|
|
529
|
+
merge(project, options) {
|
|
530
|
+
}
|
|
531
|
+
// what else might we need?
|
|
532
|
+
// get workflow by name or id
|
|
533
|
+
// this is fuzzy, but is that wrong?
|
|
534
|
+
getWorkflow(id) {
|
|
535
|
+
return this.workflows.find((wf) => wf.id == id);
|
|
536
|
+
}
|
|
537
|
+
// it's the name of the project.yaml file
|
|
538
|
+
// qualified name? Remote name? App name?
|
|
539
|
+
// every project in a repo need a unique identifier
|
|
540
|
+
getIdentifier() {
|
|
541
|
+
return get_identifier_default(this.openfn);
|
|
542
|
+
}
|
|
543
|
+
// Compare this project with another and return a diff
|
|
544
|
+
compare(proj) {
|
|
545
|
+
}
|
|
546
|
+
// find the UUID for a given node or edge
|
|
547
|
+
// returns null if it doesn't exist
|
|
548
|
+
getUUID(workflow, stepId, otherStep) {
|
|
549
|
+
if (otherStep) {
|
|
550
|
+
return getUuidForEdge(this, workflow, stepId, otherStep);
|
|
551
|
+
}
|
|
552
|
+
return getUuidForStep(this, workflow, stepId);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/index.ts
|
|
557
|
+
var src_default = Project;
|
|
558
|
+
export {
|
|
559
|
+
src_default as default,
|
|
560
|
+
jsonToYaml,
|
|
561
|
+
yamlToJson
|
|
562
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openfn/project",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read, serialize, replicate and sync OpenFn projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"module": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"author": "Open Function Group <admin@openfn.org>",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"ava": "5.3.1",
|
|
21
|
+
"mock-fs": "^5.4.1",
|
|
22
|
+
"rimraf": "^3.0.2",
|
|
23
|
+
"tslib": "^2.4.0",
|
|
24
|
+
"tsup": "^7.2.0",
|
|
25
|
+
"typescript": "^5.1.6"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"glob": "^11.0.2",
|
|
29
|
+
"yaml": "^2.2.2",
|
|
30
|
+
"@openfn/lexicon": "^1.2.2",
|
|
31
|
+
"@openfn/logger": "1.0.5"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "pnpm ava",
|
|
39
|
+
"test:watch": "pnpm ava -w",
|
|
40
|
+
"test:types": "pnpm tsc --noEmit --project tsconfig.json",
|
|
41
|
+
"build": "tsup --config ../../tsup.config.js src/index.ts",
|
|
42
|
+
"build:watch": "pnpm build --watch",
|
|
43
|
+
"pack": "pnpm pack --pack-destination ../../dist"
|
|
44
|
+
}
|
|
45
|
+
}
|