From 4a89a67f08e35efc807fc59465834a2c04a37deb Mon Sep 17 00:00:00 2001 From: tomoya yokota Date: Tue, 4 Dec 2018 13:58:32 +0900 Subject: [PATCH] feat: add import subcommand to import a customize setting from an existing app (#56) fix #24 * Generate customize-manifest.json from the app setting #Issue-24 Introduce --import-customize-setting option. With this option (and --dest option), this tool fetch customize settings, And then generate customize-manifest.json. customize-manifest.json has a format of this tool. finally, download uploaded files and put it in dest directroy. At kintone customize partner, there are kintone application which already has been customized. kintone-customize-uploader is very good tool, but It is not usefull in contining development situation. If I tried this tool at contining developmenet situation, I must create customize-manifest.json using various APIs Or copy from browser by hand. By this option, I can introduce this tool more easily :) * Use mkdirp library (v6/v8 hasn't api for make directory recursively) * Add Test Code and fix Promise warning - Add test code about api request history - Improve promise warning * Fix for test at windows os. use rimraf * Introduce import subcommand alternative of --import-customize-settings * Use sync method on writing file or creating directory * Add TypeAnnotation for GetAppCustomizeResp * Fix async/wait problem * Add log message * Fix for windows platform test * fix wait test * Add SubCommands help * Replace ${sep} to "/" * Fix test for multiplat form path * Remove unused option * simplify array#concat * return Promise --- packages/customize-uploader/bin/cli.js | 43 +++- packages/customize-uploader/package-lock.json | 114 +++++----- packages/customize-uploader/package.json | 6 +- .../src/KintoneApiClient.ts | 36 +++- packages/customize-uploader/src/constants.ts | 3 + packages/customize-uploader/src/import.ts | 203 ++++++++++++++++++ packages/customize-uploader/src/messages.ts | 13 ++ .../test/MockKintoneApiClient.ts | 39 +++- .../fixtures/get-appcustomize-response.json | 93 ++++++++ .../customize-uploader/test/import-test.ts | 191 ++++++++++++++++ .../customize-uploader/test/index-test.ts | 2 +- packages/customize-uploader/test/util-test.ts | 7 +- 12 files changed, 682 insertions(+), 68 deletions(-) create mode 100644 packages/customize-uploader/src/constants.ts create mode 100644 packages/customize-uploader/src/import.ts create mode 100644 packages/customize-uploader/test/fixtures/get-appcustomize-response.json create mode 100644 packages/customize-uploader/test/import-test.ts diff --git a/packages/customize-uploader/bin/cli.js b/packages/customize-uploader/bin/cli.js index 5c97680269..8808e29118 100755 --- a/packages/customize-uploader/bin/cli.js +++ b/packages/customize-uploader/bin/cli.js @@ -3,6 +3,7 @@ const osLocale = require('os-locale'); const meow = require('meow'); const { run } = require('../dist/src/index'); +const { runImport } = require('../dist/src/import'); const { inquireParams } = require('../dist/src/params'); const { getDefaultLang } = require('../dist/src/lang'); const { getMessage } = require('../dist/src/messages'); @@ -16,7 +17,6 @@ const { KINTONE_BASIC_AUTH_USERNAME, KINTONE_BASIC_AUTH_PASSWORD } = process.env; - const cli = meow( ` Usage @@ -29,8 +29,16 @@ const cli = meow( --basic-auth-password Basic Authentication password --proxy Proxy server --watch Watch the changes of customize files and re-run + --dest-dir -d option for subcommand import. + this option stands for output directory + default value is dest/ --lang Using language (en or ja) --guest-space-id Guest space ID for uploading files + + SubCommands + import generate customize-manifest.json and + download js/css files from existing app customization + You can set the values through environment variables domain: KINTONE_DOMAIN username: KINTONE_USERNAME @@ -77,11 +85,23 @@ const cli = meow( type: 'number', default: 0 }, + // Optional option for import subcommand + destDir: { + type: 'string', + default: 'dest', + alias: 'd' + } } } ); -const manifestFile = cli.input[0]; +const subCommands = ["import"]; +const hasSubCommand = subCommands.indexOf(cli.input[0]) >= 0; +const subCommand = hasSubCommand ? cli.input[0] : null; +const isImportCommand = subCommand === "import"; + +const manifestFile = hasSubCommand ? cli.input[1] : cli.input[0]; + const { username, password, @@ -91,7 +111,8 @@ const { proxy, watch, lang, - guestSpaceId + guestSpaceId, + destDir, } = cli.flags; const options = proxy ? { watch, lang, proxy } : { watch, lang }; @@ -99,6 +120,10 @@ if (guestSpaceId) { options.guestSpaceId = guestSpaceId; } +if(isImportCommand) { + options.destDir = destDir; +} + if (!manifestFile) { console.error(getMessage(lang, 'E_requiredManifestFile')); cli.showHelp(); @@ -106,8 +131,12 @@ if (!manifestFile) { } inquireParams({ username, password, domain, lang }) - .then(({ username, password, domain }) => ( - run(domain, username, password, basicAuthUsername, basicAuthPassword, manifestFile, options) - )) + .then(({ username, password, domain }) => { + if(isImportCommand) { + runImport(domain, username, password, basicAuthUsername, basicAuthPassword, manifestFile, options) + } else { + run(domain, username, password, basicAuthUsername, basicAuthPassword, manifestFile, options) + } + }) .catch(error => console.log(error.message)); - ; + diff --git a/packages/customize-uploader/package-lock.json b/packages/customize-uploader/package-lock.json index 0bd75ddf9c..b53d3a9033 100644 --- a/packages/customize-uploader/package-lock.json +++ b/packages/customize-uploader/package-lock.json @@ -29,8 +29,7 @@ "@types/events": { "version": "1.2.0", "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", - "dev": true + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" }, "@types/form-data": { "version": "2.2.1", @@ -41,6 +40,16 @@ "@types/node": "*" } }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, "@types/inquirer": { "version": "0.0.43", "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-0.0.43.tgz", @@ -51,6 +60,28 @@ "@types/through": "*" } }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + }, + "@types/mkdirp": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.2.tgz", + "integrity": "sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==", + "dev": true, + "requires": { + "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "10.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.7.tgz", + "integrity": "sha512-Zh5Z4kACfbeE8aAOYh9mqotRxaZMro8MbBQtR8vEXOMiZo2rGEh2LayJijKdlu48YnS6y2EFU/oo2NCe5P6jGw==", + "dev": true + } + } + }, "@types/mocha": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", @@ -85,6 +116,15 @@ "@types/request": "*" } }, + "@types/rimraf": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-2.0.2.tgz", + "integrity": "sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ==", + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, "@types/rx": { "version": "4.1.1", "resolved": "http://registry.npmjs.org/@types/rx/-/rx-4.1.1.tgz", @@ -445,8 +485,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -520,7 +559,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -783,8 +821,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -1763,8 +1800,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.4", @@ -1783,8 +1819,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -1802,13 +1837,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1821,18 +1854,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -1935,8 +1965,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -1946,7 +1975,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1959,20 +1987,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.2.4", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -1989,7 +2014,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2062,8 +2086,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -2073,7 +2096,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2149,8 +2171,7 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -2180,7 +2201,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2198,7 +2218,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2237,13 +2256,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.2", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -2570,7 +2587,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2748,7 +2764,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -3313,7 +3328,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3321,8 +3335,7 @@ "minimist": { "version": "0.0.8", "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minimist-options": { "version": "3.0.2", @@ -3356,7 +3369,6 @@ "version": "0.5.1", "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -3566,7 +3578,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -3944,6 +3955,14 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "^7.0.5" + } + }, "run-async": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", @@ -4809,8 +4828,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "xtend": { "version": "4.0.1", diff --git a/packages/customize-uploader/package.json b/packages/customize-uploader/package.json index 21924b0b1f..4e24b69b4c 100644 --- a/packages/customize-uploader/package.json +++ b/packages/customize-uploader/package.json @@ -42,6 +42,7 @@ "@types/mocha": "^5.2.5", "@types/node": "^10.12.11", "@types/request-promise": "^4.1.42", + "@types/mkdirp": "^0.5.2", "mocha": "^5.2.0", "npm-run-all": "^4.1.5", "prettier": "^1.15.3", @@ -52,11 +53,14 @@ "typescript": "^3.1.6" }, "dependencies": { + "@types/rimraf": "^2.0.2", "chokidar": "^2.0.4", "inquirer": "^6.2.1", "meow": "^5.0.0", + "mkdirp": "^0.5.1", "os-locale": "^3.0.1", "request": "^2.88.0", - "request-promise": "^4.2.2" + "request-promise": "^4.2.2", + "rimraf": "^2.6.2" } } diff --git a/packages/customize-uploader/src/KintoneApiClient.ts b/packages/customize-uploader/src/KintoneApiClient.ts index 871be7757d..c15e11c57a 100755 --- a/packages/customize-uploader/src/KintoneApiClient.ts +++ b/packages/customize-uploader/src/KintoneApiClient.ts @@ -12,6 +12,7 @@ interface RequestOption { formData?: object; proxy?: string; tunnel?: boolean; + resolveWithFullResponse: boolean; } export interface Option { @@ -119,11 +120,38 @@ export default class KintoneApiClient { deployed = true; } + public async downloadFile(fileKey: string) { + return this.sendRequest({ + method: "GET", + path: "/k/v1/file.json", + body: { fileKey } + }); + } + + public async getAppCustomize(appId: string) { + return this.sendRequest({ + method: "GET", + path: "/k/v1/app/customize.json", + body: { app: appId } + }); + } + public async sendRequest(params: RequestParams) { const requestOptions = this.buildRequestOptions(params); try { - const resp = await request(requestOptions); - return JSON.parse(resp); + return request(requestOptions).then(response => { + if (response.statusCode !== 200) { + throw new Error( + `Failed to Request(StatusCode:${response.statusCode}` + ); + } + const contentType = response.headers["content-type"]; + if (contentType && contentType.startsWith("application/json")) { + return JSON.parse(response.body); + } else { + return response.body; + } + }); } catch (e) { if (e.statusCode === 520) { const responseBody = JSON.parse(e.response.body); @@ -154,7 +182,8 @@ export default class KintoneApiClient { headers: { "X-Cybozu-Authorization": this.auth, "Content-Type": contentType || "application/json" - } + }, + resolveWithFullResponse: true }, isFormData ? { formData: body, body: null } @@ -167,6 +196,7 @@ export default class KintoneApiClient { requestOptions.proxy = this.options.proxy; requestOptions.tunnel = true; } + requestOptions.resolveWithFullResponse = true; return requestOptions; } diff --git a/packages/customize-uploader/src/constants.ts b/packages/customize-uploader/src/constants.ts new file mode 100644 index 0000000000..8c298a96ad --- /dev/null +++ b/packages/customize-uploader/src/constants.ts @@ -0,0 +1,3 @@ +export const Constans = { + MAX_RETRY_COUNT: 3 +}; diff --git a/packages/customize-uploader/src/import.ts b/packages/customize-uploader/src/import.ts new file mode 100644 index 0000000000..381c831490 --- /dev/null +++ b/packages/customize-uploader/src/import.ts @@ -0,0 +1,203 @@ +import fs from "fs"; +import mkdirp from "mkdirp"; +import { sep } from "path"; +import { Constans } from "./constants"; +import { CustomizeManifest } from "./index"; +import KintoneApiClient, { AuthenticationError } from "./KintoneApiClient"; +import { Lang } from "./lang"; +import { getBoundMessage } from "./messages"; +import { wait } from "./util"; + +export interface Option { + lang: Lang; + proxy: string; + guestSpaceId: number; + destDir: string; +} + +export interface ImportCustomizeManifest { + app: string; +} + +interface UploadedFile { + type: "FILE"; + file: { + fileKey: string; + name: string; + }; +} + +interface CDNFile { + type: "URL"; + url: string; +} +type CustomizeFile = UploadedFile | CDNFile; + +interface GetAppCustomizeResp { + scope: "ALL" | "ADMIN" | "NONE"; + desktop: { + js: CustomizeFile[]; + css: CustomizeFile[]; + }; + mobile: { + js: CustomizeFile[]; + }; +} + +export async function importCustomizeSetting( + kintoneApiClient: KintoneApiClient, + manifest: ImportCustomizeManifest, + status: { + retryCount: number; + }, + options: Option +): Promise { + const m = getBoundMessage(options.lang); + const appId = manifest.app; + let { retryCount } = status; + + try { + const appCustomize = kintoneApiClient.getAppCustomize(appId); + return appCustomize + .then((resp: GetAppCustomizeResp) => { + console.log(m("M_GenerateManifestFile")); + return exportAsManifestFile(appId, options.destDir, resp); + }) + .then((resp: GetAppCustomizeResp) => { + console.log(m("M_DownloadUploadedFile")); + return downloadCustomizeFiles( + kintoneApiClient, + appId, + options.destDir, + resp + ); + }); + } catch (e) { + const isAuthenticationError = e instanceof AuthenticationError; + retryCount++; + if (isAuthenticationError) { + throw new Error(m("E_Authentication")); + } else if (retryCount < Constans.MAX_RETRY_COUNT) { + await wait(1000); + console.log(m("E_Retry")); + await importCustomizeSetting( + kintoneApiClient, + manifest, + { retryCount }, + options + ); + } else { + throw e; + } + } +} + +function exportAsManifestFile( + appId: string, + destRootDir: string, + resp: GetAppCustomizeResp +): GetAppCustomizeResp { + const toNameOrUrl = (destDir: string) => (f: CustomizeFile) => { + if (f.type === "FILE") { + return `${destDir}/${f.file.name}`; + } else { + return f.url; + } + }; + + const desktopJs: CustomizeFile[] = resp.desktop.js; + const desktopCss: CustomizeFile[] = resp.desktop.css; + const mobileJs: CustomizeFile[] = resp.mobile.js; + + const customizeJson: CustomizeManifest = { + app: appId, + scope: resp.scope, + desktop: { + js: desktopJs.map(toNameOrUrl(`${destRootDir}/desktop/js`)), + css: desktopCss.map(toNameOrUrl(`${destRootDir}/desktop/css`)) + }, + mobile: { + js: mobileJs.map(toNameOrUrl(`${destRootDir}/mobile/js`)) + } + }; + + if (!fs.existsSync(`${destRootDir}`)) { + mkdirp.sync(`${destRootDir}`); + } + fs.writeFileSync( + `${destRootDir}/customize-manifest.json`, + JSON.stringify(customizeJson, null, 4) + ); + return resp; +} + +async function downloadCustomizeFiles( + kintoneApiClient: KintoneApiClient, + appId: string, + destDir: string, + { desktop, mobile }: GetAppCustomizeResp +): Promise { + const desktopJs: CustomizeFile[] = desktop.js; + const desktopCss: CustomizeFile[] = desktop.css; + const mobileJs: CustomizeFile[] = mobile.js; + + [ + `${destDir}${sep}desktop${sep}js${sep}`, + `${destDir}${sep}desktop${sep}css${sep}`, + `${destDir}${sep}mobile${sep}js${sep}` + ].forEach(path => mkdirp.sync(path)); + + const desktopJsPromise = desktopJs.map( + downloadAndWriteFile(kintoneApiClient, `${destDir}${sep}desktop${sep}js`) + ); + const desktopCssPromise = desktopCss.map( + downloadAndWriteFile(kintoneApiClient, `${destDir}${sep}desktop${sep}css`) + ); + const mobileJsPromise = mobileJs.map( + downloadAndWriteFile(kintoneApiClient, `${destDir}${sep}mobile${sep}js`) + ); + return [...desktopJsPromise, ...desktopCssPromise, ...mobileJsPromise]; +} + +function downloadAndWriteFile( + kintoneApiClient: KintoneApiClient, + destDir: string +): (f: CustomizeFile) => void { + return async f => { + if (f.type === "URL") { + return; + } + return kintoneApiClient + .downloadFile(f.file.fileKey) + .then(resp => fs.writeFileSync(`${destDir}${sep}${f.file.name}`, resp)); + }; +} + +export const runImport = async ( + domain: string, + username: string, + password: string, + basicAuthUsername: string | null, + basicAuthPassword: string | null, + manifestFile: string, + options: Option +): Promise => { + const m = getBoundMessage(options.lang); + const manifest: ImportCustomizeManifest = JSON.parse( + fs.readFileSync(manifestFile, "utf8") + ); + const status = { + retryCount: 0 + }; + + const kintoneApiClient = new KintoneApiClient( + username, + password, + basicAuthUsername, + basicAuthPassword, + domain, + options + ); + await importCustomizeSetting(kintoneApiClient, manifest, status, options); + console.log(m("M_CommandImportFinish")); +}; diff --git a/packages/customize-uploader/src/messages.ts b/packages/customize-uploader/src/messages.ts index 8ac5dcf0ba..d589a1850c 100755 --- a/packages/customize-uploader/src/messages.ts +++ b/packages/customize-uploader/src/messages.ts @@ -40,6 +40,19 @@ const messages = { en: "Customize setting has been updated!", ja: "JavaScript/CSS カスタマイズの設定を変更しました!" }, + M_GenerateManifestFile: { + en: "Generate customize-manifest.json from kintone app customize", + ja: "kintoneのアプリから customize-manifest.jsonを生成しています" + }, + M_DownloadUploadedFile: { + en: "Download Uploaded files on kintone app customize", + ja: + "kintoneのアプリからカスタマイズ設定されたファイルをダウンロードしています" + }, + M_CommandImportFinish: { + en: "Fisnish importing from kintone app customize", + ja: "kintoneのアプリカスタマイズからインポートが完了しました" + }, E_Updated: { en: "Failed to update customize setting", ja: "JavaScript/CSS カスタマイズの設定の変更に失敗しました" diff --git a/packages/customize-uploader/test/MockKintoneApiClient.ts b/packages/customize-uploader/test/MockKintoneApiClient.ts index 4f9f0826ee..49f704c670 100644 --- a/packages/customize-uploader/test/MockKintoneApiClient.ts +++ b/packages/customize-uploader/test/MockKintoneApiClient.ts @@ -5,19 +5,48 @@ import KintoneApiClient, { export default class MockKintoneApiClient extends KintoneApiClient { public logs: RequestParams[]; + public willBeReturnResponse: any; constructor( ...args: [string, string, string, string, string, ApiClientOption] ) { super(...args); this.logs = []; + this.willBeReturnResponse = {}; + const appDeployResp = { + apps: [{ status: "SUCCESS" }] + }; + this.willBeReturn("/k/v1/preview/app/deploy.json", "GET", appDeployResp) + .willBeReturn("/k/v1/preview/app/deploy.json", "POST", appDeployResp) + .willBeReturn("/k/v1/preview/app/customize.json", "PUT", {}); } + + public willBeReturn( + path: string, + method: string, + willBeReturn: string | object + ) { + let byPath: any = this.willBeReturnResponse[path]; + if (!byPath) { + byPath = {}; + } + byPath[method] = willBeReturn; + this.willBeReturnResponse[path] = byPath; + return this; + } + public async sendRequest(params: RequestParams) { this.logs.push(params); - switch (params.path) { - case "/k/v1/file.json": - return { fileKey: `key--${params.path}` }; - case "/k/v1/preview/app/deploy.json": - return { apps: [{ status: "SUCCESS" }] }; + const method = params.method; + if (method === "POST" && params.path === "/k/v1/file.json") { + return { fileKey: `key--${params.path}` }; + } + const byPath = this.willBeReturnResponse[params.path]; + if (!byPath || !byPath[params.method]) { + console.info( + `not mocked request: [${params.method}] ${params.path} returns {}` + ); + return {}; } + return byPath[params.method]; } } diff --git a/packages/customize-uploader/test/fixtures/get-appcustomize-response.json b/packages/customize-uploader/test/fixtures/get-appcustomize-response.json new file mode 100644 index 0000000000..711af0be36 --- /dev/null +++ b/packages/customize-uploader/test/fixtures/get-appcustomize-response.json @@ -0,0 +1,93 @@ +{ + "scope": "ALL", + "desktop": { + "js": [ + { + "type": "URL", + "url": "https://js.cybozu.com/vuejs/v2.5.17/vue.min.js" + }, + { + "type": "URL", + "url": "https://js.cybozu.com/lodash/4.17.11/lodash.min.js" + }, + { + "type": "FILE", + "file": { + "fileKey": "20181116095653774F24C632AF46E69BDC8F5EF04C8E24014", + "name": "bootstrap.min.js", + "contentType": "text/javascript", + "size": "51039" + } + }, + { + "type": "FILE", + "file": { + "fileKey": "20181116095653FF8D40EE4B364FD89A2B3A382EDCB259286", + "name": "a.js", + "contentType": "text/javascript", + "size": "33" + } + } + ], + "css": [ + { + "type": "FILE", + "file": { + "fileKey": "201811160956531AFF9246D7CB40938A91EAC14A0622C9250", + "name": "bootstrap.min.css", + "contentType": "text/css", + "size": "140936" + } + }, + { + "type": "FILE", + "file": { + "fileKey": "201811160956531DCC5DA8C6E0480C8F3BD8A92EEFF584123", + "name": "bootstrap-reboot.min.css", + "contentType": "text/css", + "size": "4019" + } + }, + { + "type": "FILE", + "file": { + "fileKey": "20181116095653A6A52415705D403A9B1AB36E1448B32E191", + "name": "bootstrap-grid.min.css", + "contentType": "text/css", + "size": "28977" + } + } + ] + }, + "mobile": { + "js": [ + { + "type": "URL", + "url": "https://js.cybozu.com/jquery/3.3.1/jquery.min.js" + }, + { + "type": "URL", + "url": "https://js.cybozu.com/jqueryui/1.12.1/jquery-ui.min.js" + }, + { + "type": "FILE", + "file": { + "fileKey": "20181116095653C06A1FE34CFD43D6B21BE7F55D3B6ECB031", + "name": "bootstrap.js", + "contentType": "text/javascript", + "size": "123765" + } + }, + { + "type": "FILE", + "file": { + "fileKey": "201811160956535E4F00740689488C9ABE7DCF3E794B34315", + "name": "b.js", + "contentType": "text/javascript", + "size": "33" + } + } + ] + }, + "revision": "5" +} diff --git a/packages/customize-uploader/test/import-test.ts b/packages/customize-uploader/test/import-test.ts new file mode 100644 index 0000000000..e3d8a874c5 --- /dev/null +++ b/packages/customize-uploader/test/import-test.ts @@ -0,0 +1,191 @@ +import assert from "assert"; +import * as fs from "fs"; +import { sep } from "path"; +import rimraf from "rimraf"; +import { + ImportCustomizeManifest, + importCustomizeSetting, + Option +} from "../src/import"; +import MockKintoneApiClient from "./MockKintoneApiClient"; + +describe("import", () => { + const testDestDir = "testDestDir"; + const filesToTestContent = [ + `${testDestDir}/desktop/js/bootstrap.min.js`, + `${testDestDir}/desktop/js/a.js`, + `${testDestDir}/desktop/css/bootstrap.min.css`, + `${testDestDir}/desktop/css/bootstrap-reboot.min.css`, + `${testDestDir}/desktop/css/bootstrap-grid.min.css`, + `${testDestDir}/mobile/js/bootstrap.js`, + `${testDestDir}/mobile/js/b.js` + ]; + + const uploadFileBody = `(function() { console.log("hello"); })();`; + + describe("runImport", () => { + let kintoneApiClient: MockKintoneApiClient; + let manifest: ImportCustomizeManifest; + let status: { retryCount: number }; + let options: Option; + beforeEach(() => { + kintoneApiClient = new MockKintoneApiClient( + "kintone", + "hogehoge", + "basicAuthUser", + "basicAuthPass", + "example.com", + { + proxy: "", + guestSpaceId: 0 + } + ); + manifest = { + app: "1" + }; + status = { + retryCount: 0 + }; + options = { + lang: "en", + proxy: "", + guestSpaceId: 0, + destDir: testDestDir + }; + }); + + afterEach(() => { + rimraf.sync(`${testDestDir}`); + }); + + const assertManifestContent = (buffer: Buffer) => { + const appCustomize = { + app: "1", + scope: "ALL", + desktop: { + js: [ + "https://js.cybozu.com/vuejs/v2.5.17/vue.min.js", + "https://js.cybozu.com/lodash/4.17.11/lodash.min.js", + `${testDestDir}/desktop/js/bootstrap.min.js`, + `${testDestDir}/desktop/js/a.js` + ], + css: [ + `${testDestDir}/desktop/css/bootstrap.min.css`, + `${testDestDir}/desktop/css/bootstrap-reboot.min.css`, + `${testDestDir}/desktop/css/bootstrap-grid.min.css` + ] + }, + mobile: { + js: [ + "https://js.cybozu.com/jquery/3.3.1/jquery.min.js", + "https://js.cybozu.com/jqueryui/1.12.1/jquery-ui.min.js", + `${testDestDir}/mobile/js/bootstrap.js`, + `${testDestDir}/mobile/js/b.js` + ] + } + }; + assert.deepStrictEqual(JSON.parse(buffer.toString()), appCustomize); + }; + + const assertDownloadFile = (path: string) => { + assert.ok(fs.existsSync(path), `test ${path} is exists`); + const content = fs.readFileSync(path); + assert.strictEqual(uploadFileBody, content.toString()); + }; + + const assertImportUseCaseApiRequest = ( + mockKintoneApiClient: MockKintoneApiClient + ) => { + const expected: any[] = [ + { + body: { + app: "1" + }, + method: "GET", + path: "/k/v1/app/customize.json" + }, + { + body: { + fileKey: "20181116095653774F24C632AF46E69BDC8F5EF04C8E24014" + }, + method: "GET", + path: "/k/v1/file.json" + }, + { + body: { + fileKey: "20181116095653FF8D40EE4B364FD89A2B3A382EDCB259286" + }, + method: "GET", + path: "/k/v1/file.json" + }, + { + body: { + fileKey: "201811160956531AFF9246D7CB40938A91EAC14A0622C9250" + }, + method: "GET", + path: "/k/v1/file.json" + }, + { + body: { + fileKey: "201811160956531DCC5DA8C6E0480C8F3BD8A92EEFF584123" + }, + method: "GET", + path: "/k/v1/file.json" + }, + { + body: { + fileKey: "20181116095653A6A52415705D403A9B1AB36E1448B32E191" + }, + method: "GET", + path: "/k/v1/file.json" + }, + { + body: { + fileKey: "20181116095653C06A1FE34CFD43D6B21BE7F55D3B6ECB031" + }, + method: "GET", + path: "/k/v1/file.json" + }, + { + body: { + fileKey: "201811160956535E4F00740689488C9ABE7DCF3E794B34315" + }, + method: "GET", + path: "/k/v1/file.json" + } + ]; + assert.deepStrictEqual(mockKintoneApiClient.logs, expected); + }; + + it("should success generate customize-manifest.json and download uploaded js/css files", () => { + const getAppCustomizeResponse = JSON.parse( + fs + .readFileSync("test/fixtures/get-appcustomize-response.json") + .toString() + ); + + kintoneApiClient.willBeReturn("/k/v1/file.json", "GET", uploadFileBody); + kintoneApiClient.willBeReturn( + "/k/v1/app/customize.json", + "GET", + getAppCustomizeResponse + ); + + const execCommand = importCustomizeSetting( + kintoneApiClient, + manifest, + status, + options + ); + + return execCommand.then(() => { + assertImportUseCaseApiRequest(kintoneApiClient); + const manifestFile = `${testDestDir}/customize-manifest.json`; + assert.ok(fs.existsSync(manifestFile), `test ${manifestFile} exists`); + const contents = fs.readFileSync(manifestFile); + assertManifestContent(contents); + filesToTestContent.map(assertDownloadFile); + }); + }); + }); +}); diff --git a/packages/customize-uploader/test/index-test.ts b/packages/customize-uploader/test/index-test.ts index 7e95088aa6..3ea58a48b2 100644 --- a/packages/customize-uploader/test/index-test.ts +++ b/packages/customize-uploader/test/index-test.ts @@ -46,7 +46,7 @@ describe("index", () => { guestSpaceId: 0 }; }); - it("shoule succeed the uploading", async () => { + it("should succeed the uploading", async () => { try { await upload(kintoneApiClient, manifest, status, options); assert.ok(true, "the upload has been successful"); diff --git a/packages/customize-uploader/test/util-test.ts b/packages/customize-uploader/test/util-test.ts index f7f690092a..02a4154642 100644 --- a/packages/customize-uploader/test/util-test.ts +++ b/packages/customize-uploader/test/util-test.ts @@ -12,10 +12,11 @@ describe("util", () => { }); describe("wait", () => { - it("should wait the specific ms", async () => { + it("should wait the specific ms", () => { const start = Date.now(); - await wait(50); - assert(Date.now() >= start + 50); + return wait(50).then(() => { + assert(Date.now() >= start + 50); + }); }); }); });