Skip to content
5 changes: 5 additions & 0 deletions .changeset/brave-islands-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openfn/cli': minor
---

Support aliases on all project subcommands
5 changes: 5 additions & 0 deletions .changeset/tiny-forks-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openfn/cli': minor
---

Auto-load collections adptor when using collections
11 changes: 10 additions & 1 deletion packages/cli/src/execute/command.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import yargs from 'yargs';
import { build, ensure } from '../util/command-builders';
import { build, ensure, override } from '../util/command-builders';
import * as o from '../options';

import type { Opts } from '../options';

export type ExecuteOptions = Required<
Pick<
Opts,
| 'apiKey'
| 'adaptors'
| 'autoinstall'
| 'baseDir'
| 'cacheSteps'
| 'command'
| 'compile'
| 'credentials'
| 'collectionsEndpoint'
| 'collectionsVersion'
| 'expandAdaptors'
| 'end'
| 'immutable'
Expand Down Expand Up @@ -44,10 +47,16 @@ const options = [
o.expandAdaptors, // order is important

o.adaptors,
override(o.apikey, {
description: 'API token for collections',
alias: ['collections-api-key', 'collections-token', 'pat'],
}),
o.autoinstall,
o.cacheSteps,
o.compile,
o.credentials,
o.collectionsEndpoint,
o.collectionsVersion,
o.end,
o.ignoreImports,
o.immutable,
Expand Down
26 changes: 25 additions & 1 deletion packages/cli/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type Opts = {
configPath?: string;
confirm?: boolean;
credentials?: string;
collectionsEndpoint?: string;
collectionsVersion?: string;
describe?: string;
end?: string; // workflow end node
expandAdaptors?: boolean; // for unit tests really
Expand Down Expand Up @@ -141,7 +143,12 @@ export const apikey: CLIOption = {
yargs: {
alias: ['key', 'pat', 'token'],
description:
'[beta only] API Key, Personal Access Token (Pat), or other access token',
'API Key, Personal Access Token (PAT), or other access token from Ligtning',
},
ensure: (opts: any) => {
if (!opts.apikey) {
opts.apiKey = process.env.OPENFN_API_KEY;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO I should really set a helper in the CLI for this - just pass the env var name. Also need to apply this to other options

}
},
};

Expand Down Expand Up @@ -240,6 +247,23 @@ export const configPath: CLIOption = {
},
};

export const collectionsVersion: CLIOption = {
name: 'collections-version',
yargs: {
description:
'The version of the collections adaptor to use. Defaults to latest. Use OPENFN_COLLECTIONS_VERSION env.',
},
};

export const collectionsEndpoint: CLIOption = {
name: 'collections-endpoint',
yargs: {
alias: ['endpoint'],
description:
'The Lightning server to use for collections. Will use the project endpoint if available. Use OPENFN_COLLECTIONS_ENDPOINT env.',
},
};

export const credentials: CLIOption = {
name: 'credentials',
yargs: {
Expand Down
70 changes: 69 additions & 1 deletion packages/cli/src/util/load-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,70 @@ const ensureAdaptors = (plan: CLIExecutionPlan) => {
});
};

type ensureCollectionsOptions = {
endpoint?: string;
version?: string;
apiKey?: string;
};

const ensureCollections = (
plan: CLIExecutionPlan,
{
endpoint = 'https://app.openfn.org',
version = 'latest',
apiKey = 'null',
}: ensureCollectionsOptions = {},
logger?: Logger
) => {
let collectionsFound = false;

Object.values(plan.workflow.steps)
.filter((step) => (step as any).expression?.match(/(collections\.)/))
.forEach((step) => {
const job = step as CLIJobNode;
if (
!job.adaptors?.find((v: string) =>
v.startsWith('@openfn/language-collections')
)
) {
collectionsFound = true;
job.adaptors ??= [];
job.adaptors.push(
`@openfn/language-collections@${version || 'latest'}`
);

job.configuration = Object.assign({}, job.configuration, {
collections_endpoint: `${endpoint}/collections`,
collections_token: apiKey,
});
}
});

if (collectionsFound) {
if (!apiKey || apiKey === 'null') {
logger?.warn(
'WARNING: collections API was not set. Pass --api-key or OPENFN_API_KEY'
);
}
logger?.info(
`Configured collections to use endpoint ${endpoint} and API Key ending with ${apiKey?.substring(
apiKey.length - 10
)}`
);
}
};

const loadXPlan = async (
plan: CLIExecutionPlan,
options: Pick<
Opts,
'monorepoPath' | 'baseDir' | 'expandAdaptors' | 'globals'
| 'monorepoPath'
| 'baseDir'
| 'expandAdaptors'
| 'globals'
| 'collectionsVersion'
| 'collectionsEndpoint'
| 'apiKey'
>,
logger: Logger,
defaultName: string = ''
Expand All @@ -348,6 +407,15 @@ const loadXPlan = async (
plan.workflow.name = defaultName;
}
ensureAdaptors(plan);
ensureCollections(
plan,
{
version: options.collectionsVersion,
apiKey: options.apiKey,
endpoint: options.collectionsEndpoint,
},
logger
);

// import global functions
// if globals is provided via cli argument. it takes precedence
Expand Down
88 changes: 87 additions & 1 deletion packages/cli/test/util/load-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { createMockLogger } from '@openfn/logger';
import type { Job } from '@openfn/lexicon';

import loadPlan from '../../src/util/load-plan';
import { Opts } from '../../src/options';
import {
collectionsEndpoint,
collectionsVersion,
Opts,
} from '../../src/options';

const logger = createMockLogger(undefined, { level: 'debug' });

Expand All @@ -28,6 +32,7 @@ const createPlan = (steps: Partial<Job>[] = []) => ({
test.beforeEach(() => {
mock({
'test/job.js': 'x',
'test/collections.js': 'collections.get()',
'test/wf-old.json': JSON.stringify({
start: 'a',
jobs: [{ id: 'a', expression: 'x()' }],
Expand Down Expand Up @@ -114,6 +119,50 @@ test.serial('expression: set a start on the plan', async (t) => {
t.is(plan.options.start, 'x');
});

test.serial('expression: load the collections adaptor', async (t) => {
const opts = {
expressionPath: 'test/collections.js',
} as Partial<Opts>;

const plan = await loadPlan(opts as Opts, logger);

t.deepEqual(plan.workflow.steps[0].adaptors, [
'@openfn/language-collections@latest',
]);
});

test.serial(
'expression: load the collections adaptor with another',
async (t) => {
const opts = {
expressionPath: 'test/collections.js',
adaptors: ['@openfn/language-common@latest'],
} as Partial<Opts>;

const plan = await loadPlan(opts as Opts, logger);

t.deepEqual(plan.workflow.steps[0].adaptors, [
'@openfn/language-common@latest',
'@openfn/language-collections@latest',
]);
}
);
test.serial(
'expression: load the collections adaptor with a specific version',
async (t) => {
const opts = {
expressionPath: 'test/collections.js',
collectionsVersion: '1.1.1',
} as Partial<Opts>;

const plan = await loadPlan(opts as Opts, logger);

t.deepEqual(plan.workflow.steps[0].adaptors, [
'@openfn/language-collections@1.1.1',
]);
}
);

test.serial('xplan: load a plan from workflow path', async (t) => {
const opts = {
workflowPath: 'test/wf.json',
Expand Down Expand Up @@ -343,3 +392,40 @@ test.serial('xplan: support multiple adaptors', async (t) => {
// @ts-ignore
t.is(step.adaptor, undefined);
});

test.serial('xplan: append collections', async (t) => {
const opts = {
workflowPath: 'test/wf.json',
collectionsVersion: '1.1.1',
collectionsEndpoint: 'https://localhost:4000/',
apiKey: 'abc',
};

const plan = createPlan([
{
id: 'a',
expression: 'collections.get()',
adaptors: ['@openfn/language-common@1.0.0'],
},
]);

mock({
'test/wf.json': JSON.stringify(plan),
});

const result = await loadPlan(opts, logger);
t.truthy(result);

const step = result.workflow.steps[0] as Job;
t.deepEqual(step.adaptors, [
'@openfn/language-common@1.0.0',
'@openfn/language-collections@1.1.1',
]);
// @ts-ignore
t.is(step.adaptor, undefined);

t.deepEqual(step.configuration, {
collections_endpoint: `${opts.collectionsEndpoint}/collections`,
collections_token: opts.apiKey,
});
});