@openfn/project 0.5.0 → 0.5.1

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
@@ -51,10 +51,10 @@ a-b
51
51
  a-c
52
52
  ```
53
53
 
54
- You can set properties on the workflow itself - probably the name, with `@attributes`
54
+ You can set properties on the workflow itself - probably the id, with `@attributes`
55
55
 
56
56
  ```
57
- @name my-cool-workflow
57
+ @id my-cool-workflow
58
58
  ```
59
59
 
60
60
  You can also set properties on a node by putting comma seperated key-value pairs in brackets
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ declare class Workflow {
15
15
  uuid: {};
16
16
  id: {};
17
17
  };
18
- name: string;
18
+ name?: string;
19
19
  id: string;
20
20
  openfn: OpenfnMeta;
21
21
  constructor(workflow: l.Workflow);
@@ -31,6 +31,7 @@ declare class Workflow {
31
31
  }) | undefined;
32
32
  getUUID(id: any): string;
33
33
  toJSON(): JSON.Object;
34
+ getUUIDMap(): Record<string, string>;
34
35
  }
35
36
 
36
37
  type FromFsConfig = {
@@ -47,6 +48,10 @@ type FileFormats = 'yaml' | 'json';
47
48
  interface OpenfnConfig {
48
49
  name: string;
49
50
  workflowRoot: string;
51
+ dirs: {
52
+ workflows: string;
53
+ projects: string;
54
+ };
50
55
  formats: {
51
56
  openfn: FileFormats;
52
57
  project: FileFormats;
@@ -82,7 +87,9 @@ declare class Project {
82
87
  collections: any;
83
88
  static from(type: 'state', data: any, options: Partial<l.ProjectConfig>): Project;
84
89
  static from(type: 'fs', options: FromFsConfig): Project;
85
- static from(type: 'path', data: any): Project;
90
+ static from(type: 'path', data: string, options?: {
91
+ config?: Partial<OpenfnConfig>;
92
+ }): Project;
86
93
  static diff(a: Project, b: Project): void;
87
94
  static merge(source: Project, target: Project, options: MergeProjectOptions): Project;
88
95
  constructor(data: l.Project, repoConfig?: RepoOptions);
@@ -92,6 +99,13 @@ declare class Project {
92
99
  getIdentifier(): string;
93
100
  compare(proj: Project): void;
94
101
  getUUID(workflow: string | Workflow, stepId: string, otherStep?: string): any;
102
+ /**
103
+ * Returns a map of ids:uuids for everything in the project
104
+ */
105
+ getUUIDMap(options?: {
106
+ workflows: boolean;
107
+ project: false;
108
+ }): {};
95
109
  }
96
110
 
97
111
  declare class Workspace {
@@ -104,7 +118,7 @@ declare class Workspace {
104
118
  get(id: string): Project | undefined;
105
119
  getProjectPath(id: string): string | undefined;
106
120
  getActiveProject(): Project | undefined;
107
- getConfig(): OpenfnConfig | undefined;
121
+ getConfig(): Partial<OpenfnConfig>;
108
122
  get activeProjectId(): string | undefined;
109
123
  get valid(): boolean;
110
124
  }
@@ -115,14 +129,18 @@ declare function jsonToYaml(json: string | JSONObject): string;
115
129
  type GenerateWorkflowOptions = {
116
130
  name: string;
117
131
  uuidSeed: number;
118
- openfnUuid: boolean;
119
132
  printErrors: boolean;
133
+ uuidMap?: Record<string, string>;
134
+ openfnUuid: boolean;
135
+ };
136
+ type GenerateProjectOptions = GenerateWorkflowOptions & {
137
+ uuidMap: Array<Record<string, string>>;
120
138
  };
121
139
  /**
122
140
  * Generate a Workflow from a simple text based representation
123
141
  * eg, `a-b b-c a-c`
124
142
  */
125
143
  declare function generateWorkflow(def: string, options?: Partial<GenerateWorkflowOptions>): Workflow;
126
- declare function generateProject(name: string, workflowDefs: string[], options: Partial<GenerateWorkflowOptions>): Project;
144
+ declare function generateProject(name: string, workflowDefs: string[], options?: Partial<GenerateProjectOptions>): Project;
127
145
 
128
146
  export { Workspace, Project as default, generateProject, generateWorkflow, jsonToYaml, yamlToJson };
package/dist/index.js CHANGED
@@ -31,8 +31,15 @@ var Workflow = class {
31
31
  };
32
32
  this.workflow = clone(workflow);
33
33
  const { id, name, openfn, steps, ...options } = workflow;
34
+ if (!(id || name)) {
35
+ throw new Error("A Workflow MUST have a name or id");
36
+ }
34
37
  this.id = id ?? slugify(name);
35
- this.name = name ?? id;
38
+ this.name = name;
39
+ this.workflow.id = this.id;
40
+ if (name) {
41
+ this.workflow.name = this.name;
42
+ }
36
43
  this.openfn = openfn;
37
44
  this.options = options;
38
45
  this.#buildIndex();
@@ -128,6 +135,9 @@ var Workflow = class {
128
135
  toJSON() {
129
136
  return this.workflow;
130
137
  }
138
+ getUUIDMap() {
139
+ return this.index.uuid;
140
+ }
131
141
  };
132
142
  var Workflow_default = Workflow;
133
143
 
@@ -157,7 +167,7 @@ function to_json_default(project) {
157
167
  }
158
168
 
159
169
  // src/util/rename-keys.ts
160
- function renameKeys(props, keyMap) {
170
+ function renameKeys(props = {}, keyMap) {
161
171
  return Object.fromEntries(
162
172
  Object.entries(props).map(([key, value]) => [
163
173
  keyMap[key] ? keyMap[key] : key,
@@ -183,7 +193,7 @@ function jsonToYaml(json) {
183
193
  // src/serialize/to-app-state.ts
184
194
  import { randomUUID } from "node:crypto";
185
195
  function to_app_state_default(project, options = {}) {
186
- const { uuid: id, endpoint, env, ...rest } = project.openfn;
196
+ const { uuid: id, endpoint, env, ...rest } = project.openfn ?? {};
187
197
  const state = {
188
198
  id,
189
199
  name: project.name,
@@ -204,9 +214,11 @@ var mapWorkflow = (workflow) => {
204
214
  if (workflow instanceof Workflow_default) {
205
215
  workflow = workflow.toJSON();
206
216
  }
217
+ const { uuid, ...originalOpenfnProps } = workflow.openfn ?? {};
207
218
  const wfState = {
219
+ ...originalOpenfnProps,
220
+ id: workflow.openfn?.uuid ?? randomUUID(),
208
221
  name: workflow.name,
209
- ...renameKeys(workflow.openfn, { uuid: "id" }),
210
222
  jobs: [],
211
223
  triggers: [],
212
224
  edges: []
@@ -349,10 +361,12 @@ function slugify2(text) {
349
361
  return text.replace(/\W/g, " ").trim().replace(/\s+/g, "-").toLowerCase();
350
362
  }
351
363
  var from_app_state_default = (state, config) => {
352
- if (config.format === "yaml") {
353
- state = yamlToJson(state);
354
- } else if (typeof state === "string") {
355
- state = JSON.parse(state);
364
+ if (typeof state === "string") {
365
+ if (config?.format === "yaml") {
366
+ state = yamlToJson(state);
367
+ } else {
368
+ state = JSON.parse(state);
369
+ }
356
370
  }
357
371
  const {
358
372
  id,
@@ -367,13 +381,11 @@ var from_app_state_default = (state, config) => {
367
381
  } = state;
368
382
  const proj = {
369
383
  name,
370
- // TODO do we need to slug this or anything?
371
384
  description,
372
385
  collections,
373
386
  credentials,
374
387
  options
375
388
  };
376
- const repoConfig = {};
377
389
  proj.openfn = {
378
390
  uuid: id,
379
391
  endpoint: config.endpoint,
@@ -385,7 +397,7 @@ var from_app_state_default = (state, config) => {
385
397
  fetched_at: config.fetchedAt
386
398
  };
387
399
  proj.workflows = state.workflows.map(mapWorkflow2);
388
- return new Project(proj, repoConfig);
400
+ return new Project(proj, config?.repo);
389
401
  };
390
402
  var mapTriggerEdgeCondition = (edge) => {
391
403
  const e = {
@@ -406,11 +418,13 @@ var mapTriggerEdgeCondition = (edge) => {
406
418
  var mapWorkflow2 = (workflow) => {
407
419
  const { jobs, edges, triggers, name, ...remoteProps } = workflow;
408
420
  const mapped = {
409
- id: slugify2(workflow.name),
410
421
  name: workflow.name,
411
422
  steps: [],
412
423
  openfn: renameKeys(remoteProps, { id: "uuid" })
413
424
  };
425
+ if (workflow.name) {
426
+ mapped.id = slugify2(workflow.name);
427
+ }
414
428
  workflow.triggers.forEach((trigger) => {
415
429
  const { type, ...otherProps } = trigger;
416
430
  const connectedEdges = edges.filter(
@@ -454,6 +468,30 @@ var mapWorkflow2 = (workflow) => {
454
468
  return mapped;
455
469
  };
456
470
 
471
+ // src/parse/from-path.ts
472
+ import { extname } from "node:path";
473
+ import { readFile } from "node:fs/promises";
474
+ var from_path_default = async (path4, options = {}) => {
475
+ const ext = extname(path4).toLowerCase();
476
+ const source = await readFile(path4, "utf8");
477
+ const config = {
478
+ format: null,
479
+ repo: options.repo ?? options.config
480
+ // TMP
481
+ };
482
+ let state;
483
+ if (ext === ".json") {
484
+ config.format = "json";
485
+ state = JSON.parse(source);
486
+ } else if (ext.match(/(ya?ml)$/)) {
487
+ config.format = "yaml";
488
+ state = yamlToJson(source);
489
+ } else {
490
+ throw new Error(`Cannot load a project from a ${ext} file`);
491
+ }
492
+ return from_app_state_default(state, config);
493
+ };
494
+
457
495
  // src/parse/from-fs.ts
458
496
  import fs from "node:fs/promises";
459
497
  import path from "node:path";
@@ -501,8 +539,12 @@ var parseProject = async (options = {}) => {
501
539
  env: config.project?.env
502
540
  });
503
541
  try {
504
- const format = config.formats.project ?? "yaml";
505
- const statePath = path.join(root, ".projects", `${identifier}.${format}`);
542
+ const format = config.formats?.project ?? config.formats?.projects ?? "yaml";
543
+ const statePath = path.join(
544
+ root,
545
+ config.dirs?.projects ?? ".projects",
546
+ `${identifier}.${format}`
547
+ );
506
548
  const stateFile = await fs.readFile(statePath, "utf8");
507
549
  state = from_app_state_default(stateFile, { format });
508
550
  } catch (e) {
@@ -510,7 +552,8 @@ var parseProject = async (options = {}) => {
510
552
  }
511
553
  const { project: openfn, ...repo } = config;
512
554
  proj.openfn = openfn;
513
- const workflowDir = config.workflowRoot ?? "workflows";
555
+ proj.config = repo;
556
+ const workflowDir = config.workflowRoot ?? config.dirs?.workflows ?? "workflows";
514
557
  const fileType = config.formats?.workflow ?? "yaml";
515
558
  const pattern = `${root}/${workflowDir}/*/*.${fileType}`;
516
559
  const candidateWfs = await glob(pattern, {
@@ -527,7 +570,6 @@ var parseProject = async (options = {}) => {
527
570
  uuid: wfState.openfn?.uuid ?? null
528
571
  // TODO do we need to transfer more stuff?
529
572
  };
530
- console.log("Loading workflow at ", filePath);
531
573
  for (const step of wf.steps) {
532
574
  if (step.expression && step.expression.endsWith(".js")) {
533
575
  const dir = path.dirname(filePath);
@@ -565,7 +607,9 @@ var parseProject = async (options = {}) => {
565
607
  var getUuidForStep = (project, workflow, stepId) => {
566
608
  const wf = typeof workflow === "string" ? project.getWorkflow(workflow) : workflow;
567
609
  if (!wf) {
568
- throw new Error(`Workflow "${workflow} not found in project ${project.id}`);
610
+ throw new Error(
611
+ `Workflow "${workflow}" not found in project ${project.id}`
612
+ );
569
613
  }
570
614
  for (const step of wf.steps) {
571
615
  if (step.id === stepId) {
@@ -950,6 +994,7 @@ function merge(source, target, options) {
950
994
  // src/Project.ts
951
995
  var maybeCreateWorkflow = (wf) => wf instanceof Workflow_default ? wf : new Workflow_default(wf);
952
996
  var setConfigDefaults = (config = {}) => ({
997
+ ...config,
953
998
  workflowRoot: config.workflowRoot ?? "workflows",
954
999
  formats: {
955
1000
  // TODO change these maybe
@@ -976,7 +1021,7 @@ var Project = class {
976
1021
  meta;
977
1022
  // this contains meta about the connected openfn project
978
1023
  openfn;
979
- // repo configuration options
1024
+ // workspace-wide configuration options
980
1025
  // these should be shared across projects
981
1026
  // and saved to an openfn.yaml file
982
1027
  repo;
@@ -991,6 +1036,8 @@ var Project = class {
991
1036
  return from_app_state_default(data, options);
992
1037
  } else if (type === "fs") {
993
1038
  return parseProject(data, options);
1039
+ } else if (type === "path") {
1040
+ return from_path_default(data, options);
994
1041
  }
995
1042
  throw new Error(`Didn't recognize type ${type}`);
996
1043
  }
@@ -1052,6 +1099,19 @@ var Project = class {
1052
1099
  }
1053
1100
  return getUuidForStep(this, workflow, stepId);
1054
1101
  }
1102
+ /**
1103
+ * Returns a map of ids:uuids for everything in the project
1104
+ */
1105
+ getUUIDMap(options = {}) {
1106
+ const result = {};
1107
+ for (const wf of this.workflows) {
1108
+ result[wf.id] = {
1109
+ self: wf.openfn?.uuid,
1110
+ children: wf.getUUIDMap()
1111
+ };
1112
+ }
1113
+ return result;
1114
+ }
1055
1115
  };
