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