@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 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 { id, name, openfn, steps, history, ...options } = workflow;
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.push(node);
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.push(node);
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.push(e);
394
+ wfState.edges[`${s.id}->${next}`] = e;
377
395
  });
378
396
  });
379
- wfState.edges = sortBy(wfState.edges, "id");
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
- const format = workspace.formats.openfn;
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
- proj.workflows = stateJson.workflows.map(mapWorkflow2);
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
- const connectedEdges = edges.filter(
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: "trigger",
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((j) => j.id === edge.target_job_id);
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((j) => j.id === edge.target_job_id);
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
- json = from_v2(rawJson);
754
- } else {
755
- json = from_v1(rawJson);
798
+ return new Project_default(from_v2(rawJson), config);
756
799
  }
757
- return new Project_default(json, config);
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 = `${root}/${workflowDir}/*/*.${fileType}`;
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
- const wf = fileType === "yaml" ? yamlToJson(candidate) : JSON.parse(candidate);
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: { ...target.openfn }
928
- // preserving the target uuid. we might need a proper helper function for this.
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.10.1",
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.3.0",
37
+ "@openfn/lexicon": "^1.4.0",
38
38
  "@openfn/logger": "1.1.1"
39
39
  },
40
40
  "files": [