Skip to content

Commit edfc759

Browse files
authored
Fixing pull --beta and refactoring along the way (#1084)
* trying to enable old json formats * refactor project.repo to project.config * changeset * test and refactor in workspace * make project meta public * new helper to load workspace file * standardise workflow.yaml loading * update new format for openfn.yaml * fix types * suppress error logging * types * cli: fix config * fix tests * track project name in legacy config files * track project name from app state * update test * fix more tests * fix empty workspace files
1 parent f4209dd commit edfc759

File tree

23 files changed

+563
-314
lines changed

23 files changed

+563
-314
lines changed

.changeset/fine-mammals-juggle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openfn/cli': patch
3+
---
4+
5+
Update Project dependency

.changeset/fuzzy-colts-invite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openfn/project': patch
3+
---
4+
5+
Refactor project.repo to project.config

packages/cli/src/checkout/handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const checkoutHandler = async (options: CheckoutOptions, logger: Logger) => {
1515

1616
// get the config
1717
// TODO: try to retain the endpoint for the projects
18-
const { project: _, ...config } = workspace.getConfig() ?? {};
18+
const { project: _, ...config } = workspace.getConfig() as any;
1919

2020
// get the project
2121
let switchProject;

packages/cli/src/pull/beta.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { confirm } from '@inquirer/prompts';
33
import path from 'path';
44
import fs from 'node:fs/promises';
55
import { DeployConfig, getProject } from '@openfn/deploy';
6-
import Project from '@openfn/project';
6+
import Project, { Workspace } from '@openfn/project';
77
import type { Logger } from '../util/logger';
88
import { rimraf } from 'rimraf';
99
import { Opts } from '../options';
@@ -37,47 +37,52 @@ export type PullOptionsBeta = Required<
3737
export async function handler(options: PullOptionsBeta, logger: Logger) {
3838
const { OPENFN_API_KEY, OPENFN_ENDPOINT } = process.env;
3939

40-
const config: Partial<Config> = {
40+
const cfg: Partial<Config> = {
4141
apiKey: options.apiKey,
4242
endpoint: options.endpoint,
4343
};
4444

4545
if (!options.apiKey && OPENFN_API_KEY) {
4646
logger.info('Using OPENFN_API_KEY environment variable');
47-
config.apiKey = OPENFN_API_KEY;
47+
cfg.apiKey = OPENFN_API_KEY;
4848
}
4949

5050
if (!options.endpoint && OPENFN_ENDPOINT) {
5151
logger.info('Using OPENFN_ENDPOINT environment variable');
52-
config.endpoint = OPENFN_ENDPOINT;
52+
cfg.endpoint = OPENFN_ENDPOINT;
5353
}
5454

55+
// TODO `path` or `output` ?
56+
// I don't think I want to model this as output. deploy is really
57+
// designed to run from the working folder
58+
// could be projectPath or repoPath too
59+
const outputRoot = path.resolve(options.path || '.');
60+
61+
// TODO is outputRoot the right dir for this?
62+
const workspace = new Workspace(outputRoot);
63+
const config = workspace.getConfig();
64+
5565
// download the state.json from lightning
56-
const { data } = await getProject(config as DeployConfig, options.projectId);
66+
const { data } = await getProject(cfg as DeployConfig, options.projectId);
5767

5868
// TODO if the user doesn't specify an env name, prompt for one
5969
const name = options.env || 'project';
6070

6171
const project = Project.from('state', data, {
62-
endpoint: config.endpoint,
72+
config,
73+
endpoint: cfg.endpoint,
6374
env: name,
6475
fetched_at: new Date().toISOString(),
6576
});
6677

67-
// TODO `path` or `output` ?
68-
// I don't think I want to model this as output. deploy is really
69-
// designed to run from the working folder
70-
// could be projectPath or repoPath too
71-
const outputRoot = path.resolve(options.path || '.');
72-
7378
const projectFileName = project.getIdentifier();
7479

7580
await fs.mkdir(`${outputRoot}/.projects`, { recursive: true });
7681
let stateOutputPath = `${outputRoot}/.projects/${projectFileName}`;
7782

7883
const workflowsRoot = path.resolve(
7984
outputRoot,
80-
project.repo?.workflowRoot ?? 'workflows'
85+
project.config.dirs.workflows ?? 'workflows'
8186
);
8287
// Prompt before deleting
8388
// TODO this is actually the wrong path
@@ -96,7 +101,8 @@ export async function handler(options: PullOptionsBeta, logger: Logger) {
96101
await rimraf(workflowsRoot);
97102

98103
const state = project?.serialize('state');
99-
if (project.repo?.formats.project === 'yaml') {
104+
105+
if (project.config.formats.project === 'yaml') {
100106
await fs.writeFile(`${stateOutputPath}.yaml`, state);
101107
} else {
102108
await fs.writeFile(

packages/cli/test/checkout/handler.test.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ test.serial('checkout: invalid project id', (t) => {
164164
test.serial('checkout: to a different valid project', async (t) => {
165165
// before checkout. some-project-name is active and expanded
166166
const bcheckout = new Workspace('/ws');
167-
t.is(bcheckout.getConfig()?.name, 'some-project-name');
167+
t.is(bcheckout.projectMeta.name, 'some-project-name');
168168
t.is(bcheckout.getActiveProject()?.name, 'some-project-name');
169169

170170
await checkoutHandler(
@@ -176,7 +176,7 @@ test.serial('checkout: to a different valid project', async (t) => {
176176

177177
// after checkout. main-project-id is active and expanded
178178
const acheckout = new Workspace('/ws');
179-
t.is(acheckout.getConfig()?.name, 'main-project-id');
179+
t.is(acheckout.projectMeta.name, 'main-project-id');
180180
t.is(acheckout.getActiveProject()?.name, 'main-project-id');
181181

182182
// check if files where well expanded
@@ -189,19 +189,23 @@ test.serial('checkout: to a different valid project', async (t) => {
189189
test.serial('checkout: same id as active', async (t) => {
190190
// before checkout. some-project-name is active and expanded
191191
const bcheckout = new Workspace('/ws');
192-
t.is(bcheckout.getConfig()?.name, 'some-project-name');
192+
t.is(bcheckout.projectMeta.name, 'some-project-name');
193193
t.is(bcheckout.getActiveProject()?.name, 'some-project-name');
194194

195195
await checkoutHandler(
196-
{ command: 'checkout', projectName: 'some-project-name', projectPath: '/ws' },
196+
{
197+
command: 'checkout',
198+
projectName: 'some-project-name',
199+
projectPath: '/ws',
200+
},
197201
logger
198202
);
199203
const { message } = logger._parse(logger._last);
200204
t.is(message, 'Expanded project to /ws');
201205

202206
// after checkout. main-project-id is active and expanded
203207
const acheckout = new Workspace('/ws');
204-
t.is(acheckout.getConfig()?.name, 'some-project-name');
208+
t.is(acheckout.projectMeta.name, 'some-project-name');
205209
t.is(acheckout.getActiveProject()?.name, 'some-project-name');
206210

207211
// check if files where well expanded
@@ -214,7 +218,7 @@ test.serial('checkout: same id as active', async (t) => {
214218
test.serial('checkout: switching to and back between projects', async (t) => {
215219
// before checkout. some-project-name is active and expanded
216220
const bcheckout = new Workspace('/ws');
217-
t.is(bcheckout.getConfig()?.name, 'some-project-name');
221+
t.is(bcheckout.projectMeta.name, 'some-project-name');
218222
t.is(bcheckout.getActiveProject()?.name, 'some-project-name');
219223

220224
// 1. switch from some-project-name to main-project-id
@@ -227,7 +231,7 @@ test.serial('checkout: switching to and back between projects', async (t) => {
227231

228232
// after checkout. main-project-id is active and expanded
229233
const acheckout = new Workspace('/ws');
230-
t.is(acheckout.getConfig()?.name, 'main-project-id');
234+
t.is(acheckout.projectMeta.name, 'main-project-id');
231235
t.is(acheckout.getActiveProject()?.name, 'main-project-id');
232236

233237
// check if files where well expanded
@@ -238,15 +242,19 @@ test.serial('checkout: switching to and back between projects', async (t) => {
238242

239243
// 2. switch back from main-project-id to some-project-name
240244
await checkoutHandler(
241-
{ command: 'checkout', projectName: 'some-project-name', projectPath: '/ws' },
245+
{
246+
command: 'checkout',
247+
projectName: 'some-project-name',
248+
projectPath: '/ws',
249+
},
242250
logger
243251
);
244252
const { message: lastMsg } = logger._parse(logger._last);
245253
t.is(lastMsg, 'Expanded project to /ws');
246254

247255
// after checkout. main-project-id is active and expanded
248256
const fcheckout = new Workspace('/ws');
249-
t.is(fcheckout.getConfig()?.name, 'some-project-name');
257+
t.is(fcheckout.projectMeta.name, 'some-project-name');
250258
t.is(fcheckout.getActiveProject()?.name, 'some-project-name');
251259

252260
// check if files where well expanded

packages/cli/test/merge/handler.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ test('merging into the same project', async (t) => {
113113
test('merging a different project into checked-out', async (t) => {
114114
// state of main projects workflow before sandbox is merged in
115115
const bworkspace = new Workspace('/ws');
116-
t.is(bworkspace.getConfig()?.name, 'main-project-id');
116+
t.is(bworkspace.projectMeta.name, 'main-project-id');
117117
t.is(bworkspace.getActiveProject()?.name, 'main-project-id');
118118
const bprojects = bworkspace.list();
119119
t.is(bprojects[0].workflows[0].steps.length, 2);
@@ -133,13 +133,13 @@ test('merging a different project into checked-out', async (t) => {
133133

134134
// state of main projects workflow before sandbox is merged in
135135
const workspace = new Workspace('/ws');
136-
t.is(workspace.getConfig()?.name, 'main-project-id');
136+
t.is(workspace.projectMeta.name, 'main-project-id');
137137
t.is(workspace.getActiveProject()?.name, 'main-project-id');
138138
const projects = workspace.list();
139139
t.is(projects[0].workflows[0].steps.length, 3);
140140
t.is(projects[0].workflows[0].steps[1].name, 'Job X');
141141
t.is(projects[0].workflows[0].steps[1].openfn?.uuid, 'job-a'); // id got retained
142-
t.is(projects[0].workflows[0].steps[2].name, 'Job Y');
142+
t.is(projects[0].workflows[0].steps[2].name, 'Job Y');
143143
t.is(projects[0].workflows[0].steps[2].openfn?.uuid, 'job-y'); // id not retained - new nod
144144

145145
const { message, level } = logger._parse(logger._last);

packages/project/src/Project.ts

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,24 @@
1-
import * as l from '@openfn/lexicon';
21
import Workflow from './Workflow';
32
import * as serializers from './serialize';
4-
import fromAppState from './parse/from-app-state';
5-
import fromPath from './parse/from-path';
3+
import fromAppState, { FromAppStateConfig } from './parse/from-app-state';
4+
import fromPath, { FromPathConfig } from './parse/from-path';
65
// TODO this naming clearly isn't right
76
import { parseProject as fromFs, FromFsConfig } from './parse/from-fs';
87
import getIdentifier from './util/get-identifier';
98
import slugify from './util/slugify';
109
import { getUuidForEdge, getUuidForStep } from './util/uuid';
1110
import { merge, MergeProjectOptions } from './merge/merge-project';
11+
import { Workspace } from './Workspace';
12+
import { buildConfig, WorkspaceConfig } from './util/config';
1213

1314
type MergeOptions = {
1415
force?: boolean;
1516
workflows?: string[]; // which workflows to include
1617
};
1718

18-
type FileFormats = 'yaml' | 'json';
19-
2019
const maybeCreateWorkflow = (wf: any) =>
2120
wf instanceof Workflow ? wf : new Workflow(wf);
2221

23-
export interface OpenfnConfig {
24-
name: string;
25-
workflowRoot: string;
26-
dirs: {
27-
workflows: string;
28-
projects: string;
29-
};
30-
formats: {
31-
openfn: FileFormats;
32-
project: FileFormats;
33-
workflow: FileFormats;
34-
};
35-
project: {
36-
projectId: string;
37-
endpoint: string;
38-
env: string;
39-
inserted_at: string;
40-
updated_at: string;
41-
};
42-
}
43-
4422
// TODO --------------
4523
// I think this needs renaming to config
4624
// and it's part of the workspace technically
@@ -68,16 +46,7 @@ type RepoOptions = {
6846

6947
// TODO maybe use an npm for this, or create util
7048

71-
const setConfigDefaults = (config: OpenfnConfig = {}) => ({
72-
...config,
73-
workflowRoot: config.workflowRoot ?? 'workflows',
74-
formats: {
75-
// TODO change these maybe
76-
openfn: config.formats?.openfn ?? 'yaml',
77-
project: config.formats?.project ?? 'yaml',
78-
workflow: config.formats?.workflow ?? 'yaml',
79-
},
80-
});
49+
// TODO this need to be controlled by the workspace
8150

8251
// A single openfn project
8352
// could be an app project or a checked out fs
@@ -106,10 +75,9 @@ export class Project {
10675
// this contains meta about the connected openfn project
10776
openfn?: l.ProjectConfig;
10877

109-
// workspace-wide configuration options
110-
// these should be shared across projects
111-
// and saved to an openfn.yaml file
112-
repo?: Required<RepoOptions>;
78+
workspace?: Workspace;
79+
80+
config: WorkspaceConfig;
11381

11482
// load a project from a state file (project.json)
11583
// or from a path (the file system)
@@ -128,12 +96,12 @@ export class Project {
12896
static from(
12997
type: 'path',
13098
data: string,
131-
options?: { config?: Partial<OpenfnConfig> }
99+
options?: { config?: FromPathConfig }
132100
): Project;
133101
static from(
134102
type: 'state' | 'path' | 'fs',
135103
data: any,
136-
options: Partial<l.ProjectConfig> = {}
104+
options: FromAppStateConfig = {}
137105
): Project {
138106
if (type === 'state') {
139107
return fromAppState(data, options);
@@ -159,8 +127,11 @@ export class Project {
159127
// uh maybe
160128
// maybe this second arg is config - like env, branch rules, serialisation rules
161129
// stuff that's external to the actual project and managed by the repo
130+
131+
// TODO maybe the constructor is (data, Workspace)
162132
constructor(data: l.Project, repoConfig: RepoOptions = {}) {
163-
this.repo = setConfigDefaults(repoConfig);
133+
this.setConfig(repoConfig);
134+
164135
this.name = data.name;
165136
this.description = data.description;
166137
this.openfn = data.openfn;
@@ -171,6 +142,10 @@ export class Project {
171142
this.meta = data.meta;
172143
}
173144

145+
setConfig(config: Partial<WorkspaceConfig>) {
146+
this.config = buildConfig(config);
147+
}
148+
174149
serialize(type: 'json' | 'yaml' | 'fs' | 'state' = 'json', options?: any) {
175150
if (type in serializers) {
176151
// @ts-ignore

0 commit comments

Comments
 (0)