@openfn/project 0.7.1 → 0.7.2

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +48 -59
  2. package/dist/index.js +181 -143
  3. package/package.json +4 -3
package/dist/index.d.ts CHANGED
@@ -1,72 +1,56 @@
1
- import * as l$1 from '@openfn/lexicon';
1
+ import * as l from '@openfn/lexicon';
2
+ import l__default, { WorkspaceConfig, UUID } from '@openfn/lexicon';
3
+ import { Provisioner } from '@openfn/lexicon/lightning';
2
4
 
3
- type OpenfnMeta = {
4
- uuid?: string;
5
- };
6
5
  type WithMeta<T> = T & {
7
- openfn?: OpenfnMeta;
6
+ openfn?: l.NodeMeta;
8
7
  };
9
8
  declare class Workflow {
10
- #private;
11
- workflow: l$1.Workflow;
12
- index: {
13
- steps: {};
14
- edges: {};
15
- uuid: {};
16
- id: {};
17
- };
9
+ workflow: l.Workflow;
10
+ index: any;
18
11
  name?: string;
19
12
  id: string;
20
- openfn: OpenfnMeta;
21
- constructor(workflow: l$1.Workflow);
22
- get steps(): WithMeta<l$1.Job | l$1.Trigger>[];
23
- set(id: string, props: Parital<l$1.Job, l$1.Edge>): this;
24
- get(id: any): WithMeta<l$1.Step | l$1.Trigger | l$1.Edge>;
25
- meta(id: any): OpenfnMeta;
26
- getEdge(from: any, to: any): WithMeta<l$1.ConditionalStepEdge>;
13
+ openfn?: l.WorkflowMeta;
14
+ options: any;
15
+ constructor(workflow: l.Workflow);
16
+ get steps(): WithMeta<l.Job & l.Trigger>[];
17
+ _buildIndex(): void;
18
+ set(id: string, props: Partial<l.Job | l.StepEdge>): this;
19
+ get(id: string): WithMeta<l.Step | l.Trigger | l.StepEdge>;
20
+ meta(id: string): l.WorkflowMeta;
21
+ getEdge(from: string, to: string): WithMeta<l.ConditionalStepEdge>;
27
22
  getAllEdges(): Record<string, string[]>;
28
23
  getStep(id: string): Workflow["steps"][number];
29
- getRoot(): (l$1.Trigger & {
30
- openfn?: OpenfnMeta;
31
- }) | undefined;
32
- getUUID(id: any): string;
33
- toJSON(): JSON.Object;
24
+ getRoot(): WithMeta<l.Job & l.Trigger> | undefined;
25
+ getUUID(id: string): string;
26
+ toJSON(): Object;
34
27
  getUUIDMap(): Record<string, string>;
35
28
  getVersionHash(): string;
36
29
  pushHistory(versionHash: string): void;
37
30
  canMergeInto(target: Workflow): boolean;
38
31
  }
39
32
 
40
- type FileFormats = 'yaml' | 'json';
41
- interface WorkspaceConfig {
42
- dirs: {
43
- workflows: string;
44
- projects: string;
45
- };
46
- formats: {
47
- openfn: FileFormats;
48
- project: FileFormats;
49
- workflow: FileFormats;
50
- };
51
- }
33
+ type fromAppStateConfig = Partial<l.WorkspaceConfig> & {
34
+ format?: 'yaml' | 'json';
35
+ };
52
36
 
