@iexec/iapp
Version:
A CLI to guide you through the process of building an iExec iApp
187 lines (186 loc) • 8.87 kB
JavaScript
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { getSpinner } from '../cli-helpers/spinner.js';
import { fileExists } from '../utils/fs.utils.js';
import { PROTECTED_DATA_MOCK_DIR } from '../config/config.js';
import { handleCliError } from '../cli-helpers/handleCliError.js';
import { createZipFromObject, extractDataSchema, ALLOWED_KEY_NAMES_REGEXP, } from '../libs/dataprotector.js';
import { goToProjectRoot } from '../cli-helpers/goToProjectRoot.js';
import * as color from '../cli-helpers/color.js';
import { hintBox, objectBox } from '../cli-helpers/box.js';
export async function mockProtectedData() {
const spinner = getSpinner();
try {
await goToProjectRoot({ spinner });
async function buildData({ dataState = {}, dataSchema = {} } = {}) {
// get data fragment key
const { key } = await spinner.prompt({
type: 'text',
name: 'key',
message: "Protected Data is an object holding different asset types (email, passport, ID, etc.). Define a key to access it? (use '.' to access nested keys)",
});
// check key is valid
const keyPath = key.split('.');
const keyFragmentErrors = keyPath
.map((fragment) => {
if (fragment === '') {
return `Empty key fragment`;
}
if (!ALLOWED_KEY_NAMES_REGEXP.test(fragment)) {
return `Unsupported special character in key`;
}
})
.filter(Boolean);
// get data fragment type
if (keyFragmentErrors.length === 0) {
const BOOLEAN = 'boolean';
const NUMBER = 'number';
const STRING = 'string';
const FILE = 'file';
const { type } = await spinner.prompt({
type: 'select',
name: 'type',
message: `What kind of data is \`${key}\`?`,
choices: [
{ title: BOOLEAN, value: BOOLEAN },
{ title: NUMBER, value: NUMBER },
{ title: STRING, value: STRING },
{ title: FILE, value: FILE },
{ title: `My bad, I don't want to add data at \`${key}\`` },
],
});
// get data fragment value
let value = undefined;
switch (type) {
case BOOLEAN:
{
const res = await spinner.prompt({
type: 'select',
name: 'value',
message: `What is the value of \`${key}\`?`,
choices: [
{ title: 'true', value: true },
{ title: 'false', value: false },
],
});
value = res.value;
}
break;
case NUMBER:
{
const res = await spinner.prompt({
type: 'text',
name: 'value',
message: `What is the value of \`${key}\`? (${NUMBER})`,
});
const numValue = Number(res.value);
if (!Number.isNaN(numValue)) {
value = numValue;
}
else {
spinner.warn('Invalid input, should be a number');
}
}
break;
case STRING:
{
const res = await spinner.prompt({
type: 'text',
name: 'value',
message: `What is the value of \`${key}\`? (${STRING})`,
});
value = res.value;
}
break;
case FILE:
{
const { path } = await spinner.prompt({
type: 'text',
name: 'path',
message: `Where is the file located? (path)`,
});
const exists = await fileExists(path);
if (exists) {
const stats = await stat(path);
if (stats.isFile()) {
const buffer = await readFile(path);
value = buffer;
}
else {
spinner.warn('Invalid path, the node at specified path is not a file');
}
}
else {
spinner.warn('Invalid path, no file at specified path');
}
}
break;
default:
break;
}
// build data fragment
if (value !== undefined) {
const setNestedKeyValue = (obj, path, value) => {
const [currentKey, ...nextPath] = path;
if (nextPath.length === 0) {
obj[currentKey] = value;
}
else {
// create nested object if needed
if (typeof obj[currentKey] !== 'object' || // key is not an object
obj[currentKey].constructor.name !== 'Object' // key is a special object (file Buffer for example)
) {
obj[currentKey] = {};
}
setNestedKeyValue(obj[currentKey], nextPath, value);
}
};
setNestedKeyValue(dataState, keyPath, value);
const schema = await extractDataSchema({ dataType: value });
setNestedKeyValue(dataSchema, keyPath, schema.dataType);
}
}
else {
spinner.warn(`Invalid key: ${keyFragmentErrors.join(', ')}`);
}
spinner.info(`This is how your protectedData looks so far:
${objectBox(JSON.stringify(dataSchema, null, 2))}`);
const { addMore } = await spinner.prompt({
type: 'confirm',
name: 'addMore',
message: `Would you add more data?`,
initial: true,
});
if (addMore) {
return buildData({ dataState, dataSchema });
}
return dataState;
}
spinner.info('Answer a few questions to create your custom protectedData mock');
spinner.start('Building protectedData mock...');
const { mockName } = await spinner.prompt({
type: 'text',
name: 'mockName',
message: 'Choose a name for your protectedData mock (you will be able to use your mock in tests like this `iapp test --protectedData <name>`)',
initial: 'default',
});
const data = await buildData();
if (Object.keys(data).length === 0) {
throw Error('Data is empty, creation aborted');
}
spinner.start(`Creating protectedData mock file in ${color.file(PROTECTED_DATA_MOCK_DIR)} directory...`);
const unencryptedData = await createZipFromObject(data);
const schema = await extractDataSchema(data);
await mkdir(PROTECTED_DATA_MOCK_DIR, { recursive: true });
await writeFile(join(PROTECTED_DATA_MOCK_DIR, mockName), unencryptedData);
spinner.succeed(`Mocked protectedData ${color.file(mockName)} created in ${color.file(PROTECTED_DATA_MOCK_DIR)} directory`);
spinner.log(hintBox(`protectedData mock "${mockName}" schema:
${color.command(objectBox(JSON.stringify(schema, null, 2)))}
Use your mock in tests:
${color.command(`iapp test --protectedData "${mockName}"`)}`));
}
catch (error) {
handleCliError({ spinner, error });
}
}
//# sourceMappingURL=mock-protected-data.js.map