@openfn/project 0.10.1 → 0.12.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 +12 -0
- package/dist/index.d.ts +41 -2
- package/dist/index.js +158 -44
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,6 +8,18 @@ A single Project can be Checked Out to disk at a time, meaning its source workfl
|
|
|
8
8
|
|
|
9
9
|
A Workspace is a set of related Projects , including a Project and its associated Sandboxes, or a Project deployed to apps in multiple web domains
|
|
10
10
|
|
|
11
|
+
## Structure and Artifects
|
|
12
|
+
|
|
13
|
+
openfn.yaml
|
|
14
|
+
|
|
15
|
+
project file
|
|
16
|
+
|
|
17
|
+
sort of a mix of project.yaml, state.json and config.json
|
|
18
|
+
|
|
19
|
+
This is strictly a representation of a server-side project, it's like the last-sync-state. CLI-only or offline projects do not have one.
|
|
20
|
+
|
|
21
|
+
It's also a portable representation of the project
|
|
22
|
+
|
|
11
23
|
### Serializing and Parsing
|
|
12
24
|
|
|
13
25
|
The main idea of Projects is that a Project represents a set of OpenFn workflows defined in any format and present a standard JS-friendly interface to manipulate and reason about them.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
1
|
import * as l from '@openfn/lexicon';
|
|
2
|
-
import l__default, { WorkspaceConfig, UUID } from '@openfn/lexicon';
|
|
2
|
+
import l__default, { SandboxMeta, WorkspaceConfig, UUID } from '@openfn/lexicon';
|
|
3
3
|
import { Logger } from '@openfn/logger';
|
|
4
4
|
import { Provisioner } from '@openfn/lexicon/lightning';
|
|
5
5
|
|
|
6
|
+
type DiffType = 'added' | 'changed' | 'removed';
|
|
7
|
+
type WorkflowDiff = {
|
|
8
|
+
id: string;
|
|
9
|
+
type: DiffType;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Compare two projects and return a list of workflow changes showing how
|
|
13
|
+
* project B has diverged from project A.
|
|
14
|
+
*
|
|
15
|
+
* Workflows are identified by their ID and compared using version hashes.
|
|
16
|
+
*
|
|
17
|
+
* @param a - The baseline project (e.g., main branch)
|
|
18
|
+
* @param b - The comparison project (e.g., staging branch)
|
|
19
|
+
* @returns Array of workflow diffs indicating how B differs from A:
|
|
20
|
+
* - 'added': workflow exists in B but not in A
|
|
21
|
+
* - 'removed': workflow exists in A but not in B
|
|
22
|
+
* - 'changed': workflow exists in both but has different version hashes
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const main = await Project.from('fs', { root: '.' });
|
|
27
|
+
* const staging = await Project.from('state', stagingState);
|
|
28
|
+
* const diffs = diff(main, staging);
|
|
29
|
+
* // Shows how staging has diverged from main
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare function diff(a: Project, b: Project): WorkflowDiff[];
|
|
33
|
+
|
|
6
34
|
type WithMeta<T> = T & {
|
|
7
35
|
openfn?: l.NodeMeta;
|
|
8
36
|
};
|
|
@@ -15,6 +43,8 @@ declare class Workflow {
|
|
|
15
43
|
options: any;
|
|
16
44
|
constructor(workflow: l.Workflow);
|
|
17
45
|
get steps(): WithMeta<l.Job & l.Trigger>[];
|
|
46
|
+
get start(): string | undefined;
|
|
47
|
+
set start(s: string);
|
|
18
48
|
_buildIndex(): void;
|
|
19
49
|
set(id: string, props: Partial<l.Job | l.StepEdge>): this;
|
|
20
50
|
get(id: string): WithMeta<l.Step | l.Trigger | l.StepEdge>;
|
|
@@ -43,6 +73,7 @@ type FromPathConfig = l.WorkspaceConfig & {
|
|
|
43
73
|
|
|
44
74
|
type FromFsConfig = {
|
|
45
75
|
root: string;
|
|
76
|
+
config?: Partial<l.WorkspaceConfig>;
|
|
46
77
|
logger?: Logger;
|
|
47
78
|
};
|
|
48
79
|
|
|
@@ -57,15 +88,19 @@ type SerializedWorkflow = {
|
|
|
57
88
|
openfn?: l.ProjectMeta;
|
|
58
89
|
};
|
|
59
90
|
|
|
91
|
+
declare const SANDBOX_MERGE = "sandbox";
|
|
92
|
+
declare const REPLACE_MERGE = "replace";
|
|
60
93
|
type MergeProjectOptions = {
|
|
61
94
|
workflowMappings: Record<string, string>;
|
|
62
95
|
removeUnmapped: boolean;
|
|
63
96
|
force: boolean;
|
|
97
|
+
mode: typeof SANDBOX_MERGE | typeof REPLACE_MERGE;
|
|
64
98
|
};
|
|
65
99
|
|
|
66
100
|
declare class Workspace {
|
|
67
101
|
config: l.WorkspaceConfig;
|
|
68
102
|
activeProject?: l.ProjectMeta;
|
|
103
|
+
root: string;
|
|
69
104
|
private projects;
|
|
70
105
|
private projectPaths;
|
|
71
106
|
private isValid;
|
|
@@ -77,6 +112,8 @@ declare class Workspace {
|
|
|
77
112
|
get(nameyThing: string): Project | null;
|
|
78
113
|
getProjectPath(id: string): string | undefined;
|
|
79
114
|
getActiveProject(): Project | undefined;
|
|
115
|
+
getCheckedOutProject(): Promise<Project>;
|
|
116
|
+
getCredentialMap(): string | undefined;
|
|
80
117
|
getConfig(): Partial<l.WorkspaceConfig>;
|
|
81
118
|
get activeProjectId(): unknown;
|
|
82
119
|
get valid(): boolean;
|
|
@@ -112,6 +149,7 @@ declare class Project {
|
|
|
112
149
|
config: l__default.WorkspaceConfig;
|
|
113
150
|
collections: any;
|
|
114
151
|
credentials: string[];
|
|
152
|
+
sandbox?: SandboxMeta;
|
|
115
153
|
static from(type: 'project', data: any, options: never): Promise<Project>;
|
|
116
154
|
static from(type: 'state', data: Provisioner.Project, meta?: Partial<l__default.ProjectMeta>, config?: fromAppStateConfig): Promise<Project>;
|
|
117
155
|
static from(type: 'fs', options: FromFsConfig): Promise<Project>;
|
|
@@ -136,6 +174,7 @@ declare class Project {
|
|
|
136
174
|
* Returns a map of ids:uuids for everything in the project
|
|
137
175
|
*/
|
|
138
176
|
getUUIDMap(): UUIDMap;
|
|
177
|
+
diff(project: Project): WorkflowDiff[];
|
|
139
178
|
canMergeInto(target: Project): boolean;
|
|
140
179
|
}
|
|
141
180
|
|
|
@@ -160,4 +199,4 @@ type GenerateProjectOptions = GenerateWorkflowOptions & {
|
|
|
160
199
|
declare function generateWorkflow(def: string, options?: Partial<GenerateWorkflowOptions>): Workflow;
|
|
161
200
|
declare function generateProject(name: string, workflowDefs: string[], options?: Partial<GenerateProjectOptions>): Project;
|
|
162
201
|
|
|
163
|
-
export { Workspace, Project as default, generateProject, generateWorkflow, jsonToYaml, yamlToJson };
|
|
202
|
+
export { DiffType, WorkflowDiff, Workspace, Project as default, diff, generateProject, generateWorkflow, jsonToYaml, yamlToJson };
|
package/dist/index.js
CHANGED
|
@@ -105,7 +105,16 @@ var Workflow = class {
|
|
|
105
105
|
};
|
|
106
106
|
this.workflow = clone(workflow);
|
|
107
107
|
this.workflow.history = workflow.history?.length ? workflow.history : [];
|
|
108
|
-
const {
|
|
108
|
+
const {
|
|
109
|
+
id,
|
|
110
|
+
name,
|
|
111
|
+
openfn,
|
|
112
|
+
steps,
|
|
113
|
+
history,
|
|
114
|
+
start: _start,
|
|
115
|
+
options,
|
|
116
|
+
...rest
|
|
117
|
+
} = workflow;
|
|
109
118
|
if (!(id || name)) {
|
|
110
119
|
throw new Error("A Workflow MUST have a name or id");
|
|
111
120
|
}
|
|
@@ -116,12 +125,18 @@ var Workflow = class {
|
|
|
116
125
|
this.workflow.name = this.name;
|
|
117
126
|
}
|
|
118
127
|
this.openfn = openfn;
|
|
119
|
-
this.options = options;
|
|
128
|
+
this.options = Object.assign({}, options, rest);
|
|
120
129
|
this._buildIndex();
|
|
121
130
|
}
|
|
122
131
|
get steps() {
|
|
123
132
|
return this.workflow.steps;
|
|
124
133
|
}
|
|
134
|
+
get start() {
|
|
135
|
+
return this.workflow.start;
|
|
136
|
+
}
|
|
137
|
+
set start(s) {
|
|
138
|
+
this.workflow.start = s;
|
|
139
|
+
}
|
|
125
140
|
_buildIndex() {
|
|
126
141
|
for (const step of this.workflow.steps) {
|
|
127
142
|
const s = step;
|
|
@@ -290,7 +305,10 @@ function to_app_state_default(project, options = {}) {
|
|
|
290
305
|
state.id = uuid;
|
|
291
306
|
Object.assign(state, rest, project.options);
|
|
292
307
|
state.project_credentials = project.credentials ?? [];
|
|
293
|
-
state.workflows = project.workflows.map(mapWorkflow)
|
|
308
|
+
state.workflows = project.workflows.map(mapWorkflow).reduce((obj, wf) => {
|
|
309
|
+
obj[slugify(wf.name ?? wf.id)] = wf;
|
|
310
|
+
return obj;
|
|
311
|
+
}, {});
|
|
294
312
|
const shouldReturnYaml = options.format === "yaml" || !options.format && project.config.formats.project === "yaml";
|
|
295
313
|
if (shouldReturnYaml) {
|
|
296
314
|
return jsonToYaml(state);
|
|
@@ -305,9 +323,9 @@ var mapWorkflow = (workflow) => {
|
|
|
305
323
|
const wfState = {
|
|
306
324
|
...originalOpenfnProps,
|
|
307
325
|
id: workflow.openfn?.uuid ?? randomUUID(),
|
|
308
|
-
jobs:
|
|
309
|
-
triggers:
|
|
310
|
-
edges:
|
|
326
|
+
jobs: {},
|
|
327
|
+
triggers: {},
|
|
328
|
+
edges: {},
|
|
311
329
|
lock_version: workflow.openfn?.lock_version ?? null
|
|
312
330
|
// TODO needs testing
|
|
313
331
|
};
|
|
@@ -331,7 +349,7 @@ var mapWorkflow = (workflow) => {
|
|
|
331
349
|
type: s.type,
|
|
332
350
|
...renameKeys(s.openfn, { uuid: "id" })
|
|
333
351
|
};
|
|
334
|
-
wfState.triggers.
|
|
352
|
+
wfState.triggers[node.type] = node;
|
|
335
353
|
} else {
|
|
336
354
|
node = omitBy(pick(s, ["name", "adaptor"]), isNil);
|
|
337
355
|
const { uuid: uuid2, ...otherOpenFnProps } = s.openfn ?? {};
|
|
@@ -343,7 +361,7 @@ var mapWorkflow = (workflow) => {
|
|
|
343
361
|
otherOpenFnProps.project_credential_id = s.configuration;
|
|
344
362
|
}
|
|
345
363
|
Object.assign(node, defaultJobProps, otherOpenFnProps);
|
|
346
|
-
wfState.jobs.
|
|
364
|
+
wfState.jobs[s.id ?? slugify(s.name)] = node;
|
|
347
365
|
}
|
|
348
366
|
Object.keys(s.next ?? {}).forEach((next) => {
|
|
349
367
|
const rules = s.next[next];
|
|
@@ -373,10 +391,15 @@ var mapWorkflow = (workflow) => {
|
|
|
373
391
|
e.condition_expression = rules.condition;
|
|
374
392
|
}
|
|
375
393
|
}
|
|
376
|
-
wfState.edges.
|
|
394
|
+
wfState.edges[`${s.id}->${next}`] = e;
|
|
377
395
|
});
|
|
378
396
|
});
|
|
379
|
-
wfState.edges =
|
|
397
|
+
wfState.edges = Object.keys(wfState.edges).sort(
|
|
398
|
+
(a, b) => `${wfState.edges[a].id}`.localeCompare("" + wfState.edges[b].id)
|
|
399
|
+
).reduce((obj, key) => {
|
|
400
|
+
obj[key] = wfState.edges[key];
|
|
401
|
+
return obj;
|
|
402
|
+
}, {});
|
|
380
403
|
return wfState;
|
|
381
404
|
};
|
|
382
405
|
|
|
@@ -389,6 +412,7 @@ import { readFileSync } from "node:fs";
|
|
|
389
412
|
import path from "node:path";
|
|
390
413
|
import { pickBy, isNil as isNil2 } from "lodash-es";
|
|
391
414
|
var buildConfig = (config = {}) => ({
|
|
415
|
+
credentials: "credentials.yaml",
|
|
392
416
|
...config,
|
|
393
417
|
dirs: {
|
|
394
418
|
projects: config.dirs?.projects ?? ".projects",
|
|
@@ -400,16 +424,19 @@ var buildConfig = (config = {}) => ({
|
|
|
400
424
|
workflow: config.formats?.workflow ?? "yaml"
|
|
401
425
|
}
|
|
402
426
|
});
|
|
403
|
-
var extractConfig = (source) => {
|
|
427
|
+
var extractConfig = (source, format) => {
|
|
404
428
|
const project = {
|
|
405
429
|
...source.openfn || {},
|
|
406
430
|
id: source.id
|
|
407
431
|
};
|
|
432
|
+
if (source.name) {
|
|
433
|
+
project.name = source.name;
|
|
434
|
+
}
|
|
408
435
|
const workspace = {
|
|
409
436
|
...source.config
|
|
410
437
|
};
|
|
411
438
|
const content = { project, workspace };
|
|
412
|
-
|
|
439
|
+
format = format ?? workspace.formats.openfn;
|
|
413
440
|
if (format === "yaml") {
|
|
414
441
|
return {
|
|
415
442
|
path: "openfn.yaml",
|
|
@@ -505,6 +532,7 @@ var extractWorkflow = (project, workflowId) => {
|
|
|
505
532
|
const wf = {
|
|
506
533
|
id: workflow.id,
|
|
507
534
|
name: workflow.name,
|
|
535
|
+
start: workflow.start,
|
|
508
536
|
// Note: if no options are defined, options will serialize to an empty object
|
|
509
537
|
// Not crazy about this - maybe we should do something better? Or do we like the consistency?
|
|
510
538
|
options: workflow.options,
|
|
@@ -604,6 +632,11 @@ var to_project_default = (project, options = {}) => {
|
|
|
604
632
|
},
|
|
605
633
|
isNil4
|
|
606
634
|
);
|
|
635
|
+
if (project.sandbox?.parentId) {
|
|
636
|
+
proj.sandbox = {
|
|
637
|
+
parentId: project.sandbox.parentId
|
|
638
|
+
};
|
|
639
|
+
}
|
|
607
640
|
const format = options.format ?? proj.config?.formats.project;
|
|
608
641
|
if (format === "json") {
|
|
609
642
|
return proj;
|
|
@@ -637,6 +670,7 @@ var from_app_state_default = (state, meta = {}, config = {}) => {
|
|
|
637
670
|
collections,
|
|
638
671
|
inserted_at,
|
|
639
672
|
updated_at,
|
|
673
|
+
parent_id,
|
|
640
674
|
...options
|
|
641
675
|
} = stateJson;
|
|
642
676
|
const proj = {
|
|
@@ -655,7 +689,12 @@ var from_app_state_default = (state, meta = {}, config = {}) => {
|
|
|
655
689
|
inserted_at,
|
|
656
690
|
updated_at
|
|
657
691
|
};
|
|
658
|
-
|
|
692
|
+
if (parent_id) {
|
|
693
|
+
proj.sandbox = {
|
|
694
|
+
parentId: parent_id
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
proj.workflows = Object.values(stateJson.workflows).map(mapWorkflow2);
|
|
659
698
|
return new Project(proj, config);
|
|
660
699
|
};
|
|
661
700
|
var mapEdge = (edge) => {
|
|
@@ -688,17 +727,22 @@ var mapWorkflow2 = (workflow) => {
|
|
|
688
727
|
if (workflow.name) {
|
|
689
728
|
mapped.id = slugify(workflow.name);
|
|
690
729
|
}
|
|
691
|
-
workflow.triggers.forEach((trigger) => {
|
|
730
|
+
Object.values(workflow.triggers).forEach((trigger) => {
|
|
692
731
|
const { type, ...otherProps } = trigger;
|
|
693
|
-
|
|
732
|
+
if (!mapped.start) {
|
|
733
|
+
mapped.start = type;
|
|
734
|
+
}
|
|
735
|
+
const connectedEdges = Object.values(edges).filter(
|
|
694
736
|
(e) => e.source_trigger_id === trigger.id
|
|
695
737
|
);
|
|
696
738
|
mapped.steps.push({
|
|
697
|
-
id:
|
|
739
|
+
id: type,
|
|
698
740
|
type,
|
|
699
741
|
openfn: renameKeys(otherProps, { id: "uuid" }),
|
|
700
742
|
next: connectedEdges.reduce((obj, edge) => {
|
|
701
|
-
const target = jobs.find(
|
|
743
|
+
const target = Object.values(jobs).find(
|
|
744
|
+
(j) => j.id === edge.target_job_id
|
|
745
|
+
);
|
|
702
746
|
if (!target) {
|
|
703
747
|
throw new Error(`Failed to find ${edge.target_job_id}`);
|
|
704
748
|
}
|
|
@@ -707,8 +751,8 @@ var mapWorkflow2 = (workflow) => {
|
|
|
707
751
|
}, {})
|
|
708
752
|
});
|
|
709
753
|
});
|
|
710
|
-
workflow.jobs.forEach((step) => {
|
|
711
|
-
const outboundEdges = edges.filter(
|
|
754
|
+
Object.values(workflow.jobs).forEach((step) => {
|
|
755
|
+
const outboundEdges = Object.values(edges).filter(
|
|
712
756
|
(e) => e.source_job_id === step.id || e.source_trigger_id === step.id
|
|
713
757
|
);
|
|
714
758
|
const {
|
|
@@ -731,7 +775,9 @@ var mapWorkflow2 = (workflow) => {
|
|
|
731
775
|
}
|
|
732
776
|
if (outboundEdges.length) {
|
|
733
777
|
s.next = outboundEdges.reduce((next, edge) => {
|
|
734
|
-
const target = jobs.find(
|
|
778
|
+
const target = Object.values(jobs).find(
|
|
779
|
+
(j) => j.id === edge.target_job_id
|
|
780
|
+
);
|
|
735
781
|
next[slugify(target.name)] = mapEdge(edge);
|
|
736
782
|
return next;
|
|
737
783
|
}, {});
|
|
@@ -748,16 +794,13 @@ import path2 from "node:path";
|
|
|
748
794
|
// src/parse/from-project.ts
|
|
749
795
|
var from_project_default = (data, config) => {
|
|
750
796
|
let rawJson = ensure_json_default(data);
|
|
751
|
-
let json;
|
|
752
797
|
if (rawJson.cli?.version ?? rawJson.version) {
|
|
753
|
-
|
|
754
|
-
} else {
|
|
755
|
-
json = from_v1(rawJson);
|
|
798
|
+
return new Project_default(from_v2(rawJson), config);
|
|
756
799
|
}
|
|
757
|
-
return
|
|
800
|
+
return from_v1(rawJson, config);
|
|
758
801
|
};
|
|
759
|
-
var from_v1 = (data) => {
|
|
760
|
-
return from_app_state_default(data);
|
|
802
|
+
var from_v1 = (data, config = {}) => {
|
|
803
|
+
return from_app_state_default(data, {}, config);
|
|
761
804
|
};
|
|
762
805
|
var from_v2 = (data) => {
|
|
763
806
|
return {
|
|
@@ -789,7 +832,7 @@ var parseProject = async (options) => {
|
|
|
789
832
|
const { root, logger } = options;
|
|
790
833
|
const { type, content } = findWorkspaceFile(root);
|
|
791
834
|
const context = loadWorkspaceFile(content, type);
|
|
792
|
-
const config = buildConfig(context.workspace);
|
|
835
|
+
const config = buildConfig(options.config ?? context.workspace);
|
|
793
836
|
const proj = {
|
|
794
837
|
id: context.project?.id,
|
|
795
838
|
name: context.project?.name,
|
|
@@ -799,14 +842,26 @@ var parseProject = async (options) => {
|
|
|
799
842
|
};
|
|
800
843
|
const workflowDir = config.workflowRoot ?? config.dirs?.workflows ?? "workflows";
|
|
801
844
|
const fileType = config.formats?.workflow ?? "yaml";
|
|
802
|
-
const pattern =
|
|
845
|
+
const pattern = path3.resolve(root, workflowDir) + `/**/*.${fileType}`;
|
|
803
846
|
const candidateWfs = await glob(pattern, {
|
|
804
847
|
ignore: ["**node_modules/**", "**tmp**"]
|
|
805
848
|
});
|
|
806
849
|
for (const filePath of candidateWfs) {
|
|
807
850
|
const candidate = await fs.readFile(filePath, "utf-8");
|
|
808
851
|
try {
|
|
809
|
-
|
|
852
|
+
let wf = fileType === "yaml" ? yamlToJson(candidate) : JSON.parse(candidate);
|
|
853
|
+
if (wf.workflow) {
|
|
854
|
+
if (wf.options) {
|
|
855
|
+
const { start, ...rest } = wf.options;
|
|
856
|
+
if (start) {
|
|
857
|
+
wf.workflow.start = start;
|
|
858
|
+
}
|
|
859
|
+
if (rest) {
|
|
860
|
+
wf.workflow.options = Object.assign({}, wf.workflow.options, rest);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
wf = wf.workflow;
|
|
864
|
+
}
|
|
810
865
|
if (wf.id && Array.isArray(wf.steps)) {
|
|
811
866
|
for (const step of wf.steps) {
|
|
812
867
|
if (step.expression && step.expression.endsWith(".js")) {
|
|
@@ -924,8 +979,16 @@ function mergeWorkflows(source, target, mappings) {
|
|
|
924
979
|
return {
|
|
925
980
|
...target,
|
|
926
981
|
...newSource,
|
|
927
|
-
openfn: {
|
|
928
|
-
|
|
982
|
+
openfn: {
|
|
983
|
+
...target.openfn,
|
|
984
|
+
...source.openfn,
|
|
985
|
+
// preserving the target uuid. we might need a proper helper function for this
|
|
986
|
+
uuid: target.openfn?.uuid
|
|
987
|
+
},
|
|
988
|
+
options: {
|
|
989
|
+
...target.options,
|
|
990
|
+
...source.options
|
|
991
|
+
}
|
|
929
992
|
};
|
|
930
993
|
}
|
|
931
994
|
|
|
@@ -1177,14 +1240,20 @@ function getDuplicates(arr) {
|
|
|
1177
1240
|
}
|
|
1178
1241
|
|
|
1179
1242
|
// src/merge/merge-project.ts
|
|
1243
|
+
var SANDBOX_MERGE = "sandbox";
|
|
1180
1244
|
var UnsafeMergeError = class extends Error {
|
|
1181
1245
|
};
|
|
1246
|
+
var defaultOptions = {
|
|
1247
|
+
workflowMappings: {},
|
|
1248
|
+
removeUnmapped: false,
|
|
1249
|
+
force: true,
|
|
1250
|
+
/**
|
|
1251
|
+
* If mode is sandbox, basically only content will be merged and all metadata/settings/options/config is ignored
|
|
1252
|
+
* If mode is replace, all properties on the source will override the target (including UUIDs, name)
|
|
1253
|
+
*/
|
|
1254
|
+
mode: SANDBOX_MERGE
|
|
1255
|
+
};
|
|
1182
1256
|
function merge(source, target, opts) {
|
|
1183
|
-
const defaultOptions = {
|
|
1184
|
-
workflowMappings: {},
|
|
1185
|
-
removeUnmapped: false,
|
|
1186
|
-
force: true
|
|
1187
|
-
};
|
|
1188
1257
|
const options = defaultsDeep(
|
|
1189
1258
|
opts,
|
|
1190
1259
|
defaultOptions
|
|
@@ -1245,13 +1314,47 @@ Pass --force to force the merge anyway`
|
|
|
1245
1314
|
}
|
|
1246
1315
|
}
|
|
1247
1316
|
}
|
|
1317
|
+
const assigns = options.mode === SANDBOX_MERGE ? {
|
|
1318
|
+
workflows: finalWorkflows
|
|
1319
|
+
} : {
|
|
1320
|
+
workflows: finalWorkflows,
|
|
1321
|
+
openfn: {
|
|
1322
|
+
...target.openfn,
|
|
1323
|
+
...source.openfn
|
|
1324
|
+
},
|
|
1325
|
+
options: {
|
|
1326
|
+
...target.options,
|
|
1327
|
+
...source.options
|
|
1328
|
+
},
|
|
1329
|
+
name: source.name ?? target.name,
|
|
1330
|
+
description: source.description ?? target.description,
|
|
1331
|
+
credentials: source.credentials ?? target.credentials,
|
|
1332
|
+
collections: source.collections ?? target.collections
|
|
1333
|
+
};
|
|
1248
1334
|
return new Project(
|
|
1249
|
-
baseMerge(target, source, ["collections"],
|
|
1250
|
-
workflows: finalWorkflows
|
|
1251
|
-
})
|
|
1335
|
+
baseMerge(target, source, ["collections"], assigns)
|
|
1252
1336
|
);
|
|
1253
1337
|
}
|
|
1254
1338
|
|
|
1339
|
+
// src/util/project-diff.ts
|
|
1340
|
+
function diff(a, b) {
|
|
1341
|
+
const diffs = [];
|
|
1342
|
+
for (const workflowA of a.workflows) {
|
|
1343
|
+
const workflowB = b.getWorkflow(workflowA.id);
|
|
1344
|
+
if (!workflowB) {
|
|
1345
|
+
diffs.push({ id: workflowA.id, type: "removed" });
|
|
1346
|
+
} else if (workflowA.getVersionHash() !== workflowB.getVersionHash()) {
|
|
1347
|
+
diffs.push({ id: workflowA.id, type: "changed" });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
for (const workflowB of b.workflows) {
|
|
1351
|
+
if (!a.getWorkflow(workflowB.id)) {
|
|
1352
|
+
diffs.push({ id: workflowB.id, type: "added" });
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return diffs;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1255
1358
|
// src/Project.ts
|
|
1256
1359
|
var maybeCreateWorkflow = (wf) => wf instanceof Workflow_default ? wf : new Workflow_default(wf);
|
|
1257
1360
|
var Project = class {
|
|
@@ -1279,6 +1382,7 @@ var Project = class {
|
|
|
1279
1382
|
config;
|
|
1280
1383
|
collections;
|
|
1281
1384
|
credentials;
|
|
1385
|
+
sandbox;
|
|
1282
1386
|
static async from(type, data, ...rest) {
|
|
1283
1387
|
switch (type) {
|
|
1284
1388
|
case "project":
|
|
@@ -1303,10 +1407,6 @@ var Project = class {
|
|
|
1303
1407
|
static merge(source, target, options) {
|
|
1304
1408
|
return merge(source, target, options);
|
|
1305
1409
|
}
|
|
1306
|
-
// env is excluded because it's not really part of the project
|
|
1307
|
-
// uh maybe
|
|
1308
|
-
// maybe this second arg is config - like env, branch rules, serialisation rules
|
|
1309
|
-
// stuff that's external to the actual project and managed by the repo
|
|
1310
1410
|
// TODO maybe the constructor is (data, Workspace)
|
|
1311
1411
|
constructor(data = {}, meta) {
|
|
1312
1412
|
this.id = data.id ?? (data.name ? slugify(data.name) : humanId({ separator: "-", capitalize: false }));
|
|
@@ -1325,6 +1425,7 @@ var Project = class {
|
|
|
1325
1425
|
this.workflows = data.workflows?.map(maybeCreateWorkflow) ?? [];
|
|
1326
1426
|
this.collections = data.collections;
|
|
1327
1427
|
this.credentials = data.credentials;
|
|
1428
|
+
this.sandbox = data.sandbox;
|
|
1328
1429
|
}
|
|
1329
1430
|
/** Local alias for the project. Comes from the file name. Not shared with Lightning. */
|
|
1330
1431
|
get alias() {
|
|
@@ -1384,6 +1485,10 @@ var Project = class {
|
|
|
1384
1485
|
}
|
|
1385
1486
|
return result;
|
|
1386
1487
|
}
|
|
1488
|
+
// Compare this project with another and return a list of workflow changes
|
|
1489
|
+
diff(project) {
|
|
1490
|
+
return diff(this, project);
|
|
1491
|
+
}
|
|
1387
1492
|
canMergeInto(target) {
|
|
1388
1493
|
const potentialConflicts = {};
|
|
1389
1494
|
for (const sourceWorkflow of this.workflows) {
|
|
@@ -1454,6 +1559,7 @@ var Workspace = class {
|
|
|
1454
1559
|
config;
|
|
1455
1560
|
// TODO activeProject should be the actual project
|
|
1456
1561
|
activeProject;
|
|
1562
|
+
root;
|
|
1457
1563
|
projects = [];
|
|
1458
1564
|
projectPaths = /* @__PURE__ */ new Map();
|
|
1459
1565
|
isValid = false;
|
|
@@ -1461,6 +1567,7 @@ var Workspace = class {
|
|
|
1461
1567
|
// Set validate to false to suppress warnings if a Workspace doesn't exist
|
|
1462
1568
|
// This is appropriate if, say, fetching a project for the first time
|
|
1463
1569
|
constructor(workspacePath, logger, validate = true) {
|
|
1570
|
+
this.root = workspacePath;
|
|
1464
1571
|
this.logger = logger ?? createLogger("Workspace", { level: "info" });
|
|
1465
1572
|
let context = { workspace: void 0, project: void 0 };
|
|
1466
1573
|
try {
|
|
@@ -1526,6 +1633,12 @@ var Workspace = class {
|
|
|
1526
1633
|
getActiveProject() {
|
|
1527
1634
|
return this.projects.find((p) => p.openfn?.uuid === this.activeProject?.uuid) ?? this.projects.find((p) => p.id === this.activeProject?.id);
|
|
1528
1635
|
}
|
|
1636
|
+
getCheckedOutProject() {
|
|
1637
|
+
return Project.from("fs", { root: this.root, config: this.config });
|
|
1638
|
+
}
|
|
1639
|
+
getCredentialMap() {
|
|
1640
|
+
return this.config.credentials;
|
|
1641
|
+
}
|
|
1529
1642
|
// TODO this needs to return default values
|
|
1530
1643
|
// We should always rely on the workspace to load these values
|
|
1531
1644
|
getConfig() {
|
|
@@ -1743,6 +1856,7 @@ var src_default = Project;
|
|
|
1743
1856
|
export {
|
|
1744
1857
|
Workspace,
|
|
1745
1858
|
src_default as default,
|
|
1859
|
+
diff,
|
|
1746
1860
|
generateProject,
|
|
1747
1861
|
generateWorkflow,
|
|
1748
1862
|
jsonToYaml,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfn/project",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Read, serialize, replicate and sync OpenFn projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"lodash-es": "^4.17.21",
|
|
35
35
|
"ohm-js": "^17.2.1",
|
|
36
36
|
"yaml": "^2.2.2",
|
|
37
|
-
"@openfn/lexicon": "^1.
|
|
37
|
+
"@openfn/lexicon": "^1.4.0",
|
|
38
38
|
"@openfn/logger": "1.1.1"
|
|
39
39
|
},
|
|
40
40
|
"files": [
|