From 2583364aa77220c0895191193cac74c323cd0b2c Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Thu, 16 May 2024 19:29:17 -0600 Subject: [PATCH 1/4] added ability to generate fake users for import testing --- package-lock.json | 16 +++++ package.json | 1 + src/commands/import-generate.ts | 121 ++++++++++++++++++++++++++++++++ src/commands/index.ts | 1 + 4 files changed, 139 insertions(+) create mode 100644 src/commands/import-generate.ts diff --git a/package-lock.json b/package-lock.json index 0e850b1..80fdf4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@commander-js/extra-typings": "11.0.0", + "@faker-js/faker": "^8.4.1", "@fusionauth/typescript-client": "1.47.0", "chalk": "5.3.0", "chokidar": "3.5.3", @@ -58,6 +59,21 @@ "node": ">=12" } }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@fusionauth/typescript-client": { "version": "1.47.0", "resolved": "https://registry.npmjs.org/@fusionauth/typescript-client/-/typescript-client-1.47.0.tgz", diff --git a/package.json b/package.json index 4013424..2b14b0a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "homepage": "https://github.com/FusionAuth/fusionauth-node-cli#readme", "dependencies": { "@commander-js/extra-typings": "11.0.0", + "@faker-js/faker": "^8.4.1", "@fusionauth/typescript-client": "1.47.0", "chalk": "5.3.0", "chokidar": "3.5.3", diff --git a/src/commands/import-generate.ts b/src/commands/import-generate.ts new file mode 100644 index 0000000..bd414fc --- /dev/null +++ b/src/commands/import-generate.ts @@ -0,0 +1,121 @@ +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 { faker } from '@faker-js/faker'; +import * as fs from 'fs'; + +const action = async function ({numberOfFiles, countPerFile, applicationId, groupId, filePath } +: { + numberOfFiles?: string | undefined; + countPerFile?: string | undefined; + applicationId?: string | undefined; + groupId?: string | undefined; + filePath?: string | undefined; +} +): Promise { + console.log(`Generating users`); + try { + const finalNumberOfFiles = (numberOfFiles !== undefined ? parseInt(numberOfFiles) : 10); + const finalCountPerFile = (countPerFile !== undefined ? parseInt(countPerFile) : 1000); + const finalFilePath = (filePath !== undefined ? filePath : "tmp/output"); + const finalAppId = (applicationId !== undefined ? applicationId : '85a03867-dccf-4882-adde-1a79aeec50df'); + const finalGroupId = (groupId !== undefined ? groupId : 'a730d8c9-d060-4016-935e-170a5baaa4c7'); + + for (let i = 0; i < finalNumberOfFiles; i++) { + const jsonData = generateData(finalCountPerFile, finalAppId, finalGroupId, i * finalCountPerFile); + fs.writeFile(finalFilePath+i, JSON.stringify({"users": jsonData}), (err) => { + if (err) { + console.error('Error writing to file:', err); + return; + } + //console.log('Data has been written to', finalFilePath); + }); + } + console.log(chalk.green(`Users generated`)); + } + catch (e: unknown) { + errorAndExit(`Error updating lambda: `, e); + } +} + +// noinspection JSUnusedGlobalSymbols +export const importGenerate = new Command('import:generate') + .description('Generate sample import data') + .option('-n, --numberOfFiles ', 'The number of files.') + .option('-c, --countPerFile ', 'The count of records per file.') + .option('-a, --applicationId ', 'The application to register users to.') + .option('-g, --groupId ', 'The group id to add users to.') + .option('-f, --filePath ', 'The file path to output sample files to.') + .action(action); + + +function generateData(numObjects: number, appId: string, groupId: string, startNumber: number) { + const data = []; + for (let i = 0; i < numObjects; i++) { + const obj = { + active: true, + birthDate: faker.date.past().toISOString().split('T')[0], + data: { + displayName: faker.person.firstName() + ' ' + faker.person.lastName(), + favoriteColors: [faker.internet.color(), faker.internet.color()] + }, + email: `example${i + 1 + startNumber}@example.com`, + encryptionScheme: 'salted-pbkdf2-hmac-sha256', + expiry: faker.date.future().getTime(), + factor: 24000, + firstName: faker.person.firstName(), + fullName: faker.person.fullName(), + imageUrl: faker.image.url(), + insertInstant: faker.date.past().getTime(), + lastName: faker.person.lastName(), + memberships: [ + { + data: { + externalId: faker.string.uuid() + }, + groupId: groupId + } + ], + middleName: faker.person.middleName(), + mobilePhone: faker.phone.number(), + password: "yjs0Mj2qttSprPgtTVb+iGNMc66yBawfO1GXVTR3z7g=", + passwordChangeRequired: false, + preferredLanguages: ['en_US','en_GB'], + registrations: [ + { + applicationId: appId, + data: { + birthplace: faker.location.city() + }, + insertInstant: faker.date.past().getTime(), + preferredLanguages: ['en_US'], + username: faker.internet.userName(), + verified: faker.datatype.boolean() + } + ], + salt: 'k0eyvRy0S8lFp+IArLRGFJrm6dNM3tVGuAztU38lS3A=', + timezone: faker.location.timeZone(), + twoFactor: { + methods: [ + { + method: 'sms', + mobilePhone: faker.phone.number() + }, + { + method: 'email', + email: `example${i + 1 + startNumber}@example.com` + } + ] + }, + usernameStatus: 'ACTIVE', + username: `example${i + 1 + startNumber}`, + verified: faker.datatype.boolean() + }; + data.push(obj); + } + return data; +} + diff --git a/src/commands/index.ts b/src/commands/index.ts index 1f56e64..b2ea02f 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -10,3 +10,4 @@ export * from './lambda-retrieve.js'; export * from './theme-watch.js'; export * from './theme-upload.js'; export * from './theme-download.js'; +export * from './import-generate.js'; From d206fd28c53f48befb47066088d9c6de510d6646 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 13 Dec 2025 13:08:29 -0700 Subject: [PATCH 2/4] improve help message --- src/commands/import-generate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/import-generate.ts b/src/commands/import-generate.ts index bd414fc..549ddec 100644 --- a/src/commands/import-generate.ts +++ b/src/commands/import-generate.ts @@ -48,7 +48,7 @@ export const importGenerate = new Command('import:generate') .option('-c, --countPerFile ', 'The count of records per file.') .option('-a, --applicationId ', 'The application to register users to.') .option('-g, --groupId ', 'The group id to add users to.') - .option('-f, --filePath ', 'The file path to output sample files to.') + .option('-f, --filePath ', 'The path and filename prefix to write files to. To output files to tmp/output0, tmp/output1, etc, use tmp/output.') .action(action); From 83ad31a827dde8073a05c30e7482a45be114d8eb Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 13 Dec 2025 13:11:14 -0700 Subject: [PATCH 3/4] documented the import:generate command --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6d94ff6..a66cc2c 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Currently, the CLI supports the following commands: - `fusionauth email:upload` - Upload a specific template or all email templates to a FusionAuth server. - `fusionauth email:watch` - Watch the email template directory and upload changes to a FusionAuth server. - `fusionauth email:create` - Create a new email template locally. +Fake user generation + - `fusionauth import:generate` - Generate JSON for importing fake users for testing. - Lambdas - `fusionauth lambda:update` - Update a lambda on a FusionAuth server. - `fusionauth lambda:delete` - Delete a lambda from a FusionAuth server. From 5450c46de6b09339278dc3d14346434a09758d94 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Wed, 24 Dec 2025 16:45:27 -0700 Subject: [PATCH 4/4] updated to separate out the directory and file prefix --- src/commands/import-generate.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/commands/import-generate.ts b/src/commands/import-generate.ts index 549ddec..a0e635a 100644 --- a/src/commands/import-generate.ts +++ b/src/commands/import-generate.ts @@ -7,31 +7,39 @@ import {errorAndExit} from '../utils.js'; import { faker } from '@faker-js/faker'; import * as fs from 'fs'; -const action = async function ({numberOfFiles, countPerFile, applicationId, groupId, filePath } +const action = async function ({numberOfFiles, countPerFile, applicationId, groupId, tmpDir, filePrefix} : { numberOfFiles?: string | undefined; countPerFile?: string | undefined; applicationId?: string | undefined; groupId?: string | undefined; - filePath?: string | undefined; + tmpDir?: string | undefined; + filePrefix?: string | undefined; } ): Promise { console.log(`Generating users`); try { const finalNumberOfFiles = (numberOfFiles !== undefined ? parseInt(numberOfFiles) : 10); const finalCountPerFile = (countPerFile !== undefined ? parseInt(countPerFile) : 1000); - const finalFilePath = (filePath !== undefined ? filePath : "tmp/output"); + const finalTmpDir = (tmpDir !== undefined ? tmpDir : "tmp"); + const finalFilePrefix = (filePrefix !== undefined ? filePrefix : "output"); const finalAppId = (applicationId !== undefined ? applicationId : '85a03867-dccf-4882-adde-1a79aeec50df'); const finalGroupId = (groupId !== undefined ? groupId : 'a730d8c9-d060-4016-935e-170a5baaa4c7'); + // Ensure the tmp directory exists + if (!fs.existsSync(finalTmpDir)) { + fs.mkdirSync(finalTmpDir, { recursive: true }); + } + for (let i = 0; i < finalNumberOfFiles; i++) { const jsonData = generateData(finalCountPerFile, finalAppId, finalGroupId, i * finalCountPerFile); - fs.writeFile(finalFilePath+i, JSON.stringify({"users": jsonData}), (err) => { + const filePath = join(finalTmpDir, finalFilePrefix + i); + fs.writeFile(filePath, JSON.stringify({"users": jsonData}), (err) => { if (err) { console.error('Error writing to file:', err); return; } - //console.log('Data has been written to', finalFilePath); + //console.log('Data has been written to', filePath); }); } console.log(chalk.green(`Users generated`)); @@ -48,7 +56,8 @@ export const importGenerate = new Command('import:generate') .option('-c, --countPerFile ', 'The count of records per file.') .option('-a, --applicationId ', 'The application to register users to.') .option('-g, --groupId ', 'The group id to add users to.') - .option('-f, --filePath ', 'The path and filename prefix to write files to. To output files to tmp/output0, tmp/output1, etc, use tmp/output.') + .option('-d, --tmpDir ', 'The directory to write files to.', 'tmp') + .option('-f, --filePrefix ', 'The file prefix for output files.', 'output') .action(action);