1056
1116
  var Project_default = Project;
1057
1117
 
@@ -1082,16 +1142,19 @@ var Workspace = class {
1082
1142
  projectPaths = /* @__PURE__ */ new Map();
1083
1143
  isValid = false;
1084
1144
  constructor(workspacePath) {
1085
- const projectsPath = path2.join(workspacePath, PROJECTS_DIRECTORY);
1086
1145
  const openfnYamlPath = path2.join(workspacePath, OPENFN_YAML_FILE);
1087
1146
  if (pathExists(openfnYamlPath, "file")) {
1088
1147
  this.isValid = true;
1089
1148
  const data = fs3.readFileSync(openfnYamlPath, "utf-8");
1090
1149
  this.config = yamlToJson(data);
1091
1150
  }
1151
+ const projectsPath = path2.join(
1152
+ workspacePath,
1153
+ this.config?.dirs?.projects ?? PROJECTS_DIRECTORY
1154
+ );
1092
1155
  if (this.isValid && pathExists(projectsPath, "directory")) {
1093
1156
  const stateFiles = fs3.readdirSync(projectsPath).filter(
1094
- (fileName) => PROJECT_EXTENSIONS.includes(path2.extname(fileName))
1157
+ (fileName) => PROJECT_EXTENSIONS.includes(path2.extname(fileName)) && path2.parse(fileName).name !== "openfn"
1095
1158
  );
1096
1159
  this.projects = stateFiles.map((file) => {
1097
1160
  const stateFilePath = path2.join(projectsPath, file);
@@ -1099,7 +1162,7 @@ var Workspace = class {
1099
1162
  const project = from_app_state_default(data, { format: "yaml" });
1100
1163
  this.projectPaths.set(project.name, stateFilePath);
1101
1164
  return project;
1102
- });
1165
+ }).filter((s) => s);
1103
1166
  }
1104
1167
  }
