Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

100 implement upload command #148

Merged
merged 6 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { buildApp } from "@/lib/build";
import { initProject } from "@/lib/init";
import { Logger, LogLevel } from "@/lib/logger";
import { Command } from "commander";
import { deploy, deployAppData, DeployOptions } from "./deploy";
import { deploy, deployAppData, deployAppDataFolders, DeployOptions } from "./deploy";
import { dev } from "./dev";
import { cloneRepository } from "./repository";
import { buildWorkspace, devWorkspace } from "./workspace";
Expand Down Expand Up @@ -143,6 +143,26 @@ async function run() {
})
});

program
bb-face marked this conversation as resolved.
Show resolved Hide resolved
.command("uploadData")
.description("Upload data to SocialDB from bos.config.json configuration")
.argument("[appName]", "Workspace app name to deploy")
.option("-n, --network <network>", "network to deploy to", "mainnet")
.option("--signerPublicKey <string>", "Signer public key")
.option("--signerPrivateKey <string>", "Signer private key")
.action(async (appName, options) => {
const deployOptions: DeployOptions = {
signerPublicKey: options.signerPublicKey,
signerPrivateKey: options.signerPrivateKey,
network: options.network,
deployAccountId: options.deployAccountId,
};

await deployAppDataFolders(appName, deployOptions).catch((e: Error) => {
log.error(e.stack || e.message);
})
});

program.parse();
}

Expand Down
6 changes: 6 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export interface BaseConfig {
index?: string; // widget to use as index
aliasPrefix?: string; // prefix to use for aliases, default is "alias"
aliasesContainsPrefix?: boolean; // aliases keys contains prefix (default is false)
data?: {
include: string[]; // specify folder's array to upload along with widget
}
}

interface NetworkConfig {
Expand Down Expand Up @@ -62,6 +65,9 @@ const baseConfigSchema = Joi.object({
aliasPrefix: Joi.string().allow(null),
aliasesContainsPrefix: Joi.boolean().allow(null),
index: Joi.string().allow(null),
data: Joi.object({
include: Joi.array().items(Joi.string()).min(1).required()
}).optional()
});

const networkConfigSchema = Joi.object({
Expand Down
99 changes: 98 additions & 1 deletion lib/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import fs from "fs";
import { buildApp } from "@/lib/build";
import { readConfig } from "@/lib/config";
import { Network } from "@/lib/types";
import { move, pathExists, readdir, remove } from "@/lib/utils/fs";
import { move, pathExists, processDirectory, readdir, remove } from "@/lib/utils/fs";
import { readWorkspace } from "@/lib/workspace";
import { SOCIAL_CONTRACT } from './server';

Expand Down Expand Up @@ -176,6 +176,103 @@ export async function deployAppData(appName: string, opts: DeployOptions) {
});
}

export async function deployAppDataFolders(
appName: string,
opts: DeployOptions
) {
const config = await readConfig(
path.join(appName, "bos.config.json"),
opts.network
);
const BOS_SIGNER_ACCOUNT_ID =
config.accounts.signer || opts.signerAccountId || config.account;

if (!BOS_SIGNER_ACCOUNT_ID) {
console.log(
`App account is not defined for ${appName}. Skipping data upload`
);
return;
}

if (
!config.data ||
!Array.isArray(config.data.include) ||
config.data.include.length === 0
)
throw new Error(
"Config must contain a data.include array with at least one folder"
);

const result = {};

for (const folder of config.data.include) {
const folderName = path.basename(folder);
result[folderName] = {};
await processDirectory(folder, '', result[folderName]);
}

const args = {
data: {
[config.account]: result,
},
};

const argsBase64 = Buffer.from(JSON.stringify(args)).toString("base64");

let command = [
"near-cli-rs",
"contract",
"call-function",
"as-transaction",
opts.network === "mainnet"
? SOCIAL_CONTRACT.mainnet
: SOCIAL_CONTRACT.testnet,
"set",
"base64-args",
`${argsBase64}`,
"prepaid-gas",
"300 TeraGas",
"attached-deposit",
"0.15 NEAR", // deposit
"sign-as",
BOS_SIGNER_ACCOUNT_ID,
"network-config",
opts.network,
];

const BOS_SIGNER_PUBLIC_KEY = opts?.signerPublicKey;
const BOS_SIGNER_PRIVATE_KEY = opts?.signerPrivateKey;

const automaticSignIn = [
"sign-with-plaintext-private-key",
"--signer-public-key",
BOS_SIGNER_PUBLIC_KEY,
"--signer-private-key",
BOS_SIGNER_PRIVATE_KEY,
"send",
];

if (BOS_SIGNER_PUBLIC_KEY && BOS_SIGNER_PRIVATE_KEY)
command = command.concat(automaticSignIn);

const deployProcess = spawn("npx", command, {
cwd: path.join(appName, DEPLOY_DIST_FOLDER),
stdio: "inherit",
});

deployProcess.on("close", (code) => {
if (code === 0) {
console.log(`Uploaded data for ${appName}`);
} else {
console.error(`Data upload failed with code ${code}`);
}
});

deployProcess.on("error", (err) => {
console.error(`Error uploading data for ${appName}:\n${err.message}`);
});
}

export async function deploy(appName: string, opts: DeployOptions) {
const src = ".";

Expand Down
29 changes: 28 additions & 1 deletion lib/utils/fs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { copy, ensureDir, move, outputFile, pathExists, readdir, readFile, readJson, remove, writeJson, existsSync, promises } from 'fs-extra';
import { readdirSync, readFileSync } from 'fs';
import path from 'path';

async function loopThroughFiles(pwd: string, callback: (file: string) => Promise<void>) {
Expand All @@ -16,4 +17,30 @@ async function loopThroughFiles(pwd: string, callback: (file: string) => Promise
}
}

export { copy, ensureDir, loopThroughFiles, move, outputFile, pathExists, readdir, readFile, readJson, remove, writeJson, existsSync, promises };
async function processDirectory(baseDir, currentDir, result) {
const files = await readdirSync(path.join(baseDir, currentDir), { withFileTypes: true });
for (const file of files) {
const relativePath = path.join(currentDir, file.name);
const fullPath = path.join(baseDir, relativePath);

if (file.isDirectory()) {
await processDirectory(baseDir, relativePath, result);
} else if (path.extname(file.name).toLowerCase() === '.json') {
try {
const fileContent = await readFileSync(fullPath, 'utf8');
const jsonContent = JSON.parse(fileContent);
let key;
if (currentDir === '') {
key = path.basename(file.name, '.json');
} else {
key = path.join(currentDir, path.basename(file.name, '.json')).replace(/[\\/]/g, '.');
}
result[key] = { "": JSON.stringify(jsonContent) };
} catch (error) {
console.error(`Error processing file ${fullPath}:`, error);
}
}
}
}

export { copy, ensureDir, loopThroughFiles, move, outputFile, pathExists, readdir, readFile, readJson, remove, writeJson, existsSync, promises, processDirectory };
65 changes: 64 additions & 1 deletion tests/unit/devNoMock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { startFileWatcher } from "@/lib/watcher";
import path from "path";
import fs from "fs";
import { processDirectory } from "@/lib/utils/fs";

describe("File Watcher Tests", () => {
let watcher;
Expand Down Expand Up @@ -37,4 +38,66 @@ describe("File Watcher Tests", () => {
fs.rmSync(tempDirPath, { recursive: true, force: true });
}
});
});
});

