@openfn/project 0.5.0 → 0.6.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 +2 -2
- package/dist/index.d.ts +25 -6
- package/dist/index.js +189 -41
- package/dist/workflow.ohm +38 -0
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -51,10 +51,10 @@ a-b
|
|
|
51
51
|
a-c
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
You can set properties on the workflow itself - probably the
|
|
54
|
+
You can set properties on the workflow itself - probably the id, with `@attributes`
|
|
55
55
|
|
|
56
56
|
```
|
|
57
|
-
@
|
|
57
|
+
@id my-cool-workflow
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
You can also set properties on a node by putting comma seperated key-value pairs in brackets
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ declare class Workflow {
|
|
|
15
15
|
uuid: {};
|
|
16
16
|
id: {};
|
|
17
17
|
};
|
|
18
|
-
name
|
|
18
|
+
name?: string;
|
|
19
19
|
id: string;
|
|
20
20
|
openfn: OpenfnMeta;
|
|
21
21
|
constructor(workflow: l.Workflow);
|
|
@@ -31,6 +31,8 @@ declare class Workflow {
|
|
|
31
31
|
}) | undefined;
|
|
32
32
|
getUUID(id: any): string;
|
|
33
33
|
toJSON(): JSON.Object;
|
|
34
|
+
getUUIDMap(): Record<string, string>;
|
|
35
|
+
getVersionHash(): string;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
type FromFsConfig = {
|
|
@@ -47,6 +49,10 @@ type FileFormats = 'yaml' | 'json';
|
|
|
47
49
|
interface OpenfnConfig {
|
|
48
50
|
name: string;
|
|
49
51
|
workflowRoot: string;
|
|
52
|
+
dirs: {
|
|
53
|
+
workflows: string;
|
|
54
|
+
projects: string;
|
|
55
|
+
};
|
|
50
56
|
formats: {
|
|
51
57
|
openfn: FileFormats;
|
|
52
58
|
project: FileFormats;
|
|
@@ -82,16 +88,25 @@ declare class Project {
|
|
|
82
88
|
collections: any;
|
|
83
89
|
static from(type: 'state', data: any, options: Partial<l.ProjectConfig>): Project;
|
|
84
90
|
static from(type: 'fs', options: FromFsConfig): Project;
|
|
85
|
-
static from(type: 'path', data:
|
|
91
|
+
static from(type: 'path', data: string, options?: {
|
|
92
|
+
config?: Partial<OpenfnConfig>;
|
|
93
|
+
}): Project;
|
|
86
94
|
static diff(a: Project, b: Project): void;
|
|
87
95
|
static merge(source: Project, target: Project, options: MergeProjectOptions): Project;
|
|
88
96
|
constructor(data: l.Project, repoConfig?: RepoOptions);
|
|
89
97
|
serialize(type?: 'json' | 'yaml' | 'fs' | 'state', options?: any): any;
|
|
90
98
|
getVersionHash(): void;
|
|
91
|
-
getWorkflow(
|
|
99
|
+
getWorkflow(idOrName: string): Workflow | undefined;
|
|
92
100
|
getIdentifier(): string;
|
|
93
101
|
compare(proj: Project): void;
|
|
94
102
|
getUUID(workflow: string | Workflow, stepId: string, otherStep?: string): any;
|
|
103
|
+
/**
|
|
104
|
+
* Returns a map of ids:uuids for everything in the project
|
|
105
|
+
*/
|
|
106
|
+
getUUIDMap(options?: {
|
|
107
|
+
workflows: boolean;
|
|
108
|
+
project: false;
|
|
109
|
+
}): {};
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
declare class Workspace {
|
|
@@ -104,7 +119,7 @@ declare class Workspace {
|
|
|
104
119
|
get(id: string): Project | undefined;
|
|
105
120
|
getProjectPath(id: string): string | undefined;
|
|
106
121
|
getActiveProject(): Project | undefined;
|
|
107
|
-
getConfig(): OpenfnConfig
|
|
122
|
+
getConfig(): Partial<OpenfnConfig>;
|
|
108
123
|
get activeProjectId(): string | undefined;
|
|
109
124
|
get valid(): boolean;
|
|
110
125
|
}
|
|
@@ -115,14 +130,18 @@ declare function jsonToYaml(json: string | JSONObject): string;
|
|
|
115
130
|
type GenerateWorkflowOptions = {
|
|
116
131
|
name: string;
|
|
117
132
|
uuidSeed: number;
|
|
118
|
-
openfnUuid: boolean;
|
|
119
133
|
printErrors: boolean;
|
|
134
|
+
uuidMap?: Record<string, string>;
|
|
135
|
+
openfnUuid: boolean;
|
|
136
|
+
};
|
|
137
|
+
type GenerateProjectOptions = GenerateWorkflowOptions & {
|
|
138
|
+
uuidMap: Array<Record<string, string>>;
|
|
120
139
|
};
|
|
121
140
|
/**
|
|
122
141
|
* Generate a Workflow from a simple text based representation
|
|
123
142
|
* eg, `a-b b-c a-c`
|
|
124
143
|
*/
|
|
125
144
|
declare function generateWorkflow(def: string, options?: Partial<GenerateWorkflowOptions>): Workflow;
|
|
126
|
-
declare function generateProject(name: string, workflowDefs: string[], options
|
|
145
|
+
declare function generateProject(name: string, workflowDefs: string[], options?: Partial<GenerateProjectOptions>): Project;
|
|
127
146
|
|
|
128
147
|
export { Workspace, Project as default, generateProject, generateWorkflow, jsonToYaml, yamlToJson };
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,74 @@ function slugify(text) {
|
|
|
9
9
|
return text?.replace(/\W/g, " ").trim().replace(/\s+/g, "-").toLowerCase();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// src/util/version.ts
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
var SHORT_HASH_LENGTH = 12;
|
|
15
|
+
function isDefined(v) {
|
|
16
|
+
return v !== void 0 && v !== null;
|
|
17
|
+
}
|
|
18
|
+
var generateHash = (workflow, source = "cli") => {
|
|
19
|
+
const parts = [];
|
|
20
|
+
const wfKeys = ["name", "credentials"].sort();
|
|
21
|
+
const stepKeys = [
|
|
22
|
+
"name",
|
|
23
|
+
"adaptors",
|
|
24
|
+
"adaptor",
|
|
25
|
+
// there's ao adaptor & adaptors key in steps somehow.
|
|
26
|
+
"expression",
|
|
27
|
+
"configuration",
|
|
28
|
+
// assumes a string credential id
|
|
29
|
+
"expression"
|
|
30
|
+
// TODO need to model trigger types in this, which I think are currently ignored
|
|
31
|
+
].sort();
|
|
32
|
+
const edgeKeys = [
|
|
33
|
+
"condition",
|
|
34
|
+
"label",
|
|
35
|
+
"disabled"
|
|
36
|
+
// This feels more like an option - should be excluded?
|
|
37
|
+
].sort();
|
|
38
|
+
wfKeys.forEach((key) => {
|
|
39
|
+
if (isDefined(workflow[key])) {
|
|
40
|
+
parts.push(key, serializeValue(workflow[key]));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const steps = (workflow.steps || []).slice().sort((a, b) => {
|
|
44
|
+
const aName = a.name ?? "";
|
|
45
|
+
const bName = b.name ?? "";
|
|
46
|
+
return aName.localeCompare(bName);
|
|
47
|
+
});
|
|
48
|
+
for (const step of steps) {
|
|
49
|
+
stepKeys.forEach((key) => {
|
|
50
|
+
if (isDefined(step[key])) {
|
|
51
|
+
parts.push(key, serializeValue(step[key]));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (step.next && Array.isArray(step.next)) {
|
|
55
|
+
const edges = step.next.slice().sort((a, b) => {
|
|
56
|
+
const aLabel = a.label || "";
|
|
57
|
+
const bLabel = b.label || "";
|
|
58
|
+
return aLabel.localeCompare(bLabel);
|
|
59
|
+
});
|
|
60
|
+
for (const edge of edges) {
|
|
61
|
+
edgeKeys.forEach((key) => {
|
|
62
|
+
if (isDefined(edge[key])) {
|
|
63
|
+
parts.push(key, serializeValue(edge[key]));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const str = parts.join("");
|
|
70
|
+
const hash = crypto.createHash("sha256").update(str).digest("hex");
|
|
71
|
+
return `${source}:${hash.substring(0, SHORT_HASH_LENGTH)}`;
|
|
72
|
+
};
|
|
73
|
+
function serializeValue(val) {
|
|
74
|
+
if (typeof val === "object") {
|
|
75
|
+
return JSON.stringify(val);
|
|
76
|
+
}
|
|
77
|
+
return String(val);
|
|
78
|
+
}
|
|
79
|
+
|
|
12
80
|
// src/Workflow.ts
|
|
13
81
|
var clone = (obj) => JSON.parse(JSON.stringify(obj));
|
|
14
82
|
var Workflow = class {
|
|
@@ -31,8 +99,15 @@ var Workflow = class {
|
|
|
31
99
|
};
|
|
32
100
|
this.workflow = clone(workflow);
|
|
33
101
|
const { id, name, openfn, steps, ...options } = workflow;
|
|
102
|
+
if (!(id || name)) {
|
|
103
|
+
throw new Error("A Workflow MUST have a name or id");
|
|
104
|
+
}
|
|
34
105
|
this.id = id ?? slugify(name);
|
|
35
|
-
this.name = name
|
|
106
|
+
this.name = name;
|
|
107
|
+
this.workflow.id = this.id;
|
|
108
|
+
if (name) {
|
|
109
|
+
this.workflow.name = this.name;
|
|
110
|
+
}
|
|
36
111
|
this.openfn = openfn;
|
|
37
112
|
this.options = options;
|
|
38
113
|
this.#buildIndex();
|
|
@@ -128,6 +203,12 @@ var Workflow = class {
|
|
|
128
203
|
toJSON() {
|
|
129
204
|
return this.workflow;
|
|
130
205
|
}
|
|
206
|
+
getUUIDMap() {
|
|
207
|
+
return this.index.uuid;
|
|
208
|
+
}
|
|
209
|
+
getVersionHash() {
|
|
210
|
+
return generateHash(this);
|
|
211
|
+
}
|
|
131
212
|
};
|
|
132
213
|
var Workflow_default = Workflow;
|
|
133
214
|
|
|
@@ -157,7 +238,7 @@ function to_json_default(project) {
|
|
|
157
238
|
}
|
|
158
239
|
|
|
159
240
|
// src/util/rename-keys.ts
|
|
160
|
-
function renameKeys(props, keyMap) {
|
|
241
|
+
function renameKeys(props = {}, keyMap) {
|
|
161
242
|
return Object.fromEntries(
|
|
162
243
|
Object.entries(props).map(([key, value]) => [
|
|
163
244
|
keyMap[key] ? keyMap[key] : key,
|
|
@@ -183,7 +264,7 @@ function jsonToYaml(json) {
|
|
|
183
264
|
// src/serialize/to-app-state.ts
|
|
184
265
|
import { randomUUID } from "node:crypto";
|
|
185
266
|
function to_app_state_default(project, options = {}) {
|
|
186
|
-
const { uuid: id, endpoint, env, ...rest } = project.openfn;
|
|
267
|
+
const { uuid: id, endpoint, env, ...rest } = project.openfn ?? {};
|
|
187
268
|
const state = {
|
|
188
269
|
id,
|
|
189
270
|
name: project.name,
|
|
@@ -204,9 +285,11 @@ var mapWorkflow = (workflow) => {
|
|
|
204
285
|
if (workflow instanceof Workflow_default) {
|
|
205
286
|
workflow = workflow.toJSON();
|
|
206
287
|
}
|
|
288
|
+
const { uuid, ...originalOpenfnProps } = workflow.openfn ?? {};
|
|
207
289
|
const wfState = {
|
|
290
|
+
...originalOpenfnProps,
|
|
291
|
+
id: workflow.openfn?.uuid ?? randomUUID(),
|
|
208
292
|
name: workflow.name,
|
|
209
|
-
...renameKeys(workflow.openfn, { uuid: "id" }),
|
|
210
293
|
jobs: [],
|
|
211
294
|
triggers: [],
|
|
212
295
|
edges: []
|
|
@@ -349,10 +432,12 @@ function slugify2(text) {
|
|
|
349
432
|
return text.replace(/\W/g, " ").trim().replace(/\s+/g, "-").toLowerCase();
|
|
350
433
|
}
|
|
351
434
|
var from_app_state_default = (state, config) => {
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
435
|
+
if (typeof state === "string") {
|
|
436
|
+
if (config?.format === "yaml") {
|
|
437
|
+
state = yamlToJson(state);
|
|
438
|
+
} else {
|
|
439
|
+
state = JSON.parse(state);
|
|
440
|
+
}
|
|
356
441
|
}
|
|
357
442
|
const {
|
|
358
443
|
id,
|
|
@@ -367,13 +452,11 @@ var from_app_state_default = (state, config) => {
|
|
|
367
452
|
} = state;
|
|
368
453
|
const proj = {
|
|
369
454
|
name,
|
|
370
|
-
// TODO do we need to slug this or anything?
|
|
371
455
|
description,
|
|
372
456
|
collections,
|
|
373
457
|
credentials,
|
|
374
458
|
options
|
|
375
459
|
};
|
|
376
|
-
const repoConfig = {};
|
|
377
460
|
proj.openfn = {
|
|
378
461
|
uuid: id,
|
|
379
462
|
endpoint: config.endpoint,
|
|
@@ -385,7 +468,7 @@ var from_app_state_default = (state, config) => {
|
|
|
385
468
|
fetched_at: config.fetchedAt
|
|
386
469
|
};
|
|
387
470
|
proj.workflows = state.workflows.map(mapWorkflow2);
|
|
388
|
-
return new Project(proj,
|
|
471
|
+
return new Project(proj, config?.repo);
|
|
389
472
|
};
|
|
390
473
|
var mapTriggerEdgeCondition = (edge) => {
|
|
391
474
|
const e = {
|
|
@@ -406,11 +489,13 @@ var mapTriggerEdgeCondition = (edge) => {
|
|
|
406
489
|
var mapWorkflow2 = (workflow) => {
|
|
407
490
|
const { jobs, edges, triggers, name, ...remoteProps } = workflow;
|
|
408
491
|
const mapped = {
|
|
409
|
-
id: slugify2(workflow.name),
|
|
410
492
|
name: workflow.name,
|
|
411
493
|
steps: [],
|
|
412
494
|
openfn: renameKeys(remoteProps, { id: "uuid" })
|
|
413
495
|
};
|
|
496
|
+
if (workflow.name) {
|
|
497
|
+
mapped.id = slugify2(workflow.name);
|
|
498
|
+
}
|
|
414
499
|
workflow.triggers.forEach((trigger) => {
|
|
415
500
|
const { type, ...otherProps } = trigger;
|
|
416
501
|
const connectedEdges = edges.filter(
|
|
@@ -454,6 +539,30 @@ var mapWorkflow2 = (workflow) => {
|
|
|
454
539
|
return mapped;
|
|
455
540
|
};
|
|
456
541
|
|
|
542
|
+
// src/parse/from-path.ts
|
|
543
|
+
import { extname } from "node:path";
|
|
544
|
+
import { readFile } from "node:fs/promises";
|
|
545
|
+
var from_path_default = async (path4, options = {}) => {
|
|
546
|
+
const ext = extname(path4).toLowerCase();
|
|
547
|
+
const source = await readFile(path4, "utf8");
|
|
548
|
+
const config = {
|
|
549
|
+
format: null,
|
|
550
|
+
repo: options.repo ?? options.config
|
|
551
|
+
// TMP
|
|
552
|
+
};
|
|
553
|
+
let state;
|
|
554
|
+
if (ext === ".json") {
|
|
555
|
+
config.format = "json";
|
|
556
|
+
state = JSON.parse(source);
|
|
557
|
+
} else if (ext.match(/(ya?ml)$/)) {
|
|
558
|
+
config.format = "yaml";
|
|
559
|
+
state = yamlToJson(source);
|
|
560
|
+
} else {
|
|
561
|
+
throw new Error(`Cannot load a project from a ${ext} file`);
|
|
562
|
+
}
|
|
563
|
+
return from_app_state_default(state, config);
|
|
564
|
+
};
|
|
565
|
+
|
|
457
566
|
// src/parse/from-fs.ts
|
|
458
567
|
import fs from "node:fs/promises";
|
|
459
568
|
import path from "node:path";
|
|
@@ -501,8 +610,12 @@ var parseProject = async (options = {}) => {
|
|
|
501
610
|
env: config.project?.env
|
|
502
611
|
});
|
|
503
612
|
try {
|
|
504
|
-
const format = config.formats
|
|
505
|
-
const statePath = path.join(
|
|
613
|
+
const format = config.formats?.project ?? config.formats?.projects ?? "yaml";
|
|
614
|
+
const statePath = path.join(
|
|
615
|
+
root,
|
|
616
|
+
config.dirs?.projects ?? ".projects",
|
|
617
|
+
`${identifier}.${format}`
|
|
618
|
+
);
|
|
506
619
|
const stateFile = await fs.readFile(statePath, "utf8");
|
|
507
620
|
state = from_app_state_default(stateFile, { format });
|
|
508
621
|
} catch (e) {
|
|
@@ -510,7 +623,8 @@ var parseProject = async (options = {}) => {
|
|
|
510
623
|
}
|
|
511
624
|
const { project: openfn, ...repo } = config;
|
|
512
625
|
proj.openfn = openfn;
|
|
513
|
-
|
|
626
|
+
proj.config = repo;
|
|
627
|
+
const workflowDir = config.workflowRoot ?? config.dirs?.workflows ?? "workflows";
|
|
514
628
|
const fileType = config.formats?.workflow ?? "yaml";
|
|
515
629
|
const pattern = `${root}/${workflowDir}/*/*.${fileType}`;
|
|
516
630
|
const candidateWfs = await glob(pattern, {
|
|
@@ -527,7 +641,6 @@ var parseProject = async (options = {}) => {
|
|
|
527
641
|
uuid: wfState.openfn?.uuid ?? null
|
|
528
642
|
// TODO do we need to transfer more stuff?
|
|
529
643
|
};
|
|
530
|
-
console.log("Loading workflow at ", filePath);
|
|
531
644
|
for (const step of wf.steps) {
|
|
532
645
|
if (step.expression && step.expression.endsWith(".js")) {
|
|
533
646
|
const dir = path.dirname(filePath);
|
|
@@ -565,7 +678,9 @@ var parseProject = async (options = {}) => {
|
|
|
565
678
|
var getUuidForStep = (project, workflow, stepId) => {
|
|
566
679
|
const wf = typeof workflow === "string" ? project.getWorkflow(workflow) : workflow;
|
|
567
680
|
if (!wf) {
|
|
568
|
-
throw new Error(
|
|
681
|
+
throw new Error(
|
|
682
|
+
`Workflow "${workflow}" not found in project ${project.id}`
|
|
683
|
+
);
|
|
569
684
|
}
|
|
570
685
|
for (const step of wf.steps) {
|
|
571
686
|
if (step.id === stepId) {
|
|
@@ -950,6 +1065,7 @@ function merge(source, target, options) {
|
|
|
950
1065
|
// src/Project.ts
|
|
951
1066
|
var maybeCreateWorkflow = (wf) => wf instanceof Workflow_default ? wf : new Workflow_default(wf);
|
|
952
1067
|
var setConfigDefaults = (config = {}) => ({
|
|
1068
|
+
...config,
|
|
953
1069
|
workflowRoot: config.workflowRoot ?? "workflows",
|
|
954
1070
|
formats: {
|
|
955
1071
|
// TODO change these maybe
|
|
@@ -976,7 +1092,7 @@ var Project = class {
|
|
|
976
1092
|
meta;
|
|
977
1093
|
// this contains meta about the connected openfn project
|
|
978
1094
|
openfn;
|
|
979
|
-
//
|
|
1095
|
+
// workspace-wide configuration options
|
|
980
1096
|
// these should be shared across projects
|
|
981
1097
|
// and saved to an openfn.yaml file
|
|
982
1098
|
repo;
|
|
@@ -986,11 +1102,13 @@ var Project = class {
|
|
|
986
1102
|
// collections for the project
|
|
987
1103
|
// TODO to be well typed
|
|
988
1104
|
collections;
|
|
989
|
-
static from(type, data, options) {
|
|
1105
|
+
static from(type, data, options = {}) {
|
|
990
1106
|
if (type === "state") {
|
|
991
1107
|
return from_app_state_default(data, options);
|
|
992
1108
|
} else if (type === "fs") {
|
|
993
1109
|
return parseProject(data, options);
|
|
1110
|
+
} else if (type === "path") {
|
|
1111
|
+
return from_path_default(data, options);
|
|
994
1112
|
}
|
|
995
1113
|
throw new Error(`Didn't recognize type ${type}`);
|
|
996
1114
|
}
|
|
@@ -1032,8 +1150,8 @@ var Project = class {
|
|
|
1032
1150
|
// what else might we need?
|
|
1033
1151
|
// get workflow by name or id
|
|
1034
1152
|
// this is fuzzy, but is that wrong?
|
|
1035
|
-
getWorkflow(
|
|
1036
|
-
return this.workflows.find((wf) => wf.id ==
|
|
1153
|
+
getWorkflow(idOrName) {
|
|
1154
|
+
return this.workflows.find((wf) => wf.id == idOrName) || this.workflows.find((wf) => wf.name === idOrName);
|
|
1037
1155
|
}
|
|
1038
1156
|
// it's the name of the project.yaml file
|
|
1039
1157
|
// qualified name? Remote name? App name?
|
|
@@ -1052,6 +1170,19 @@ var Project = class {
|
|
|
1052
1170
|
}
|
|
1053
1171
|
return getUuidForStep(this, workflow, stepId);
|
|
1054
1172
|
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Returns a map of ids:uuids for everything in the project
|
|
1175
|
+
*/
|
|
1176
|
+
getUUIDMap(options = {}) {
|
|
1177
|
+
const result = {};
|
|
1178
|
+
for (const wf of this.workflows) {
|
|
1179
|
+
result[wf.id] = {
|
|
1180
|
+
self: wf.openfn?.uuid,
|
|
1181
|
+
children: wf.getUUIDMap()
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
return result;
|
|
1185
|
+
}
|
|
1055
1186
|
};
|
|
1056
1187
|
var Project_default = Project;
|
|
1057
1188
|
|
|
@@ -1082,16 +1213,19 @@ var Workspace = class {
|
|
|
1082
1213
|
projectPaths = /* @__PURE__ */ new Map();
|
|
1083
1214
|
isValid = false;
|
|
1084
1215
|
constructor(workspacePath) {
|
|
1085
|
-
const projectsPath = path2.join(workspacePath, PROJECTS_DIRECTORY);
|
|
1086
1216
|
const openfnYamlPath = path2.join(workspacePath, OPENFN_YAML_FILE);
|
|
1087
1217
|
if (pathExists(openfnYamlPath, "file")) {
|
|
1088
1218
|
this.isValid = true;
|
|
1089
1219
|
const data = fs3.readFileSync(openfnYamlPath, "utf-8");
|
|
1090
1220
|
this.config = yamlToJson(data);
|
|
1091
1221
|
}
|
|
1222
|
+
const projectsPath = path2.join(
|
|
1223
|
+
workspacePath,
|
|
1224
|
+
this.config?.dirs?.projects ?? PROJECTS_DIRECTORY
|
|
1225
|
+
);
|
|
1092
1226
|
if (this.isValid && pathExists(projectsPath, "directory")) {
|
|
1093
1227
|
const stateFiles = fs3.readdirSync(projectsPath).filter(
|
|
1094
|
-
(fileName) => PROJECT_EXTENSIONS.includes(path2.extname(fileName))
|
|
1228
|
+
(fileName) => PROJECT_EXTENSIONS.includes(path2.extname(fileName)) && path2.parse(fileName).name !== "openfn"
|
|
1095
1229
|
);
|
|
1096
1230
|
this.projects = stateFiles.map((file) => {
|
|
1097
1231
|
const stateFilePath = path2.join(projectsPath, file);
|
|
@@ -1099,7 +1233,7 @@ var Workspace = class {
|
|
|
1099
1233
|
const project = from_app_state_default(data, { format: "yaml" });
|
|
1100
1234
|
this.projectPaths.set(project.name, stateFilePath);
|
|
1101
1235
|
return project;
|
|
1102
|
-
});
|
|
1236
|
+
}).filter((s) => s);
|
|
1103
1237
|
}
|
|
1104
1238
|
}
|
|
1105
1239
|
list() {
|
|
@@ -1133,16 +1267,21 @@ import { grammar } from "ohm-js";
|
|
|
1133
1267
|
var parser;
|
|
1134
1268
|
var initOperations = (options = {}) => {
|
|
1135
1269
|
let nodes = {};
|
|
1136
|
-
const
|
|
1270
|
+
const uuidMap = options.uuidMap ?? {};
|
|
1271
|
+
const uuid = (id) => {
|
|
1272
|
+
if (id in uuidMap) {
|
|
1273
|
+
return uuidMap[id];
|
|
1274
|
+
}
|
|
1137
1275
|
return options.uuidSeed ? options.uuidSeed++ : randomUUID2();
|
|
1138
1276
|
};
|
|
1139
1277
|
const buildNode = (name) => {
|
|
1140
1278
|
if (!nodes[name]) {
|
|
1279
|
+
const id = slugify(name);
|
|
1141
1280
|
nodes[name] = {
|
|
1142
1281
|
name,
|
|
1143
|
-
id
|
|
1282
|
+
id,
|
|
1144
1283
|
openfn: {
|
|
1145
|
-
uuid: uuid()
|
|
1284
|
+
uuid: uuid(id)
|
|
1146
1285
|
}
|
|
1147
1286
|
};
|
|
1148
1287
|
}
|
|
@@ -1169,6 +1308,7 @@ var initOperations = (options = {}) => {
|
|
|
1169
1308
|
const n1 = parent.buildWorkflow();
|
|
1170
1309
|
const n2 = child.buildWorkflow();
|
|
1171
1310
|
const e = edge.buildWorkflow();
|
|
1311
|
+
e.openfn.uuid = uuid(`${n1.id}-${n2.id}`);
|
|
1172
1312
|
n1.next ??= {};
|
|
1173
1313
|
n1.next[n2.name] = e;
|
|
1174
1314
|
return [n1, n2];
|
|
@@ -1197,9 +1337,6 @@ var initOperations = (options = {}) => {
|
|
|
1197
1337
|
return props.asIteration().children.map((c) => c.buildWorkflow());
|
|
1198
1338
|
},
|
|
1199
1339
|
prop(key, _op, value) {
|
|
1200
|
-
if (value._iter) {
|
|
1201
|
-
console.log(">>>> ITER");
|
|
1202
|
-
}
|
|
1203
1340
|
return [key.sourceString, value.buildWorkflow()];
|
|
1204
1341
|
},
|
|
1205
1342
|
// Bit flaky - we need this to handle quoted props
|
|
@@ -1214,16 +1351,15 @@ var initOperations = (options = {}) => {
|
|
|
1214
1351
|
},
|
|
1215
1352
|
edge(_) {
|
|
1216
1353
|
return {
|
|
1217
|
-
openfn: {
|
|
1218
|
-
uuid: uuid()
|
|
1219
|
-
}
|
|
1354
|
+
openfn: {}
|
|
1220
1355
|
};
|
|
1221
1356
|
}
|
|
1222
1357
|
};
|
|
1223
1358
|
return operations;
|
|
1224
1359
|
};
|
|
1225
1360
|
var createParser = () => {
|
|
1226
|
-
const
|
|
1361
|
+
const grammarPath = path3.resolve(import.meta.dirname, "workflow.ohm");
|
|
1362
|
+
const contents = readFileSync(grammarPath, "utf-8");
|
|
1227
1363
|
const parser2 = grammar(contents);
|
|
1228
1364
|
return {
|
|
1229
1365
|
parse(str, options) {
|
|
@@ -1247,19 +1383,31 @@ function generateWorkflow(def, options = {}) {
|
|
|
1247
1383
|
if (!parser) {
|
|
1248
1384
|
parser = createParser();
|
|
1249
1385
|
}
|
|
1250
|
-
const
|
|
1386
|
+
const raw = parser.parse(def, options);
|
|
1387
|
+
if (!raw.name) {
|
|
1388
|
+
raw.name = "Workflow";
|
|
1389
|
+
}
|
|
1390
|
+
if (!raw.id) {
|
|
1391
|
+
raw.id = "workflow";
|
|
1392
|
+
}
|
|
1251
1393
|
if (options.openfnUuid) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
};
|
|
1394
|
+
raw.openfn ??= {};
|
|
1395
|
+
raw.openfn.uuid = randomUUID2();
|
|
1255
1396
|
}
|
|
1397
|
+
const wf = new Workflow_default(raw);
|
|
1256
1398
|
return wf;
|
|
1257
1399
|
}
|
|
1258
|
-
function generateProject(name, workflowDefs, options) {
|
|
1259
|
-
const workflows = workflowDefs.map(
|
|
1400
|
+
function generateProject(name, workflowDefs, options = {}) {
|
|
1401
|
+
const workflows = workflowDefs.map(
|
|
1402
|
+
(w, idx) => generateWorkflow(w, {
|
|
1403
|
+
...options,
|
|
1404
|
+
uuidMap: options.uuidMap && options.uuidMap[idx]
|
|
1405
|
+
})
|
|
1406
|
+
);
|
|
1260
1407
|
return new Project_default({
|
|
1261
1408
|
name,
|
|
1262
|
-
workflows
|
|
1409
|
+
workflows,
|
|
1410
|
+
openfn: options.openfnUuid && { uuid: randomUUID2() }
|
|
1263
1411
|
});
|
|
1264
1412
|
}
|
|
1265
1413
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ohm grammar for interpreting our Workflow syntax
|
|
3
|
+
* https://ohmjs.org/docs/intro
|
|
4
|
+
*/
|
|
5
|
+
Workflow {
|
|
6
|
+
Workflow = attribute* Pair*
|
|
7
|
+
|
|
8
|
+
attribute = "@" attr_name space attr_name
|
|
9
|
+
|
|
10
|
+
attr_name = (alnum | "_" | "-")+
|
|
11
|
+
|
|
12
|
+
Pair = node edge node
|
|
13
|
+
|
|
14
|
+
comment = "#" (~lineTerminator any)*
|
|
15
|
+
|
|
16
|
+
space := " " | "\t" | lineTerminator | comment
|
|
17
|
+
|
|
18
|
+
/* lower case is important: it disables whitespace */
|
|
19
|
+
node = nodeWithProps | node_name
|
|
20
|
+
|
|
21
|
+
nodeWithProps = node_name props
|
|
22
|
+
|
|
23
|
+
/* A node name can contain letters, numbers or underscores */
|
|
24
|
+
node_name = (alnum | "_")+
|
|
25
|
+
|
|
26
|
+
props = "(" listOf<prop, ","> ")"
|
|
27
|
+
|
|
28
|
+
prop = alnum+ "=" propValue
|
|
29
|
+
|
|
30
|
+
propValue = alnum+ | quotedProp
|
|
31
|
+
|
|
32
|
+
quotedProp = "\"" (~"\"" any)* "\""
|
|
33
|
+
|
|
34
|
+
edge = "-"
|
|
35
|
+
|
|
36
|
+
lineTerminator = "\n" | "\r"
|
|
37
|
+
}
|
|
38
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfn/project",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Read, serialize, replicate and sync OpenFn projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"lodash-es": "^4.17.21",
|
|
33
33
|
"ohm-js": "^17.2.1",
|
|
34
34
|
"yaml": "^2.2.2",
|
|
35
|
-
"@openfn/lexicon": "^1.2.
|
|
35
|
+
"@openfn/lexicon": "^1.2.4",
|
|
36
36
|
"@openfn/logger": "1.0.6"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
@@ -43,8 +43,7 @@
|
|
|
43
43
|
"test": "pnpm ava",
|
|
44
44
|
"test:watch": "pnpm ava -w",
|
|
45
45
|
"test:types": "pnpm tsc --noEmit --project tsconfig.json",
|
|
46
|
-
"build": "tsup --config ../../tsup.config.js src/index.ts",
|
|
47
|
-
"build:watch": "pnpm build --watch",
|
|
46
|
+
"build": "tsup --config ../../tsup.config.js src/index.ts && cp src/gen/workflow.ohm dist/workflow.ohm",
|
|
48
47
|
"pack": "pnpm pack --pack-destination ../../dist"
|
|
49
48
|
}
|
|
50
49
|
}
|