diff --git a/README.md b/README.md index 99b7381..702fe90 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Currently, the CLI supports the following commands: - `fusionauth email:create` - Create a new email template locally. - Lambdas - `fusionauth lambda:create` - Upload a lambda to a FusionAuth server. + - `fusionauth lambda:update` - Update a lambda on a FusionAuth server. - `fusionauth lambda:delete` - Delete a lambda from a FusionAuth server. - `fusionauth lambda:retrieve` - Download a lambda from a FusionAuth server. - Themes @@ -72,6 +73,9 @@ npm run build; # now you can use it npx fusionauth --version; + +# to get help on a command +npm run build; npx fusionauth lambda:link-to-application --help ``` To see examples of use look at https://fusionauth.io/docs/v1/tech/lambdas/testing. diff --git a/package-lock.json b/package-lock.json index 0e850b1..4b59011 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "figlet": "1.6.0", "fs-extra": "11.1.1", "html-to-text": "9.0.5", + "js-yaml": "^4.1.0", "log-symbols": "5.1.0", "log-update": "5.0.1", "merge": "2.1.1", @@ -31,6 +32,7 @@ "@types/figlet": "1.5.6", "@types/fs-extra": "11.0.1", "@types/html-to-text": "9.0.1", + "@types/js-yaml": "^4.0.5", "@types/node": "20.4.5", "@types/uuid": "9.0.2", "ts-node": "10.9.1", @@ -149,6 +151,12 @@ "integrity": "sha512-sHu702QGb0SP2F0Zt+CxdCmGZIZ0gEaaCjqOh/V4iba1wTxPVntEPOM/vHm5bel08TILhB3+OxUTkDJWnr/zHQ==", "dev": true }, + "node_modules/@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", + "dev": true + }, "node_modules/@types/jsonfile": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz", @@ -256,6 +264,11 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -586,6 +599,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", diff --git a/package.json b/package.json index 4013424..b905f5f 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "figlet": "1.6.0", "fs-extra": "11.1.1", "html-to-text": "9.0.5", + "js-yaml": "^4.1.0", "log-symbols": "5.1.0", "log-update": "5.0.1", "merge": "2.1.1", @@ -42,6 +43,7 @@ "@types/figlet": "1.5.6", "@types/fs-extra": "11.0.1", "@types/html-to-text": "9.0.1", + "@types/js-yaml": "^4.0.5", "@types/node": "20.4.5", "@types/uuid": "9.0.2", "ts-node": "10.9.1", diff --git a/src/commands/index.ts b/src/commands/index.ts index 1f56e64..ab7a57f 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -4,9 +4,10 @@ export * from './email-duplicate.js'; export * from './email-html-to-text.js'; export * from './email-upload.js'; export * from './email-watch.js'; -export * from './lambda-update.js'; +export * from './lambda-create.js'; export * from './lambda-delete.js'; export * from './lambda-retrieve.js'; +export * from './lambda-update.js'; export * from './theme-watch.js'; export * from './theme-upload.js'; export * from './theme-download.js'; diff --git a/src/commands/lambda-create.ts b/src/commands/lambda-create.ts new file mode 100644 index 0000000..0f4ac27 --- /dev/null +++ b/src/commands/lambda-create.ts @@ -0,0 +1,55 @@ +import {Command} from '@commander-js/extra-typings'; +import {FusionAuthClient} from '@fusionauth/typescript-client'; +import {readFile} from 'fs/promises'; +import chalk from 'chalk'; +import {join} from 'path'; +import {errorAndExit} from '../utils.js'; +import {apiKeyOption, hostOption} from "../options.js"; +import {load as loadYaml} from 'js-yaml'; + +const action = async function (lambdaId: string, {input, key: apiKey, host}: { + input: string; + key: string; + host: string +}): Promise { + console.log(`Creating lambda ${lambdaId} on ${host}`); + try { + const filename = join(input, lambdaId + ".yaml"); + const data = await readFile(filename, 'utf-8'); + const lambda = loadYaml(data) as object; + const fusionAuthClient = new FusionAuthClient(apiKey, host); + const clientResponse = await fusionAuthClient.createLambda(lambdaId, { lambda }); + if (!clientResponse.wasSuccessful()) { + errorAndExit(`Error creating lambda: `, clientResponse); + } + console.log(chalk.green(`Lambda created`)); + } + catch (e: unknown) { + errorAndExit(`Error creating lambda: `, e); + } +} + +// noinspection JSUnusedGlobalSymbols +export const lambdaCreate = new Command('lambda:create') + .summary('Create a lambda on FusionAuth') + .description(`Create a lambda on FusionAuth. +Example lambda .yaml file: + +body: | + function populate(jwt, user, registration) { + jwt.message = 'Hello World!'; + console.info('Hello World!'); + } +debug: true +engineType: GraalJS +id: f3b3b547-7754-452d-8729-21b50d111505 +insertInstant: 1692177291178 +lastUpdateInstant: 1692211131823 +name: '[ATestLambda]' +type: JWTPopulate + `) + .argument('', 'The lambda id to create. The lambda is read from the file .yaml in the directory.') + .option('-i, --input ', 'The input directory', './lambdas/') + .addOption(apiKeyOption) + .addOption(hostOption) + .action(action); diff --git a/src/commands/lambda-delete.ts b/src/commands/lambda-delete.ts index a22b261..82ea981 100644 --- a/src/commands/lambda-delete.ts +++ b/src/commands/lambda-delete.ts @@ -12,8 +12,9 @@ const action = async function (lambdaId: string, {key: apiKey, host}: { try { const fusionAuthClient = new FusionAuthClient(apiKey, host); const clientResponse = await fusionAuthClient.deleteLambda(lambdaId); - if (!clientResponse.wasSuccessful()) + if (!clientResponse.wasSuccessful()) { errorAndExit(`Error deleting lambda: `, clientResponse); + } console.log(chalk.green(`Lambda deleted`)); } catch (e: unknown) { diff --git a/src/commands/lambda-retrieve.ts b/src/commands/lambda-retrieve.ts index 4a99b44..3ad6ec0 100644 --- a/src/commands/lambda-retrieve.ts +++ b/src/commands/lambda-retrieve.ts @@ -6,6 +6,7 @@ import {join} from 'path'; import {mkdir, writeFile} from 'fs/promises'; import {errorAndExit, toJson} from '../utils.js'; import {apiKeyOption, hostOption} from "../options.js"; +import {dump as dumpYaml} from 'js-yaml'; const action = async function (lambdaId: string, {output, key: apiKey, host}: { output: string; @@ -16,12 +17,19 @@ const action = async function (lambdaId: string, {output, key: apiKey, host}: { try { const fusionAuthClient = new FusionAuthClient(apiKey, host); const clientResponse = await fusionAuthClient.retrieveLambda(lambdaId); - if (!clientResponse.wasSuccessful()) + if (!clientResponse.wasSuccessful()) { errorAndExit(`Error retrieving lambda: `, clientResponse); - if (!existsSync(output)) + } + if (!existsSync(output)) { await mkdir(output); - const filename = join(output, clientResponse.response.lambda?.id + ".json"); - await writeFile(filename, toJson(clientResponse.response.lambda)); + } + const filename = join(output, clientResponse.response.lambda?.id + ".yaml"); + const lambdaContent = clientResponse.response.lambda; + if (lambdaContent) { + lambdaContent.body = lambdaContent?.body?.replace(/\r\n/g, '\n'); // allow newlines in .yaml file + } + const yamlData = dumpYaml(lambdaContent, { styles: { '!!str': '|' } }); + await writeFile(filename, yamlData); console.log(chalk.green(`Lambda downloaded to ${filename}`)); } catch (e: unknown) { diff --git a/src/commands/lambda-update.ts b/src/commands/lambda-update.ts index c6e7ec4..cbaa7cc 100644 --- a/src/commands/lambda-update.ts +++ b/src/commands/lambda-update.ts @@ -5,6 +5,7 @@ import chalk from 'chalk'; import {join} from 'path'; import {errorAndExit} from '../utils.js'; import {apiKeyOption, hostOption} from "../options.js"; +import {load as loadYaml} from 'js-yaml'; const action = async function (lambdaId: string, {input, key: apiKey, host}: { input: string; @@ -13,14 +14,14 @@ const action = async function (lambdaId: string, {input, key: apiKey, host}: { }): Promise { console.log(`Updating lambda ${lambdaId} on ${host}`); try { - const filename = join(input, lambdaId + ".json"); + const filename = join(input, lambdaId + ".yaml"); const data = await readFile(filename, 'utf-8'); - const lambda = JSON.parse(data); - const request = { lambda }; + const lambda = loadYaml(data) as object; const fusionAuthClient = new FusionAuthClient(apiKey, host); - const clientResponse = await fusionAuthClient.updateLambda(lambdaId, request); - if (!clientResponse.wasSuccessful()) + const clientResponse = await fusionAuthClient.updateLambda(lambdaId, {lambda} ); + if (!clientResponse.wasSuccessful()) { errorAndExit(`Error updating lambda: `, clientResponse); + } console.log(chalk.green(`Lambda updated`)); } catch (e: unknown) { @@ -31,7 +32,7 @@ const action = async function (lambdaId: string, {input, key: apiKey, host}: { // noinspection JSUnusedGlobalSymbols export const lambdaUpdate = new Command('lambda:update') .description('Update a lambda on FusionAuth') - .argument('', 'The lambda id to update. The lambda is read from the file .json in the directory.') + .argument('', 'The lambda id to update. The lambda is read from the file .yaml in the directory.') .option('-i, --input ', 'The input directory', './lambdas/') .addOption(apiKeyOption) .addOption(hostOption) diff --git a/src/index.ts b/src/index.ts index 8001081..997136c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,15 +11,15 @@ const fusionString = figlet.textSync('Fusion').split('\n'); const authString = figlet.textSync('Auth').split('\n'); fusionString.forEach((line, i) => { - console.log(chalk.white(line) + chalk.hex('#F58320')(authString[i])); + console.log(chalk.white(line) + chalk.hex('#F58320')(authString[i])); }); const program = new Command(); program - .name('@fusionauth/cli') - .description('CLI for FusionAuth') - .version(pkg.version); + .name('@fusionauth/cli') + .description('CLI for FusionAuth') + .version(pkg.version); Object.values(commands).forEach(command => program.addCommand(command)); -program.parse(); +program.parse(); \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 0d7fa64..f3b4683 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import ClientResponse from '@fusionauth/typescript-client/build/src/ClientResponse.js'; +import {FusionAuthClient, Application} from '@fusionauth/typescript-client'; import {Errors} from '@fusionauth/typescript-client'; import chalk from 'chalk'; @@ -126,3 +127,18 @@ export function errorAndExit(message: string, error?: any) { reportError(message, error); process.exit(1); } + +export async function getApplication(applicationId: string, + {key: apiKey, host}: + { + key: string; + host: string + } + ): Promise +{ + const fusionAuthClient = new FusionAuthClient(apiKey, host); + const clientResponse = await fusionAuthClient.retrieveApplication(applicationId); + if (!clientResponse.wasSuccessful() || !clientResponse.response.application) + errorAndExit(`Error retrieving application: `, clientResponse); + return clientResponse.response.application as Application; +} \ No newline at end of file