describe('Folder structure processing', () => {
const tempDirPath = path.join(__dirname, 'temp_test_dir');
const testFolderPath = path.join(tempDirPath, 'test');
const nestedTestFolderPath = path.join(testFolderPath, 'nestedTest');
const test1FolderPath = path.join(tempDirPath, 'test1');

beforeAll(() => {
fs.mkdirSync(tempDirPath, { recursive: true });
fs.mkdirSync(testFolderPath, { recursive: true });
fs.mkdirSync(nestedTestFolderPath, { recursive: true });
fs.mkdirSync(test1FolderPath, { recursive: true });

fs.writeFileSync(
path.join(testFolderPath, 'file.json'),
JSON.stringify({ data: "this is test" })
);
fs.writeFileSync(
path.join(nestedTestFolderPath, 'nestedFile.json'),
JSON.stringify({ data: "this is a nested folder", data2: "other data" })
);
fs.writeFileSync(
path.join(test1FolderPath, 'file1.json'),
JSON.stringify({ data: "this is test1" })
);
});

it("should build the correct object based on folder structure", async () => {
const config = {
data: {
include: [tempDirPath]
},
account: 'myaccount'
};

const result = {};

for (const folder of config.data.include) {
const folderName = path.basename(folder);
result[folderName] = {};
await processDirectory(folder, '', result[folderName]);
}

expect(result).toEqual({
'temp_test_dir': {
'test.nestedTest.nestedFile': {
'': JSON.stringify({ data: "this is a nested folder", data2: "other data" })
},
'test.file': {
'': JSON.stringify({ data: "this is test" })
},
'test1.file1': {
'': JSON.stringify({ data: "this is test1" })
}
}
});
});

afterAll(() => {
fs.rmSync(tempDirPath, { recursive: true, force: true });
});
});