diff --git a/packages/cli/src/checkout/command.ts b/packages/cli/src/checkout/command.ts index 8fab9d994..1400e5850 100644 --- a/packages/cli/src/checkout/command.ts +++ b/packages/cli/src/checkout/command.ts @@ -4,14 +4,14 @@ import { ensure, build } from '../util/command-builders'; import * as o from '../options'; export type CheckoutOptions = Required< - Pick + Pick > & Pick; -const options = [o.projectName, o.projectPath, o.log]; +const options = [o.projectId, o.projectPath, o.log]; const checkoutCommand: yargs.CommandModule = { - command: 'checkout ', + command: 'checkout ', describe: 'Switch to a different openfn project in the same workspace', handler: ensure('checkout', options), builder: (yargs) => build(options, yargs), diff --git a/packages/cli/src/checkout/handler.ts b/packages/cli/src/checkout/handler.ts index 746801354..2f071e825 100644 --- a/packages/cli/src/checkout/handler.ts +++ b/packages/cli/src/checkout/handler.ts @@ -19,20 +19,20 @@ const checkoutHandler = async (options: CheckoutOptions, logger: Logger) => { // get the project let switchProject; - if (/\.(yaml|json)$/.test(options.projectName)) { + if (/\.(yaml|json)$/.test(options.projectId)) { // TODO: should we allow checkout into an arbitrary folder? - const filePath = path.join(commandPath, options.projectName); + const filePath = path.join(commandPath, options.projectId); logger.debug('Loading project from path ', filePath); switchProject = await Project.from('path', filePath, { config, }); } else { - switchProject = workspace.get(options.projectName); + switchProject = workspace.get(options.projectId); } if (!switchProject) { logger.error( - `Project with id/name ${options.projectName} not found in the workspace` + `Project with id/name ${options.projectId} not found in the workspace` ); return; } diff --git a/packages/cli/src/merge/command.ts b/packages/cli/src/merge/command.ts index a07447e1d..4e7872262 100644 --- a/packages/cli/src/merge/command.ts +++ b/packages/cli/src/merge/command.ts @@ -7,17 +7,16 @@ export type MergeOptions = Required< Pick< Opts, | 'command' - | 'projectName' + | 'projectId' | 'projectPath' | 'removeUnmapped' | 'workflowMappings' - | 'log' - | 'force' > ->; +> & + Pick; const options = [ - o.projectName, + o.projectId, o.projectPath, o.removeUnmapped, o.workflowMappings, @@ -28,7 +27,7 @@ const options = [ ]; const mergeCommand: yargs.CommandModule = { - command: 'merge [project-name]', + command: 'merge ', describe: 'Merges the specified project into the checked out project', handler: ensure('merge', options), builder: (yargs) => build(options, yargs), diff --git a/packages/cli/src/merge/handler.ts b/packages/cli/src/merge/handler.ts index 80c8b8d06..cc0591c59 100644 --- a/packages/cli/src/merge/handler.ts +++ b/packages/cli/src/merge/handler.ts @@ -23,29 +23,29 @@ const mergeHandler = async (options: MergeOptions, logger: Logger) => { // Lookup the source project - the thing we are getting changes from let sourceProject; - if (/\.(yaml|json)$/.test(options.projectName)) { - const filePath = path.join(commandPath, options.projectName); + if (/\.(yaml|json)$/.test(options.projectId)) { + const filePath = path.join(commandPath, options.projectId); logger.debug('Loading source project from path ', filePath); sourceProject = await Project.from('path', filePath); } else { - sourceProject = workspace.get(options.projectName); + sourceProject = workspace.get(options.projectId); } if (!sourceProject) { - logger.error(`Project "${options.projectName}" not found in the workspace`); + logger.error(`Project "${options.projectId}" not found in the workspace`); return; } - if (targetProject.name === sourceProject.name) { + if (targetProject.id === sourceProject.id) { logger.error('Merging into the same project not allowed'); return; } - if (!targetProject.name) { - logger.error('The checked out project has no name/id'); + if (!targetProject.id) { + logger.error('The checked out project has no id'); return; } - const finalPath = workspace.getProjectPath(targetProject.name); + const finalPath = workspace.getProjectPath(targetProject.id); if (!finalPath) { logger.error('Path to checked out project not found.'); return; @@ -64,13 +64,13 @@ const mergeHandler = async (options: MergeOptions, logger: Logger) => { { command: 'checkout', projectPath: commandPath, - projectName: final.name || '', + projectId: final.id, log: options.log, }, logger ); logger.success( - `Project ${sourceProject.name} has been merged into Project ${targetProject.name} successfully` + `Project ${sourceProject.id} has been merged into Project ${targetProject.id} successfully` ); }; diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index b467406c2..514006042 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -336,19 +336,7 @@ const getBaseDir = (opts: { path?: string }) => { export const projectId: CLIOption = { name: 'project-id', yargs: { - hidden: true, - }, - ensure: (opts) => { - const projectId = opts.projectId; - //check that this is a uuid - return projectId; - }, -}; - -export const projectName: CLIOption = { - name: 'project-name', - yargs: { - description: 'The name of an openfn project', + description: 'The id or UUID of an openfn project', string: true, }, ensure: (opts) => { diff --git a/packages/cli/src/projects/handler.ts b/packages/cli/src/projects/handler.ts index af76e4bc9..31b8fa048 100644 --- a/packages/cli/src/projects/handler.ts +++ b/packages/cli/src/projects/handler.ts @@ -13,19 +13,17 @@ const projectsHandler = async (options: ProjectsOptions, logger: Logger) => { logger.success(`Available openfn projects\n\n${workspace .list() - .map((p) => describeProject(p, p.name === workspace.activeProjectId)) + .map((p) => describeProject(p, p.id === workspace.activeProjectId)) .join('\n\n')} `); }; function describeProject(project: Project, active = false) { // @ts-ignore - const pId = project.openfn?.uuid; - return `${project.name} ${active ? '(active)' : ''}\n ${ - pId || '' - }\n workflows:\n${project.workflows - .map((w) => ' - ' + w.name) - .join('\n')}`; + const uuid = project.openfn?.uuid; + return `${project.id} ${active ? '(active)' : ''}\n ${ + uuid || '' + }\n workflows:\n${project.workflows.map((w) => ' - ' + w.id).join('\n')}`; } export default projectsHandler; diff --git a/packages/cli/src/version/command.ts b/packages/cli/src/version/command.ts index 1141f2a76..73686f18c 100644 --- a/packages/cli/src/version/command.ts +++ b/packages/cli/src/version/command.ts @@ -6,22 +6,11 @@ import * as o from '../options'; export type VersionOptions = Required< Pick< Opts, - | 'command' - | 'workflow' - | 'projectName' - | 'projectPath' - | 'workflowMappings' - | 'json' + 'command' | 'workflow' | 'projectPath' | 'workflowMappings' | 'json' > >; -const options = [ - o.workflow, - o.projectName, - o.projectPath, - o.workflowMappings, - o.json, -]; +const options = [o.workflow, o.projectPath, o.workflowMappings, o.json]; const workflowVersionCommand: yargs.CommandModule = { command: 'project version [workflow]', diff --git a/packages/cli/test/checkout/handler.test.ts b/packages/cli/test/checkout/handler.test.ts index ecfde24af..3bf9d03c3 100644 --- a/packages/cli/test/checkout/handler.test.ts +++ b/packages/cli/test/checkout/handler.test.ts @@ -9,17 +9,21 @@ test.beforeEach(() => { mock({ '/ws/workflows': {}, '/ws/openfn.yaml': jsonToYaml({ - name: 'some-project-name', - workflowRoot: 'workflows', - formats: { - openfn: 'yaml', - project: 'yaml', - workflow: 'yaml', + project: { + id: 'my-project', + }, + workspace: { + workflowRoot: 'workflows', + formats: { + openfn: 'yaml', + project: 'yaml', + workflow: 'yaml', + }, }, }), '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml({ - id: 'some-id', - name: 'some-project-name', + id: '>', + name: 'My Staging', workflows: [ { name: 'simple-workflow', @@ -80,8 +84,8 @@ test.beforeEach(() => { ], }), '/ws/.projects/project@app.openfn.org.yaml': jsonToYaml({ - id: 'main-id', - name: 'main-project-id', + id: '', + name: 'My Project', workflows: [ { name: 'simple-workflow-main', @@ -149,12 +153,12 @@ const logger = createMockLogger('', { level: 'debug' }); test.serial('get active project', (t) => { const workspace = new Workspace('/ws'); t.is(workspace.valid, true); - t.is(workspace.activeProjectId, 'some-project-name'); + t.is(workspace.activeProjectId, 'my-project'); }); test.serial('checkout: invalid project id', (t) => { checkoutHandler( - { command: 'checkout', projectName: 'not-known', projectPath: '/ws' }, + { command: 'checkout', projectId: 'not-known', projectPath: '/ws' }, logger ); const { message } = logger._parse(logger._last); @@ -162,22 +166,20 @@ test.serial('checkout: invalid project id', (t) => { }); test.serial('checkout: to a different valid project', async (t) => { - // before checkout. some-project-name is active and expanded + // before checkout. my-project is active and expanded const bcheckout = new Workspace('/ws'); - t.is(bcheckout.projectMeta.name, 'some-project-name'); - t.is(bcheckout.getActiveProject()?.name, 'some-project-name'); + t.is(bcheckout.activeProject.id, 'my-project'); await checkoutHandler( - { command: 'checkout', projectName: 'main-project-id', projectPath: '/ws' }, + { command: 'checkout', projectId: 'my-project', projectPath: '/ws' }, logger ); const { message } = logger._parse(logger._last); t.is(message, 'Expanded project to /ws'); - // after checkout. main-project-id is active and expanded + // after checkout. my-project is active and expanded const acheckout = new Workspace('/ws'); - t.is(acheckout.projectMeta.name, 'main-project-id'); - t.is(acheckout.getActiveProject()?.name, 'main-project-id'); + t.is(acheckout.activeProject.id, 'my-project'); // check if files where well expanded t.deepEqual( @@ -187,15 +189,14 @@ test.serial('checkout: to a different valid project', async (t) => { }); test.serial('checkout: same id as active', async (t) => { - // before checkout. some-project-name is active and expanded + // before checkout. my-project is active and expanded const bcheckout = new Workspace('/ws'); - t.is(bcheckout.projectMeta.name, 'some-project-name'); - t.is(bcheckout.getActiveProject()?.name, 'some-project-name'); + t.is(bcheckout.activeProject.id, 'my-project'); await checkoutHandler( { command: 'checkout', - projectName: 'some-project-name', + projectId: 'my-project', projectPath: '/ws', }, logger @@ -203,48 +204,45 @@ test.serial('checkout: same id as active', async (t) => { const { message } = logger._parse(logger._last); t.is(message, 'Expanded project to /ws'); - // after checkout. main-project-id is active and expanded + // after checkout. my-project is active and expanded const acheckout = new Workspace('/ws'); - t.is(acheckout.projectMeta.name, 'some-project-name'); - t.is(acheckout.getActiveProject()?.name, 'some-project-name'); + t.is(acheckout.activeProject.id, 'my-project'); // check if files where well expanded t.deepEqual( fs.readdirSync('/ws/workflows').sort(), - ['simple-workflow', 'another-workflow'].sort() + ['simple-workflow-main', 'another-workflow-main'].sort() ); }); test.serial('checkout: switching to and back between projects', async (t) => { - // before checkout. some-project-name is active and expanded + // before checkout. my-project is active and expanded const bcheckout = new Workspace('/ws'); - t.is(bcheckout.projectMeta.name, 'some-project-name'); - t.is(bcheckout.getActiveProject()?.name, 'some-project-name'); + t.is(bcheckout.activeProject.id, 'my-project'); - // 1. switch from some-project-name to main-project-id + // 1. switch from my-project to my-staging await checkoutHandler( - { command: 'checkout', projectName: 'main-project-id', projectPath: '/ws' }, + { command: 'checkout', projectId: 'my-staging', projectPath: '/ws' }, logger ); const { message } = logger._parse(logger._last); t.is(message, 'Expanded project to /ws'); - // after checkout. main-project-id is active and expanded + // after checkout. my-staging is active and expanded const acheckout = new Workspace('/ws'); - t.is(acheckout.projectMeta.name, 'main-project-id'); - t.is(acheckout.getActiveProject()?.name, 'main-project-id'); + t.is(acheckout.activeProject.id, 'my-staging'); // check if files where well expanded t.deepEqual( fs.readdirSync('/ws/workflows').sort(), - ['simple-workflow-main', 'another-workflow-main'].sort() + ['simple-workflow', 'another-workflow'].sort() ); - // 2. switch back from main-project-id to some-project-name + // 2. switch back from my-project to my-project await checkoutHandler( { command: 'checkout', - projectName: 'some-project-name', + projectId: 'my-project', projectPath: '/ws', }, logger @@ -252,14 +250,13 @@ test.serial('checkout: switching to and back between projects', async (t) => { const { message: lastMsg } = logger._parse(logger._last); t.is(lastMsg, 'Expanded project to /ws'); - // after checkout. main-project-id is active and expanded + // after checkout. my-project is active and expanded const fcheckout = new Workspace('/ws'); - t.is(fcheckout.projectMeta.name, 'some-project-name'); - t.is(fcheckout.getActiveProject()?.name, 'some-project-name'); + t.is(fcheckout.activeProject.id, 'my-project'); // check if files where well expanded t.deepEqual( fs.readdirSync('/ws/workflows').sort(), - ['simple-workflow', 'another-workflow'].sort() + ['simple-workflow-main', 'another-workflow-main'].sort() ); }); diff --git a/packages/cli/test/merge/handler.test.ts b/packages/cli/test/merge/handler.test.ts index fe9a183b6..9f5e85e2b 100644 --- a/packages/cli/test/merge/handler.test.ts +++ b/packages/cli/test/merge/handler.test.ts @@ -8,21 +8,28 @@ test.beforeEach(() => { mock({ '/ws/workflows': {}, '/ws/openfn.yaml': jsonToYaml({ - name: 'main-project-id', - workflowRoot: 'workflows', - formats: { - openfn: 'yaml', - project: 'yaml', - workflow: 'yaml', + project: { + id: 'my-project', + name: 'My Project', + }, + workspace: { + dirs: { + workflows: 'workflows', + }, + formats: { + openfn: 'yaml', + project: 'yaml', + workflow: 'yaml', + }, }, }), '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml({ - id: 'sandbox-uuid', - name: 'sandbox-project-id', + id: '', + name: 'My Sandbox', workflows: [ { - name: 'first-workflow', - id: 'first-workflow-id', + name: 'Workflow 1', + id: 'workflow-1', jobs: [ { id: 'job-x', @@ -62,12 +69,12 @@ test.beforeEach(() => { ], }), '/ws/.projects/project@app.openfn.org.yaml': jsonToYaml({ - id: 'main-project-uuid', - name: 'main-project-id', + id: '', + name: 'My Project', workflows: [ { - name: 'first-workflow', - id: 'first-workflow-id', + name: 'Workflow 1', + id: 'workflow-1', jobs: [{ id: 'job-a', name: 'Job A' }], triggers: [ { @@ -98,7 +105,7 @@ test('merging into the same project', async (t) => { { command: 'merge', projectPath: '/ws', - projectName: 'main-project-id', + projectId: 'my-project', removeUnmapped: false, workflowMappings: {}, }, @@ -112,40 +119,39 @@ test('merging into the same project', async (t) => { test('merging a different project into checked-out', async (t) => { // state of main projects workflow before sandbox is merged in - const bworkspace = new Workspace('/ws'); - t.is(bworkspace.projectMeta.name, 'main-project-id'); - t.is(bworkspace.getActiveProject()?.name, 'main-project-id'); - const bprojects = bworkspace.list(); - t.is(bprojects[0].workflows[0].steps.length, 2); - t.is(bprojects[0].workflows[0].steps[1].name, 'Job A'); + const beforeWs = new Workspace('/ws'); + t.is(beforeWs.activeProject.id, 'my-project'); + const beforeProjects = beforeWs.list(); + t.is(beforeProjects[0].workflows[0].steps.length, 2); + t.is(beforeProjects[0].workflows[0].steps[1].name, 'Job A'); // do merging await mergeHandler( { command: 'merge', projectPath: '/ws', - projectName: 'sandbox-project-id', + projectId: 'my-sandbox', removeUnmapped: false, workflowMappings: {}, }, logger ); - // state of main projects workflow before sandbox is merged in - const workspace = new Workspace('/ws'); - t.is(workspace.projectMeta.name, 'main-project-id'); - t.is(workspace.getActiveProject()?.name, 'main-project-id'); - const projects = workspace.list(); - t.is(projects[0].workflows[0].steps.length, 3); - t.is(projects[0].workflows[0].steps[1].name, 'Job X'); - t.is(projects[0].workflows[0].steps[1].openfn?.uuid, 'job-a'); // id got retained - t.is(projects[0].workflows[0].steps[2].name, 'Job Y'); - t.is(projects[0].workflows[0].steps[2].openfn?.uuid, 'job-y'); // id not retained - new nod + // state of main projects workflow AFTER sandbox is merged in + const afterWorkspace = new Workspace('/ws'); + t.is(afterWorkspace.activeProject.id, 'my-project'); + const afterProjects = afterWorkspace.list(); + const wf = afterProjects[0].workflows[0]; + t.is(wf.steps.length, 3); + t.is(wf.steps[1].name, 'Job X'); + t.is(wf.steps[1].openfn?.uuid, 'job-a'); // id got retained + t.is(wf.steps[2].name, 'Job Y'); + t.is(wf.steps[2].openfn?.uuid, 'job-y'); // id not retained - new nod const { message, level } = logger._parse(logger._last); t.is(level, 'success'); t.is( message, - 'Project sandbox-project-id has been merged into Project main-project-id successfully' + 'Project my-sandbox has been merged into Project my-project successfully' ); }); diff --git a/packages/cli/test/projects/handler.test.ts b/packages/cli/test/projects/handler.test.ts index 6a26a71cb..086b475e1 100644 --- a/packages/cli/test/projects/handler.test.ts +++ b/packages/cli/test/projects/handler.test.ts @@ -7,17 +7,21 @@ import { jsonToYaml } from '@openfn/project'; mock({ 'no-ws/': { 'some.yaml': 'name: smth' }, '/ws/openfn.yaml': jsonToYaml({ - name: 'some-project-name', - workflowRoot: 'workflows', - formats: { - openfn: 'yaml', - project: 'yaml', - workflow: 'yaml', + project: { + id: 'my-project', + }, + workspace: { + workflowRoot: 'workflows', + formats: { + openfn: 'yaml', + project: 'yaml', + workflow: 'yaml', + }, }, }), '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml({ - id: 'some-id', - name: 'some-project-name', + id: '', + name: 'my-project', workflows: [ { name: 'simple-workflow', @@ -93,15 +97,15 @@ test('openfn projects: not a workspace', (t) => { t.is(message, 'Command was run in an invalid openfn workspace'); }); -test('openfn projects: valid workspace', (t) => { +test.only('openfn projects: valid workspace', (t) => { projectsHandler({ command: 'projects', projectPath: '/ws' }, logger); const { message, level } = logger._parse(logger._last); t.is('success', level); t.is( `Available openfn projects -some-project-name (active) - some-id +my-project (active) + workflows: - simple-workflow - another-workflow diff --git a/packages/project/src/Workspace.ts b/packages/project/src/Workspace.ts index 2be350ccc..027cacb5d 100644 --- a/packages/project/src/Workspace.ts +++ b/packages/project/src/Workspace.ts @@ -54,7 +54,7 @@ export class Workspace { const stateFilePath = path.join(projectsPath, file); const data = fs.readFileSync(stateFilePath, 'utf-8'); const project = fromAppState(data, { format: 'yaml' }); - this.projectPaths.set(project.name, stateFilePath); + this.projectPaths.set(project.id, stateFilePath); return project; }) .filter((s) => s); @@ -85,8 +85,10 @@ export class Workspace { } getActiveProject() { - // TODO should use id, not name - return this.projects.find((p) => p.id === this.activeProject?.id); + return ( + this.projects.find((p) => p.id === this.activeProject?.id) ?? + this.projects.find((p) => p.openfn?.uuid === this.activeProject?.id) + ); } // TODO this needs to return default values diff --git a/packages/project/src/util/config.ts b/packages/project/src/util/config.ts index 37022161e..cc7a26efa 100644 --- a/packages/project/src/util/config.ts +++ b/packages/project/src/util/config.ts @@ -81,6 +81,7 @@ export const buildConfig = (config: WorkspaceConfig = {}) => ({ export const extractConfig = (source: Project) => { const project = { ...(source.openfn || {}), + id: source.id, }; const workspace = { ...source.config, diff --git a/packages/project/test/serialize/to-fs.test.ts b/packages/project/test/serialize/to-fs.test.ts index a455338aa..d864cb2bd 100644 --- a/packages/project/test/serialize/to-fs.test.ts +++ b/packages/project/test/serialize/to-fs.test.ts @@ -187,6 +187,7 @@ test('extractWorkflow: single simple workflow with custom root', (t) => { test('toFs: extract a project with 1 workflow and 1 step', (t) => { const project = new Project( { + name: 'My Project', workflows: [ { id: 'my-workflow', @@ -219,7 +220,9 @@ test('toFs: extract a project with 1 workflow and 1 step', (t) => { formats: { openfn: 'json', project: 'yaml', workflow: 'json' }, dirs: { projects: '.projects', workflows: 'workflows' }, }, - project: {}, + project: { + id: 'my-project', + }, }); const workflow = JSON.parse(files['workflows/my-workflow/my-workflow.json']); diff --git a/packages/project/test/util/config.test.ts b/packages/project/test/util/config.test.ts index b463d52d0..8bc520264 100644 --- a/packages/project/test/util/config.test.ts +++ b/packages/project/test/util/config.test.ts @@ -167,6 +167,7 @@ test('generate openfn.yaml', (t) => { result.content, `project: uuid: 1234 + id: my-project workspace: formats: openfn: yaml