1105
1168
  list() {
@@ -1133,16 +1196,21 @@ import { grammar } from "ohm-js";
1133
1196
  var parser;
1134
1197
  var initOperations = (options = {}) => {
1135
1198
  let nodes = {};
1136
- const uuid = () => {
1199
+ const uuidMap = options.uuidMap ?? {};
1200
+ const uuid = (id) => {
1201
+ if (id in uuidMap) {
1202
+ return uuidMap[id];
1203
+ }
1137
1204
  return options.uuidSeed ? options.uuidSeed++ : randomUUID2();
1138
1205
  };
1139
1206
  const buildNode = (name) => {
1140
1207
  if (!nodes[name]) {
1208
+ const id = slugify(name);
1141
1209
  nodes[name] = {
1142
1210
  name,
1143
- id: slugify(name),
1211
+ id,
1144
1212
  openfn: {
1145
- uuid: uuid()
1213
+ uuid: uuid(id)
1146
1214
  }
1147
1215
  };
1148
1216
  }
@@ -1169,6 +1237,7 @@ var initOperations = (options = {}) => {
1169
1237
  const n1 = parent.buildWorkflow();
1170
1238
  const n2 = child.buildWorkflow();
1171
1239
  const e = edge.buildWorkflow();
1240
+ e.openfn.uuid = uuid(`${n1.id}-${n2.id}`);
1172
1241
  n1.next ??= {};
1173
1242
  n1.next[n2.name] = e;
1174
1243
  return [n1, n2];
@@ -1197,9 +1266,6 @@ var initOperations = (options = {}) => {
1197
1266
  return props.asIteration().children.map((c) => c.buildWorkflow());
1198
1267
  },
1199
1268
  prop(key, _op, value) {
1200
- if (value._iter) {
1201
- console.log(">>>> ITER");
1202
- }
1203
1269
  return [key.sourceString, value.buildWorkflow()];
1204
1270
  },
1205
1271
  // Bit flaky - we need this to handle quoted props
@@ -1214,16 +1280,15 @@ var initOperations = (options = {}) => {
1214
1280
  },
1215
1281
  edge(_) {
1216
1282
  return {
1217
- openfn: {
1218
- uuid: uuid()
1219
- }
1283
+ openfn: {}
1220
1284
  };
1221
1285
  }
1222
1286
  };
1223
1287
  return operations;
1224
1288
  };
1225
1289
  var createParser = () => {
1226
- const contents = readFileSync(path3.resolve("src/gen/workflow.ohm"), "utf-8");
1290
+ const grammarPath = path3.resolve(import.meta.dirname, "workflow.ohm");
1291
+ const contents = readFileSync(grammarPath, "utf-8");
1227
1292
  const parser2 = grammar(contents);
1228
1293
  return {
1229
1294
  parse(str, options) {
@@ -1247,19 +1312,31 @@ function generateWorkflow(def, options = {}) {
1247
1312
  if (!parser) {
1248
1313
  parser = createParser();
1249
1314
  }
1250
- const wf = new Workflow_default(parser.parse(def, options));
1315
+ const raw = parser.parse(def, options);
1316
+ if (!raw.name) {
1317
+ raw.name = "Workflow";
1318
+ }
1319
+ if (!raw.id) {
1320
+ raw.id = "workflow";
1321
+ }
1251
1322
  if (options.openfnUuid) {
1252
- wf.openfn = {
1253
- uuid: randomUUID2()
1254
- };
1323
+ raw.openfn ??= {};
1324
+ raw.openfn.uuid = randomUUID2();
1255
1325
  }
1326
+ const wf = new Workflow_default(raw);
1256
1327
  return wf;
1257
1328
  }
1258
- function generateProject(name, workflowDefs, options) {
1259
- const workflows = workflowDefs.map((w) => generateWorkflow(w, options));
1329
+ function generateProject(name, workflowDefs, options = {}) {
1330
+ const workflows = workflowDefs.map(
1331
+ (w, idx) => generateWorkflow(w, {
1332
+ ...options,
1333
+ uuidMap: options.uuidMap && options.uuidMap[idx]
1334
+ })
1335
+ );
1260
1336
  return new Project_default({
1261
1337
  name,
1262
- workflows
1338
+ workflows,
1339
+ openfn: options.openfnUuid && { uuid: randomUUID2() }
1263
1340
  });
1264
1341
  }
1265
1342
 
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Ohm grammar for interpreting our Workflow syntax
3
+ * https://ohmjs.org/docs/intro
4
+ */
5
+ Workflow {
6
+ Workflow = attribute* Pair*
7
+
8
+ attribute = "@" attr_name space attr_name
9
+
10
+ attr_name = (alnum | "_" | "-")+
11
+
12
+ Pair = node edge node
13
+
14
+ comment = "#" (~lineTerminator any)*
15
+
16
+ space := " " | "\t" | lineTerminator | comment
17
+
18
+ /* lower case is important: it disables whitespace */
19
+ node = nodeWithProps | node_name
20
+
21
+ nodeWithProps = node_name props
22
+
23
+ /* A node name can contain letters, numbers or underscores */
24
+ node_name = (alnum | "_")+
25
+
26
+ props = "(" listOf<prop, ","> ")"
27
+
28
+ prop = alnum+ "=" propValue
29
+
30
+ propValue = alnum+ | quotedProp
31
+
32
+ quotedProp = "\"" (~"\"" any)* "\""
33
+
34
+ edge = "-"
35
+
36
+ lineTerminator = "\n" | "\r"
37
+ }
38
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfn/project",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Read, serialize, replicate and sync OpenFn projects",
5
5
  "type": "module",
6
6
  "exports": {
@@ -32,7 +32,7 @@
32
32
  "lodash-es": "^4.17.21",
33
33
  "ohm-js": "^17.2.1",
34
34
  "yaml": "^2.2.2",
35
- "@openfn/lexicon": "^1.2.3",
35
+ "@openfn/lexicon": "^1.2.4",
36
36
  "@openfn/logger": "1.0.6"
37
37
  },
38
38
  "files": [
@@ -43,8 +43,7 @@
43
43
  "test": "pnpm ava",
44
44
  "test:watch": "pnpm ava -w",
45
45
  "test:types": "pnpm tsc --noEmit --project tsconfig.json",
46
- "build": "tsup --config ../../tsup.config.js src/index.ts",
47
- "build:watch": "pnpm build --watch",
46
+ "build": "tsup --config ../../tsup.config.js src/index.ts && cp src/gen/workflow.ohm dist/workflow.ohm",
48
47
  "pack": "pnpm pack --pack-destination ../../dist"
49
48
  }
50
49
  }