53
- type FromPathConfig = {
54
- config: WorkspaceConfig;
37
+ type FromPathConfig = l.WorkspaceConfig & {
38
+ format: 'json' | 'yaml';
55
39
  };
56
40
 
57
41
  type FromFsConfig = {
58
42
  root: string;
59
43
  };
60
44
 
61
- type MergeProjectOptions = Partial<{
45
+ type MergeProjectOptions = {
62
46
  workflowMappings: Record<string, string>;
63
47
  removeUnmapped: boolean;
64
48
  force: boolean;
65
- }>;
49
+ };
66
50
 
67
51
  declare class Workspace {
68
- config?: WorkspaceConfig;
69
- activeProject: ProjectMeta;
52
+ config: l.WorkspaceConfig;
53
+ activeProject?: l.ProjectMeta;
70
54
  private projects;
71
55
  private projectPaths;
72
56
  private isValid;
@@ -77,11 +61,19 @@ declare class Workspace {
77
61
  get(id: string): Project | undefined;
78
62
  getProjectPath(id: string): string | undefined;
79
63
  getActiveProject(): Project | undefined;
80
- getConfig(): Partial<WorkspaceConfig>;
81
- get activeProjectId(): any;
64
+ getConfig(): Partial<l.WorkspaceConfig>;
65
+ get activeProjectId(): unknown;
82
66
  get valid(): boolean;
83
67
  }
84
68
 
69
+ type UUIDMap = {
70
+ [workflowId: string]: {
71
+ self?: UUID;
72
+ children: {
73
+ [nodeId: string]: UUID;
74
+ };
75
+ };
76
+ };
85
77
  declare class Project {
86
78
  /** Human readable project name. This corresponds to the label in Lightning */
87
79
  name?: string;
@@ -92,35 +84,31 @@ declare class Project {
92
84
  workflows: Workflow[];
93
85
  options: any;
94
86
  meta: any;
95
- openfn?: l.ProjectConfig;
87
+ openfn?: l__default.ProjectMeta;
96
88
  workspace?: Workspace;
97
- config: WorkspaceConfig;
89
+ config: l__default.WorkspaceConfig;
98
90
  collections: any;
99
- static from(type: 'state', data: any, options: Partial<l.ProjectConfig>): Project;
100
- static from(type: 'fs', options: FromFsConfig): Project;
91
+ credentials: string[];
92
+ static from(type: 'state', data: Provisioner.Project, meta?: Partial<l__default.ProjectMeta>, config?: fromAppStateConfig): Promise<Project>;
93
+ static from(type: 'fs', options: FromFsConfig): Promise<Project>;
101
94
  static from(type: 'path', data: string, options?: {
102
95
  config?: FromPathConfig;
103
- }): Project;
104
- static diff(a: Project, b: Project): void;
105
- static merge(source: Project, target: Project, options: MergeProjectOptions): Project;
106
- constructor(data: l.Project, config?: RepoOptions);
96
+ }): Promise<Project>;
97
+ static merge(source: Project, target: Project, options?: Partial<MergeProjectOptions>): Project;
98
+ constructor(data: Partial<l__default.Project>, config?: Partial<l__default.WorkspaceConfig>);
107
99
  setConfig(config: Partial<WorkspaceConfig>): void;
108
100
  serialize(type?: 'json' | 'yaml' | 'fs' | 'state', options?: any): any;
109
101
  getWorkflow(idOrName: string): Workflow | undefined;
110
102
  getIdentifier(): string;
111
- compare(proj: Project): void;
112
103
  getUUID(workflow: string | Workflow, stepId: string, otherStep?: string): any;
113
104
  /**
114
105
  * Returns a map of ids:uuids for everything in the project
115
106
  */
116
- getUUIDMap(options?: {
117
- workflows: boolean;
118
- project: false;
119
- }): {};
107
+ getUUIDMap(): UUIDMap;
120
108
  }
121
109
 
122
110
  declare function yamlToJson(y: string): any;
123
- declare function jsonToYaml(json: string | JSONObject): string;
111
+ declare function jsonToYaml(json: string | Object): string;
124
112
 
125
113
  type GenerateWorkflowOptions = {
126
114
  name: string;
@@ -131,6 +119,7 @@ type GenerateWorkflowOptions = {
131
119
  };
132
120
  type GenerateProjectOptions = GenerateWorkflowOptions & {
133
121
  uuidMap: Array<Record<string, string>>;
122
+ uuid?: string | number;
134
123
  };
135
124
  /**
136
125
  * Generate a Workflow from a simple text based representation
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { humanId } from "human-id";
9
9
 
10
10
  // src/util/slugify.ts
11
11
  function slugify(text) {
12
- return text?.replace(/\W/g, " ").trim().replace(/\s+/g, "-").toLowerCase();
12
+ return text?.replace(/\W/g, " ").trim().replace(/\s+/g, "-").toLowerCase() ?? "";
13
13
  }
14
14
 
15
15
  // src/util/version.ts
@@ -25,7 +25,7 @@ var generateHash = (workflow, source = "cli") => {
25
25
  "name",
26
26
  "adaptors",
27
27
  "adaptor",
28
- // there's ao adaptor & adaptors key in steps somehow.
28
+ // there's both adaptor & adaptors key in steps somehow
29
29
  "expression",
30
30
  "configuration",
31
31
  // assumes a string credential id
@@ -55,12 +55,13 @@ var generateHash = (workflow, source = "cli") => {
55
55
  }
56
56
  });
57
57
  if (step.next && Array.isArray(step.next)) {
58
- const edges = step.next.slice().sort((a, b) => {
58
+ const steps2 = step.next.slice();
59
+ steps2.slice().sort((a, b) => {
59
60
  const aLabel = a.label || "";
60
61
  const bLabel = b.label || "";
61
62
  return aLabel.localeCompare(bLabel);
62
63
  });
63
- for (const edge of edges) {
64
+ for (const edge of step.next) {
64
65
  edgeKeys.forEach((key) => {
65
66
  if (isDefined(edge[key])) {
66
67
  parts.push(key, serializeValue(edge[key]));
@@ -89,6 +90,8 @@ var Workflow = class {
89
90
  name;
90
91
  id;
91
92
  openfn;
93
+ options;
94
+ // TODO
92
95
  constructor(workflow) {
93
96
  this.index = {
94
97
  steps: {},
@@ -114,13 +117,14 @@ var Workflow = class {
114
117
  }
115
118
  this.openfn = openfn;
116
119
  this.options = options;
117
- this.#buildIndex();
120
+ this._buildIndex();
118
121
  }
119
122
  get steps() {
120
123
  return this.workflow.steps;
121
124
  }
122
- #buildIndex() {
123
- for (const s of this.workflow.steps) {
125
+ _buildIndex() {
126
+ for (const step of this.workflow.steps) {
127
+ const s = step;
124
128
  this.index.steps[s.id] = s;
125
129
  this.index.uuid[s.id] = s.openfn?.uuid;
126
130
  if (s.openfn?.uuid) {
@@ -174,7 +178,8 @@ var Workflow = class {
174
178
  }
175
179
  getAllEdges() {
176
180
  const edges = {};
177
- for (const step of this.steps) {
181
+ for (const s of this.steps) {
182
+ const step = s;
178
183
  const next = typeof step.next === "string" ? { [step.next]: true } : step.next || {};
179
184
  for (const toNode of Object.keys(next)) {
180
185
  if (!Array.isArray(edges[step.id]))
@@ -218,14 +223,10 @@ var Workflow = class {
218
223
  }
219
224
  // return true if the current workflow can be merged into the target workflow without losing any changes
220
225
  canMergeInto(target) {
221
- const thisHistory = this.workflow.history?.concat(this.getVersionHash());
222
- const targetHistory = target.workflow.history?.concat(
223
- target.getVersionHash()
224
- );
226
+ const thisHistory = this.workflow.history?.concat(this.getVersionHash()) ?? [];
227
+ const targetHistory = target.workflow.history?.concat(target.getVersionHash()) ?? [];
225
228
  const targetHead = targetHistory[targetHistory.length - 1];
226
- if (thisHistory.indexOf(targetHead) > -1)
227
- return true;
228
- return false;
229
+ return thisHistory.indexOf(targetHead) > -1;
229
230
  }
230
231
  };
231
232
  var Workflow_default = Workflow;
@@ -243,11 +244,12 @@ function to_json_default(project) {
243
244
  return {
244
245
  // There must be a better way to do this?
245
246
  // Do we just serialize all public fields?
247
+ id: project.id,
246
248
  name: project.name,
247
249
  description: project.description,
248
250
  config: project.config,
249
251
  meta: project.meta,
250
- workflows: project.workflows,
252
+ workflows: project.workflows.map((w) => w.toJSON()),
251
253
  collections: project.collections,
252
254
  credentials: project.credentials,
253
255
  openfn: project.openfn,
@@ -255,6 +257,10 @@ function to_json_default(project) {
255
257
  };
256
258
  }
257
259
 
260
+ // src/serialize/to-app-state.ts
261
+ import { pick, omitBy, isNil } from "lodash-es";
262
+ import { randomUUID } from "node:crypto";
263
+
258
264
  // src/util/rename-keys.ts
259
265
  function renameKeys(props = {}, keyMap) {
260
266
  return Object.fromEntries(
@@ -280,19 +286,16 @@ function jsonToYaml(json) {
280
286
  }
281
287
 
282
288
  // src/serialize/to-app-state.ts
283
- import { randomUUID } from "node:crypto";
284
289
  function to_app_state_default(project, options = {}) {
285
- const { uuid: id, endpoint, env, ...rest } = project.openfn ?? {};
286
- const state = {
287
- id,
288
- name: project.name,
289
- description: project.description,
290
- project_credentials: project.credentials,
291
- collections: project.collections,
292
- ...rest,
293
- ...project.options,
294
- workflows: project.workflows.map(mapWorkflow)
295
- };
290
+ const { uuid, endpoint, env, ...rest } = project.openfn ?? {};
291
+ const state = omitBy(
292
+ pick(project, ["name", "description", "collections"]),
293
+ isNil
294
+ );
295
+ state.id = uuid;
296
+ Object.assign(state, rest, project.options);
297
+ state.project_credentials = project.credentials ?? [];
298
+ state.workflows = project.workflows.map(mapWorkflow);
296
299
  const shouldReturnYaml = options.format === "yaml" || !options.format && project.config.formats.project === "yaml";
297
300
  if (shouldReturnYaml) {
298
301
  return jsonToYaml(state);
@@ -307,11 +310,15 @@ var mapWorkflow = (workflow) => {
307
310
  const wfState = {
308
311
  ...originalOpenfnProps,
309
312
  id: workflow.openfn?.uuid ?? randomUUID(),
310
- name: workflow.name,
311
313
  jobs: [],
312
314
  triggers: [],
313
- edges: []
315
+ edges: [],
316
+ lock_version: workflow.openfn?.lock_version ?? null
317
+ // TODO needs testing
314
318
  };
319
+ if (workflow.name) {
320
+ wfState.name = workflow.name;
321
+ }
315
322
  const lookup = workflow.steps.reduce((obj, next) => {
316
323
  if (!next.openfn?.uuid) {
317
324
  next.openfn ??= {};
@@ -321,7 +328,7 @@ var mapWorkflow = (workflow) => {
321
328
  return obj;
322
329
  }, {});
323
330
  workflow.steps.forEach((s) => {
324
- let isTrigger;
331
+ let isTrigger = false;
325
332
  let node;
326
333
  if (s.type && !s.expression) {
327
334
  isTrigger = true;
@@ -331,23 +338,28 @@ var mapWorkflow = (workflow) => {
331
338
  };
332
339
  wfState.triggers.push(node);
333
340
  } else {
334
- node = {
335
- name: s.name,
336
- body: s.expression,
337
- adaptor: s.adaptor,
338
- ...renameKeys(s.openfn, { uuid: "id" })
339
- };
341
+ node = omitBy(pick(s, ["name", "adaptor"]), isNil);
342
+ const { uuid: uuid2, ...otherOpenFnProps } = s.openfn ?? {};
343
+ node.id = uuid2;
344
+ Object.assign(node, otherOpenFnProps);
345
+ if (s.expression) {
346
+ node.body = s.expression;
347
+ }
348
+ node.project_credential_id = s.openfn?.project_credential_id ?? null;
349
+ node.keychain_credential_id = null;
340
350
  wfState.jobs.push(node);
341
351
  }
342
352
  Object.keys(s.next ?? {}).forEach((next) => {
343
353
  const rules = s.next[next];
354
+ const { uuid: uuid2, ...otherOpenFnProps } = rules.openfn ?? {};
344
355
  const e = {
345
- id: rules.openfn?.uuid ?? randomUUID(),
356
+ id: uuid2 ?? randomUUID(),
346
357
  target_job_id: lookup[next],
347
358
  enabled: !rules.disabled,
348
359
  source_trigger_id: null
349
360
  // lightning complains if this isn't set, even if its falsy :(
350
361
  };
362
+ Object.assign(e, otherOpenFnProps);
351
363
  if (isTrigger) {
352
364
  e.source_trigger_id = node.id;
353
365
  } else {
@@ -371,13 +383,12 @@ import nodepath from "path";
371
383
  // src/util/config.ts
372
384
  import { readFileSync } from "node:fs";
373
385
  import path from "node:path";
374
- import { pickBy, isNil } from "lodash-es";
386
+ import { pickBy, isNil as isNil2 } from "lodash-es";
375
387
  var buildConfig = (config = {}) => ({
376
388
  ...config,
377
389
  dirs: {
378
- projects: ".projects",
379
- // TODO change to projects
380
- workflows: "workflows"
390
+ projects: config.dirs?.projects ?? ".projects",
391
+ workflows: config.dirs?.workflows ?? "workflows"
381
392
  },
382
393
  formats: {
383
394
  openfn: config.formats?.openfn ?? "yaml",
@@ -433,7 +444,7 @@ var loadWorkspaceFile = (contents, format = "yaml") => {
433
444
  formats,
434
445
  dirs
435
446
  },
436
- (value) => !isNil(value)
447
+ (value) => !isNil2(value)
437
448
  );
438
449
  } else {
439
450
  project = json.project ?? {};
@@ -454,7 +465,6 @@ var findWorkspaceFile = (dir = ".") => {
454
465
  content = JSON.parse(file);
455
466
  }
456
467
  } catch (e2) {
457
- console.log(e2);
458
468
  throw e2;
459
469
  }
460
470
  }
@@ -480,13 +490,13 @@ function to_fs_default(project) {
480
490
  }
481
491
  return files;
482
492
  }
483
- var extractWorkflow = (project, workflowId2) => {
493
+ var extractWorkflow = (project, workflowId) => {
484
494
  const format = project.config.formats.workflow;
485
- const workflow = project.getWorkflow(workflowId2);
495
+ const workflow = project.getWorkflow(workflowId);
486
496
  if (!workflow) {
487
- throw new Error(`workflow not found: ${workflowId2}`);
497
+ throw new Error(`workflow not found: ${workflowId}`);
488
498
  }
489
- const root = project.config.dirs.workflow ?? project.config.workflowRoot ?? "workflows/";
499
+ const root = project.config.dirs.workflows ?? project.config.workflowRoot ?? "workflows/";
490
500
  const path5 = nodepath.join(root, workflow.id, workflow.id);
491
501
  const wf = {
492
502
  id: workflow.id,
@@ -504,17 +514,17 @@ var extractWorkflow = (project, workflowId2) => {
504
514
  };
505
515
  return handleOutput(wf, path5, format);
506
516
  };
507
- var extractStep = (project, workflowId2, stepId) => {
508
- const workflow = project.getWorkflow(workflowId2);
517
+ var extractStep = (project, workflowId, stepId) => {
518
+ const workflow = project.getWorkflow(workflowId);
509
519
  if (!workflow) {
510
- throw new Error(`workflow not found: ${workflowId2}`);
520
+ throw new Error(`workflow not found: ${workflowId}`);
511
521
  }
512
522
  const step = workflow.steps.find((s) => s.id === stepId);
513
523
  if (!step) {
514
524
  throw new Error(`step not found: ${stepId}`);
515
525
  }
516
526
  if (step.expression) {
517
- const root = project.config?.workflowRoot ?? "workflows/";
527
+ const root = project.config?.dirs.workflows ?? project.config?.workflowRoot ?? "workflows/";
518
528
  const path5 = nodepath.join(root, `${workflow.id}/${step.id}.js`);
519
529
  const content = step.expression;
520
530
  return { path: path5, content };
@@ -534,17 +544,18 @@ var handleOutput = (data, filePath, format) => {
534
544
  };
535
545
 
536
546
  // src/parse/from-app-state.ts
537
- function slugify2(text) {
538
- return text.replace(/\W/g, " ").trim().replace(/\s+/g, "-").toLowerCase();
539
- }
540
- var from_app_state_default = (state, config) => {
547
+ var from_app_state_default = (state, meta, config = {}) => {
548
+ let stateJson;
541
549
  if (typeof state === "string") {
542
- if (config?.format === "yaml") {
543
- state = yamlToJson(state);
550
+ if (config.format === "yaml") {
551
+ stateJson = yamlToJson(state);
544
552
  } else {
545
- state = JSON.parse(state);
553
+ stateJson = JSON.parse(state);
546
554
  }
555
+ } else {
556
+ stateJson = state;
547
557
  }
558
+ delete config.format;
548
559
  const {
549
560
  id,
550
561
  name,
@@ -555,27 +566,25 @@ var from_app_state_default = (state, config) => {
555
566
  inserted_at,
556
567
  updated_at,
557
568
  ...options
558
- } = state;
569
+ } = stateJson;
559
570
  const proj = {
560
571
  name,
561
- description,
572
+ description: description ?? void 0,
562
573
  collections,
563
574
  credentials,
564
- options
575
+ options,
576
+ config
565
577
  };
578
+ const { id: _ignore, ...restMeta } = meta;
566
579
  proj.openfn = {
580
+ // @ts-ignore
567
581
  uuid: id,
568
- name,
569
- endpoint: config.endpoint,
570
- env: config.env,
582
+ ...restMeta,
571
583
  inserted_at,
572
584
  updated_at
573
585
  };
574
- proj.meta = {
575
- fetched_at: config.fetchedAt
576
- };
577
- proj.workflows = state.workflows.map(mapWorkflow2);
578
- return new Project(proj, config?.config);
586
+ proj.workflows = stateJson.workflows.map(mapWorkflow2);
587
+ return new Project(proj, config);
579
588
  };
580
589
  var mapTriggerEdgeCondition = (edge) => {
581
590
  const e = {
@@ -601,7 +610,7 @@ var mapWorkflow2 = (workflow) => {
601
610
  openfn: renameKeys(remoteProps, { id: "uuid" })
602
611
  };
603
612
  if (workflow.name) {
604
- mapped.id = slugify2(workflow.name);
613
+ mapped.id = slugify(workflow.name);
605
614
  }
606
615
  workflow.triggers.forEach((trigger) => {
607
616
  const { type, ...otherProps } = trigger;
@@ -617,7 +626,7 @@ var mapWorkflow2 = (workflow) => {
617
626
  if (!target) {
618
627
  throw new Error(`Failed to find ${edge.target_job_id}`);
619
628
  }
620
- obj[slugify2(target.name)] = mapTriggerEdgeCondition(edge);
629
+ obj[slugify(target.name)] = mapTriggerEdgeCondition(edge);
621
630
  return obj;
622
631
  }, {})
623
632
  });
@@ -628,16 +637,17 @@ var mapWorkflow2 = (workflow) => {
628
637
  );
629
638
  const { body: expression, name: name2, adaptor, ...remoteProps2 } = step;
630
639
  const s = {
631
- id: slugify2(name2),
640
+ id: slugify(name2),
632
641
  name: name2,
633
642
  expression,
634
643
  adaptor,
644
+ // TODO is this wrong?
635
645
  openfn: renameKeys(remoteProps2, { id: "uuid" })
636
646
  };
637
647
  if (outboundEdges.length) {
638
648
  s.next = outboundEdges.reduce((next, edge) => {
639
649
  const target = jobs.find((j) => j.id === edge.target_job_id);
640
- next[slugify2(target.name)] = mapTriggerEdgeCondition(edge);
650
+ next[slugify(target.name)] = mapTriggerEdgeCondition(edge);
641
651
  return next;
642
652
  }, {});
643
653
  }
@@ -649,13 +659,10 @@ var mapWorkflow2 = (workflow) => {
649
659
  // src/parse/from-path.ts
650
660
  import { extname } from "node:path";
651
661
  import { readFile } from "node:fs/promises";
652
- var from_path_default = async (path5, options = {}) => {
662
+ import { omit } from "lodash-es";
663
+ var from_path_default = async (path5, config = {}) => {
653
664
  const ext = extname(path5).toLowerCase();
654
665
  const source = await readFile(path5, "utf8");
655
- const config = {
656
- format: null,
657
- config: options.config
658
- };
659
666
  let state;
660
667
  if (ext === ".json") {
661
668
  config.format = "json";
@@ -666,7 +673,8 @@ var from_path_default = async (path5, options = {}) => {
666
673
  } else {
667
674
  throw new Error(`Cannot load a project from a ${ext} file`);
668
675
  }
669
- return from_app_state_default(state, config);
676
+ const meta = {};
677
+ return from_app_state_default(state, meta, omit(config, ["format"]));
670
678
  };
671
679
 
672
680
  // src/parse/from-fs.ts
@@ -688,18 +696,18 @@ var get_identifier_default = (config = {}) => {
688
696
  };
689
697
 
690
698
  // src/parse/from-fs.ts
691
- var parseProject = async (options = {}) => {
699
+ var parseProject = async (options) => {
692
700
  const { root } = options;
693
701
  const { type, content } = findWorkspaceFile(root);
694
702
  const context = loadWorkspaceFile(content, type);
695
703
  const config = buildConfig(context.workspace);
696
- let state;
704
+ let state = null;
697
705
  const identifier = get_identifier_default({
698
706
  endpoint: context.project?.endpoint,
699
707
  env: context.project?.env
700
708
  });
701
709
  try {
702
- const format = config.formats?.project ?? config.formats?.projects ?? "yaml";
710
+ const format = config.formats?.project ?? config.formats?.project ?? "yaml";
703
711
  const statePath = path2.join(
704
712
  root,
705
713
  config.dirs?.projects ?? ".projects",
@@ -722,7 +730,6 @@ var parseProject = async (options = {}) => {
722
730
  const candidateWfs = await glob(pattern, {
723
731
  ignore: ["**node_modules/**", "**tmp**"]
724
732
  });
725
- const workflows = [];
726
733
  for (const filePath of candidateWfs) {
727
734
  const candidate = await fs.readFile(filePath, "utf-8");
728
735
  try {
@@ -783,9 +790,7 @@ var getUuidForStep = (project, workflow, stepId) => {
783
790
  var getUuidForEdge = (project, workflow, from, to) => {
784
791
  const wf = typeof workflow === "string" ? project.getWorkflow(workflow) : workflow;
785
792
  if (!wf) {
786
- throw new Error(
787
- `Workflow "${workflowId} not found in project ${project.id}`
788
- );
793
+ throw new Error(`Workflow "${workflow} not found in project ${project.id}`);
789
794
  }
790
795
  for (const step of wf.steps) {
791
796
  if (step.id === from) {
@@ -804,43 +809,48 @@ var getUuidForEdge = (project, workflow, from, to) => {
804
809
  import { defaultsDeep, isEmpty } from "lodash-es";
805
810
 
806
811
  // src/util/base-merge.ts
807
- import { pick, assign } from "lodash-es";
812
+ import { pick as pick2, assign } from "lodash-es";
808
813
  function baseMerge(target, source, sourceKeys, assigns = {}) {
809
- const pickedSource = sourceKeys ? pick(source, sourceKeys) : source;
814
+ const pickedSource = sourceKeys ? pick2(source, sourceKeys) : source;
810
815
  return assign(target, { ...pickedSource, ...assigns });
811
816
  }
812
817
 
813
818
  // src/merge/merge-node.ts
819
+ var clone2 = (obj) => JSON.parse(JSON.stringify(obj));
814
820
  function mergeWorkflows(source, target, mappings) {
815
821
  const targetNodes = {};
816
- for (const tstep2 of target.steps)
817
- targetNodes[tstep2.openfn.uuid || tstep2.id] = tstep2;
822
+ for (const targetStep of target.steps) {
823
+ targetNodes[targetStep.openfn?.uuid || targetStep.id] = targetStep;
824
+ }
818
825
  const steps = [];
819
- for (const sstep of source.steps) {
820
- let newNode = sstep;
821
- if (typeof mappings.nodes[sstep.id] === "string") {
822
- const preservedId = mappings.nodes[sstep.id];
823
- const preservedEdgeIds = {};
824
- for (const toNode of Object.keys(
825
- typeof sstep.next === "string" ? { [tstep.next]: true } : sstep.next || {}
826
- )) {
827
- const key = sstep.id + "-" + toNode;
828
- if (typeof mappings.edges[key] === "string") {
826
+ for (const sourceStep of source.steps) {
827
+ let newNode = clone2(sourceStep);
828
+ if (sourceStep.id in mappings.nodes) {
829
+ const preservedId = mappings.nodes[sourceStep.id];
830
+ const toNodeIds = Object.keys(
831
+ typeof sourceStep.next === "string" ? { [sourceStep.next]: true } : sourceStep.next || {}
832
+ );
833
+ for (const toNode of toNodeIds) {
834
+ const key = sourceStep.id + "-" + toNode;
835
+ if (key in mappings.edges) {
829
836
  const preservedEdgeId = mappings.edges[key];
830
- const toEdge = sstep.next?.[toNode] || {};
831
- preservedEdgeIds[toNode] = sstep.next[toNode] = {
832
- ...toEdge,
833
- openfn: { ...toEdge?.openfn || {}, uuid: preservedEdgeId }
837
+ const edge = sourceStep.next?.[toNode] || {};
838
+ sourceStep.next[toNode] = {
839
+ ...edge,
840
+ openfn: Object.assign({}, edge?.openfn, {
841
+ uuid: preservedEdgeId
842
+ })
834
843
  };
835
844
  }
836
845
  }
837
- newNode = baseMerge(targetNodes[preservedId], sstep, [
846
+ newNode = baseMerge(targetNodes[preservedId], sourceStep, [
838
847
  "id",
839
848
  "name",
849
+ // @ts-ignore
840
850
  "adaptor",
851
+ "adaptors",
841
852
  "expression",
842
- "next",
843
- "previous"
853
+ "next"
844
854
  ]);
845
855
  } else {
846
856
  }
@@ -964,7 +974,7 @@ function findBestMatch(sourceStep, candidates, sourceEdges, targetEdges, getMapp
964
974
  }
965
975
  return null;
966
976
  }
967
- function mapEdges(sourceEdges, targetEdges, idMap, getTargetUUID) {
977
+ function mapEdges(sourceEdges, _targetEdges, idMap, getTargetUUID) {
968
978
  const edgeMapping = {};
969
979
  for (const [parentId, children] of Object.entries(sourceEdges)) {
970
980
  for (const childId of children) {
@@ -980,7 +990,7 @@ function mapEdges(sourceEdges, targetEdges, idMap, getTargetUUID) {
980
990
  return edgeMapping;
981
991
  }
982
992
  function getStepUuid(step) {
983
- return step?.openfn?.uuid || step.id;
993
+ return step?.openfn?.uuid;
984
994
  }
985
995
  function mapStepsById(source, target) {
986
996
  const targetIndex = {};
@@ -1085,8 +1095,7 @@ function mapStepByChildren(sourceStep, candidates, sourceEdges, targetEdges, get
1085
1095
  };
1086
1096
  }
1087
1097
  function mapStepByExpression(sourceStep, candidates) {
1088
- const expression = sourceStep.expression;
1089
- return findByExpression(expression, candidates);
1098
+ return findByExpression(sourceStep.expression, candidates);
1090
1099
  }
1091
1100
 
1092
1101
  // src/util/get-duplicates.ts
@@ -1104,15 +1113,20 @@ function getDuplicates(arr) {
1104
1113
  }
1105
1114
 
1106
1115
  // src/merge/merge-project.ts
1107
- function merge(source, target, options) {
1116
+ var UnsafeMergeError = class extends Error {
1117
+ };
1118
+ function merge(source, target, opts) {
1108
1119
  const defaultOptions = {
1109
1120
  workflowMappings: {},
1110
1121
  removeUnmapped: false,
1111
1122
  force: true
1112
1123
  };
1113
- options = defaultsDeep(options, defaultOptions);
1124
+ const options = defaultsDeep(
1125
+ opts,
1126
+ defaultOptions
1127
+ );
1114
1128
  const dupTargetMappings = getDuplicates(
1115
- Object.values(options?.workflowMappings)
1129
+ Object.values(options.workflowMappings ?? {})
1116
1130
  );
1117
1131
  if (dupTargetMappings.length) {
1118
1132
  throw new Error(
@@ -1134,11 +1148,11 @@ function merge(source, target, options) {
1134
1148
  const targetId = options.workflowMappings?.[sourceWorkflow.id] ?? sourceWorkflow.id;
1135
1149
  const targetWorkflow = target.getWorkflow(targetId);
1136
1150
  if (targetWorkflow && !sourceWorkflow.canMergeInto(targetWorkflow)) {
1137
- potentialConflicts[sourceWorkflow.name] = targetWorkflow?.name;
1151
+ potentialConflicts[sourceWorkflow.id] = targetWorkflow?.id;
1138
1152
  }
1139
1153
  }
1140
1154
  if (Object.keys(potentialConflicts).length && !options?.force) {
1141
- throw new Error(
1155
+ throw new UnsafeMergeError(
1142
1156
  `The below workflows can't be merged directly without losing data
1143
1157
  ${Object.entries(
1144
1158
  potentialConflicts
@@ -1153,6 +1167,7 @@ Pass --force to force the merge anyway`
1153
1167
  usedTargetIds.add(targetWorkflow.id);
1154
1168
  const mappings = map_uuids_default(sourceWorkflow, targetWorkflow);
1155
1169
  finalWorkflows.push(
1170
+ // @ts-ignore
1156
1171
  mergeWorkflows(sourceWorkflow, targetWorkflow, mappings)
1157
1172
  );
1158
1173
  } else {
@@ -1167,7 +1182,9 @@ Pass --force to force the merge anyway`
1167
1182
  }
1168
1183
  }
1169
1184
  return new Project(
1170
- baseMerge(target, source, ["collections"], { workflows: finalWorkflows })
1185
+ baseMerge(target, source, ["collections"], {
1186
+ workflows: finalWorkflows
1187
+ })
1171
1188
  );
1172
1189
  }
1173
1190
 
@@ -1196,19 +1213,19 @@ var Project = class {
1196
1213
  workspace;
1197
1214
  config;
1198
1215
  collections;
1199
- static from(type, data, options = {}) {
1216
+ credentials;
1217
+ static async from(type, data, ...rest) {
1200
1218
  if (type === "state") {
1201
- return from_app_state_default(data, options);
1219
+ return from_app_state_default(data, rest[0], rest[1]);
1202
1220
  } else if (type === "fs") {
1203
- return parseProject(data, options);
1221
+ return parseProject(data);
1204
1222
  } else if (type === "path") {
1205
- return from_path_default(data, options);
1223
+ return from_path_default(data, rest[0]);
1206
1224
  }
1207
1225
  throw new Error(`Didn't recognize type ${type}`);
1208
1226
  }
1209
1227
  // Diff two projects
1210
- static diff(a, b) {
1211
- }
1228
+ // /static diff(a: Project, b: Project) {}
1212
1229
  // Merge a source project (staging) into the target project (main)
1213
1230
  // Returns a new Project
1214
1231
  // TODO: throw if histories have diverged
@@ -1220,17 +1237,16 @@ var Project = class {
1220
1237
  // maybe this second arg is config - like env, branch rules, serialisation rules
1221
1238
  // stuff that's external to the actual project and managed by the repo
1222
1239
  // TODO maybe the constructor is (data, Workspace)
1223
- constructor(data, config = {}) {
1224
- this.setConfig(config);
1225
- this.id = data.id ?? data.name ? slugify(data.name) : humanId({ separator: "-", capitalize: false });
1240
+ constructor(data, config) {
1241
+ this.config = buildConfig(config);
1242
+ this.id = data.id ?? (data.name ? slugify(data.name) : humanId({ separator: "-", capitalize: false }));
1226
1243
  this.name = data.name;
1227
- this.description = data.description;
1244
+ this.description = data.description ?? void 0;
1228
1245
  this.openfn = data.openfn;
1229
1246
  this.options = data.options;
1230
1247
  this.workflows = data.workflows?.map(maybeCreateWorkflow) ?? [];
1231
1248
  this.collections = data.collections;
1232
1249
  this.credentials = data.credentials;
1233
- this.meta = data.meta;
1234
1250
  }
1235
1251
  setConfig(config) {
1236
1252
  this.config = buildConfig(config);
@@ -1252,8 +1268,7 @@ var Project = class {
1252
1268
  return get_identifier_default(this.openfn);
1253
1269
  }
1254
1270
  // Compare this project with another and return a diff
1255
- compare(proj) {
1256
- }
1271
+ // compare(proj: Project) {}
1257
1272
  // find the UUID for a given node or edge
1258
1273
  // returns null if it doesn't exist
1259
1274
  getUUID(workflow, stepId, otherStep) {
@@ -1265,7 +1280,7 @@ var Project = class {
1265
1280
  /**
1266
1281
  * Returns a map of ids:uuids for everything in the project
1267
1282
  */
1268
- getUUIDMap(options = {}) {
1283
+ getUUIDMap() {
1269
1284
  const result = {};
1270
1285
  for (const wf of this.workflows) {
1271
1286
  result[wf.id] = {
@@ -1298,8 +1313,8 @@ function pathExists(fpath, type) {
1298
1313
  }
1299
1314
 
1300
1315
  // src/Workspace.ts
1301
- var PROJECT_EXTENSIONS = [".yaml", ".yml"];
1302
1316
  var Workspace = class {
1317
+ // @ts-ignore config not defininitely assigned - it sure is
1303
1318
  config;
1304
1319
  activeProject;
1305
1320
  projects = [];
@@ -1312,22 +1327,35 @@ var Workspace = class {
1312
1327
  context = loadWorkspaceFile(content, type);
1313
1328
  this.isValid = true;
1314
1329
  } catch (e) {
1315
- console.log(e);
1330
+ console.error(e);
1316
1331
  return;
1317
1332
  }
1318
1333
  this.config = buildConfig(context.workspace);
1319
1334
  this.activeProject = context.project;
1320
1335
  const projectsPath = path3.join(workspacePath, this.config.dirs.projects);
1321
1336
  if (this.isValid && pathExists(projectsPath, "directory")) {
1337
+ const ext = `.${this.config.formats.project}`;
1322
1338
  const stateFiles = fs3.readdirSync(projectsPath).filter(
1323
- (fileName) => PROJECT_EXTENSIONS.includes(path3.extname(fileName)) && path3.parse(fileName).name !== "openfn"
1339
+ (fileName) => path3.extname(fileName) === ext && path3.parse(fileName).name !== "openfn"
1324
1340
  );
1325
1341
  this.projects = stateFiles.map((file) => {
1326
1342
  const stateFilePath = path3.join(projectsPath, file);
1327
- const data = fs3.readFileSync(stateFilePath, "utf-8");
1328
- const project = from_app_state_default(data, { format: "yaml" });
1329
- this.projectPaths.set(project.id, stateFilePath);
1330
- return project;
1343
+ try {
1344
+ const data = fs3.readFileSync(stateFilePath, "utf-8");
1345
+ const project = from_app_state_default(
1346
+ data,
1347
+ {},
1348
+ {
1349
+ ...this.config,
1350
+ format: this.config?.formats.project
1351
+ }
1352
+ );
1353
+ this.projectPaths.set(project.id, stateFilePath);
1354
+ return project;
1355
+ } catch (e) {
1356
+ console.warn(`Failed to load project from ${stateFilePath}`);
1357
+ console.warn(e);
1358
+ }
1331
1359
  }).filter((s) => s);
1332
1360
  }
1333
1361
  }
@@ -1349,7 +1377,7 @@ var Workspace = class {
1349
1377
  return this.projectPaths.get(id);
1350
1378
  }
1351
1379
  getActiveProject() {
1352
- return this.projects.find((p) => p.id === this.activeProject?.id) ?? this.projects.find((p) => p.openfn?.uuid === this.activeProject?.id);
1380
+ return this.projects.find((p) => p.id === this.activeProject?.id) ?? this.projects.find((p) => p.openfn?.uuid === this.activeProject?.uuid);
1353
1381
  }
1354
1382
  // TODO this needs to return default values
1355
1383
  // We should always rely on the workspace to load these values
@@ -1369,6 +1397,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
1369
1397
  import path4 from "node:path";
1370
1398
  import { readFileSync as readFileSync2 } from "node:fs";
1371
1399
  import { grammar } from "ohm-js";
1400
+ import { isNil as isNil3 } from "lodash-es";
1372
1401
  var parser;
1373
1402
  var initOperations = (options = {}) => {
1374
1403
  let nodes = {};
@@ -1488,6 +1517,10 @@ function generateWorkflow(def, options = {}) {
1488
1517
  if (!parser) {
1489
1518
  parser = createParser();
1490
1519
  }
1520
+ let uuid;
1521
+ if (options.openfnUuid) {
1522
+ uuid = options.uuidSeed ? options.uuidSeed++ : randomUUID2();
1523
+ }
1491
1524
  const raw = parser.parse(def, options);
1492
1525
  if (!raw.name) {
1493
1526
  raw.name = "Workflow";
@@ -1495,9 +1528,12 @@ function generateWorkflow(def, options = {}) {
1495
1528
  if (!raw.id) {
1496
1529
  raw.id = "workflow";
1497
1530
  }
1498
- if (options.openfnUuid) {
1531
+ if (options.uuidMap && raw.id in options.uuidMap) {
1532
+ uuid = options.uuidMap[raw.id];
1533
+ }
1534
+ if (!isNil3(uuid) && options.openfnUuid) {
1499
1535
  raw.openfn ??= {};
1500
- raw.openfn.uuid = randomUUID2();
1536
+ raw.openfn.uuid = uuid;
1501
1537
  }
1502
1538
  const wf = new Workflow_default(raw);
1503
1539
  return wf;
@@ -1512,7 +1548,9 @@ function generateProject(name, workflowDefs, options = {}) {
1512
1548
  return new Project_default({
1513
1549
  name,
1514
1550
  workflows,
1515
- openfn: options.openfnUuid && { uuid: randomUUID2() }
1551
+ openfn: {
1552
+ uuid: options.uuid ?? (options.openfnUuid ? randomUUID2() : void 0)
1553
+ }
1516
1554
  });
1517
1555
  }
1518
1556
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfn/project",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Read, serialize, replicate and sync OpenFn projects",
5
5
  "type": "module",
6
6
  "exports": {
@@ -27,14 +27,15 @@
27
27
  "typescript": "^5.9.2"
28
28
  },
29
29
  "dependencies": {
30
+ "@types/lodash-es": "^4.17.12",
30
31
  "glob": "^11.0.2",
31
32
  "human-id": "^4.1.1",
32
33
  "lodash": "^4.17.21",
33
34
  "lodash-es": "^4.17.21",
34
35
  "ohm-js": "^17.2.1",
35
36
  "yaml": "^2.2.2",
36
- "@openfn/lexicon": "^1.2.5",
37
- "@openfn/logger": "1.0.6"
37
+ "@openfn/logger": "1.0.6",
38
+ "@openfn/lexicon": "^1.2.6"
38
39
  },
39
40
  "files": [
40
41
  "dist",