diff --git a/packages/cli/index.d.ts b/packages/cli/index.d.ts new file mode 100644 index 000000000000..1b9698add96d --- /dev/null +++ b/packages/cli/index.d.ts @@ -0,0 +1,12 @@ +declare namespace NodeJS { + interface Global { + __dirname: string + } +} + +declare module 'pascalcase' { + function pascalcase(input: string): string + export default pascalcase +} + +declare module 'listr-verbose-renderer' diff --git a/packages/cli/package.json b/packages/cli/package.json index c289fc00fa56..c4b294218253 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -35,7 +35,11 @@ "yargs": "^15.3.1" }, "devDependencies": { + "@types/listr": "^0.14.2", + "@types/lodash": "^4.14.151", "@types/node-fetch": "^2.5.5", + "@types/pluralize": "^0.0.29", + "@types/yargs": "^15.0.5", "rimraf": "^3.0.2" }, "scripts": { diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.js index f4fd61ab207d..9ca8c478e03f 100644 --- a/packages/cli/src/commands/build.js +++ b/packages/cli/src/commands/build.js @@ -1,9 +1,9 @@ import execa from 'execa' import Listr from 'listr' import VerboseRenderer from 'listr-verbose-renderer' - import { getPaths } from 'src/lib' import c from 'src/lib/colors' + import { handler as generatePrismaClient } from 'src/commands/dbCommands/generate' export const command = 'build [app..]' diff --git a/packages/cli/src/commands/dbCommands/up.js b/packages/cli/src/commands/dbCommands/up.js index a5d2a5997255..ab73425e8ca4 100644 --- a/packages/cli/src/commands/dbCommands/up.js +++ b/packages/cli/src/commands/dbCommands/up.js @@ -1,4 +1,5 @@ import { runCommandTask } from 'src/lib' + import { handler as generatePrismaClient } from 'src/commands/dbCommands/generate' export const command = 'up' diff --git a/packages/cli/src/commands/dev.js b/packages/cli/src/commands/dev.js index f639b73c617c..be02ac726367 100644 --- a/packages/cli/src/commands/dev.js +++ b/packages/cli/src/commands/dev.js @@ -2,7 +2,6 @@ import fs from 'fs' import path from 'path' import concurrently from 'concurrently' - import { getPaths } from 'src/lib' import c from 'src/lib/colors' diff --git a/packages/cli/src/commands/generate/__tests__/helpers.test.js b/packages/cli/src/commands/generate/__tests__/helpers.test.ts similarity index 100% rename from packages/cli/src/commands/generate/__tests__/helpers.test.js rename to packages/cli/src/commands/generate/__tests__/helpers.test.ts diff --git a/packages/cli/src/commands/generate/auth/auth.js b/packages/cli/src/commands/generate/auth/auth.js index a67eb6a4845f..439a4a682a27 100644 --- a/packages/cli/src/commands/generate/auth/auth.js +++ b/packages/cli/src/commands/generate/auth/auth.js @@ -3,7 +3,6 @@ import path from 'path' import execa from 'execa' import Listr from 'listr' - import { getPaths, writeFilesTask } from 'src/lib' import c from 'src/lib/colors' diff --git a/packages/cli/src/commands/generate/function/function.js b/packages/cli/src/commands/generate/function/function.js index 434b024f46b1..32457de5167c 100644 --- a/packages/cli/src/commands/generate/function/function.js +++ b/packages/cli/src/commands/generate/function/function.js @@ -1,7 +1,6 @@ import path from 'path' import camelcase from 'camelcase' - import { getPaths } from 'src/lib' import { diff --git a/packages/cli/src/commands/generate/helpers.js b/packages/cli/src/commands/generate/helpers.ts similarity index 75% rename from packages/cli/src/commands/generate/helpers.js rename to packages/cli/src/commands/generate/helpers.ts index 7ac71a1a0daf..f65779a545e1 100644 --- a/packages/cli/src/commands/generate/helpers.js +++ b/packages/cli/src/commands/generate/helpers.ts @@ -5,9 +5,10 @@ import pluralize from 'pluralize' import Listr from 'listr' import pascalcase from 'pascalcase' import { paramCase } from 'param-case' - +import type { CommandModule } from 'yargs' import { generateTemplate, getPaths, writeFilesTask } from 'src/lib' import c from 'src/lib/colors' +import type { Paths } from '@redwoodjs/internal' /** * Reduces boilerplate for generating an output path and content to write to disk @@ -26,6 +27,17 @@ export const templateForComponentFile = ({ templateVars, componentName, outputPath, +}: { + name: string + suffix?: string + extension?: string + webPathSection?: keyof Paths['web'] + apiPathSection?: keyof Paths['api'] + generator: string + templatePath: string + templateVars?: {} + componentName?: string + outputPath?: string }) => { const basePath = webPathSection ? getPaths().web[webPathSection] @@ -50,7 +62,7 @@ export const templateForComponentFile = ({ * Creates a route path, either returning the existing path if passed, otherwise * creates one based on the name */ -export const pathName = (path, name) => { +export const pathName = (path: string | null, name: string): string => { return path ?? `/${paramCase(name)}` } @@ -62,10 +74,13 @@ export const pathName = (path, name) => { export const createYargsForComponentGeneration = ({ componentName, filesFn, -}) => { +}: { + componentName: 'cell' | 'component' | 'function' | 'layout' | 'service' + filesFn: Function +}): CommandModule => { return { command: `${componentName} `, - desc: `Generate a ${componentName} component.`, + describe: `Generate a ${componentName} component.`, builder: { force: { type: 'boolean', default: false } }, handler: async ({ force, ...rest }) => { const tasks = new Listr( @@ -91,7 +106,8 @@ export const createYargsForComponentGeneration = ({ } // Returns all relations to other models -export const relationsForModel = (model) => { +// TODO align with prisma type once src/lib is typed +export const relationsForModel = (model: { fields: any[] }) => { return model.fields .filter((f) => f.relationName) .map((field) => { @@ -100,8 +116,11 @@ export const relationsForModel = (model) => { }) } -// Returns only relations that are of datatype Int -export const intForeignKeysForModel = (model) => { +/** + * Returns only relations that are of datatype Int + * */ +// TODO align with prisma types once src/lib is typed +export const intForeignKeysForModel = (model: { fields: any[] }) => { return model.fields .filter((f) => f.name.match(/Id$/) && f.type === 'Int') .map((f) => f.name) diff --git a/packages/cli/src/commands/generate/page/page.js b/packages/cli/src/commands/generate/page/page.js index 17332ee15ac2..de5605dec56f 100644 --- a/packages/cli/src/commands/generate/page/page.js +++ b/packages/cli/src/commands/generate/page/page.js @@ -1,7 +1,6 @@ import Listr from 'listr' import camelcase from 'camelcase' import pascalcase from 'pascalcase' - import { writeFilesTask, addRoutesToRouterTask } from 'src/lib' import c from 'src/lib/colors' diff --git a/packages/cli/src/commands/generate/scaffold/scaffold.js b/packages/cli/src/commands/generate/scaffold/scaffold.js index 773b8253285c..78b6f28d014b 100644 --- a/packages/cli/src/commands/generate/scaffold/scaffold.js +++ b/packages/cli/src/commands/generate/scaffold/scaffold.js @@ -7,7 +7,6 @@ import pascalcase from 'pascalcase' import pluralize from 'pluralize' import { paramCase } from 'param-case' import humanize from 'humanize-string' - import { generateTemplate, templateRoot, diff --git a/packages/cli/src/commands/generate/sdl/sdl.js b/packages/cli/src/commands/generate/sdl/sdl.js index 466eb7b8dc9b..61d510e8ce6b 100644 --- a/packages/cli/src/commands/generate/sdl/sdl.js +++ b/packages/cli/src/commands/generate/sdl/sdl.js @@ -4,7 +4,6 @@ import Listr from 'listr' import camelcase from 'camelcase' import pascalcase from 'pascalcase' import pluralize from 'pluralize' - import { generateTemplate, getSchema, diff --git a/packages/cli/src/commands/lint.js b/packages/cli/src/commands/lint.js index e79423d5bcf6..26daef5a9572 100644 --- a/packages/cli/src/commands/lint.js +++ b/packages/cli/src/commands/lint.js @@ -1,5 +1,4 @@ import execa from 'execa' - import { getPaths } from 'src/lib' export const command = 'lint' diff --git a/packages/cli/src/commands/test.js b/packages/cli/src/commands/test.js index 19bd46a81395..0f5756fb3ab2 100644 --- a/packages/cli/src/commands/test.js +++ b/packages/cli/src/commands/test.js @@ -1,7 +1,6 @@ import execa from 'execa' import Listr from 'listr' import VerboseRenderer from 'listr-verbose-renderer' - import { getPaths } from 'src/lib' import c from 'src/lib/colors' diff --git a/packages/cli/src/commands/upgrade.js b/packages/cli/src/commands/upgrade.js index 226450c70b81..c424189f66a0 100644 --- a/packages/cli/src/commands/upgrade.js +++ b/packages/cli/src/commands/upgrade.js @@ -1,6 +1,5 @@ import execa from 'execa' import Listr from 'listr' - import c from 'src/lib/colors' export const command = 'upgrade' diff --git a/packages/cli/src/lib/__tests__/fixtures/code.js b/packages/cli/src/lib/__tests__/fixtures/code.js deleted file mode 100644 index 7631e7768c66..000000000000 --- a/packages/cli/src/lib/__tests__/fixtures/code.js +++ /dev/null @@ -1,2 +0,0 @@ -const line1 = "The quick brown ${pluralCamelName} jumps over the lazy ${foo}."; -const line2 = 'Sphinx of black quartz, judge my vow.' diff --git a/packages/cli/src/lib/__tests__/fixtures/code.ts b/packages/cli/src/lib/__tests__/fixtures/code.ts new file mode 100644 index 000000000000..d81e3f824473 --- /dev/null +++ b/packages/cli/src/lib/__tests__/fixtures/code.ts @@ -0,0 +1,2 @@ +const line1 = 'The quick brown ${pluralCamelName} jumps over the lazy ${foo}.' +const line2 = 'Sphinx of black quartz, judge my vow.' diff --git a/packages/cli/src/lib/__tests__/index.test.js b/packages/cli/src/lib/__tests__/index.test.ts similarity index 99% rename from packages/cli/src/lib/__tests__/index.test.js rename to packages/cli/src/lib/__tests__/index.test.ts index fcd21c31bf61..14c36db94e0c 100644 --- a/packages/cli/src/lib/__tests__/index.test.js +++ b/packages/cli/src/lib/__tests__/index.test.ts @@ -93,7 +93,7 @@ test('generateTemplate returns a lodash-templated string', () => { // Be careful when editing the code.js fixture as the prettifier.config.js will cause it to get // prettified and then it already match the expected output, with no changes test('generateTemplate returns prettified JS code', () => { - const output = index.generateTemplate(path.join('fixtures', 'code.js'), { + const output = index.generateTemplate(path.join('fixtures', 'code.ts'), { root: __dirname, name: 'fox', foo: 'dog', diff --git a/packages/cli/src/lib/colors.js b/packages/cli/src/lib/colors.ts similarity index 100% rename from packages/cli/src/lib/colors.js rename to packages/cli/src/lib/colors.ts diff --git a/packages/cli/src/lib/index.js b/packages/cli/src/lib/index.ts similarity index 82% rename from packages/cli/src/lib/index.js rename to packages/cli/src/lib/index.ts index 474403584add..8c0c70e2d101 100644 --- a/packages/cli/src/lib/index.js +++ b/packages/cli/src/lib/index.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' -import lodash from 'lodash/string' +import lodash from 'lodash' import camelcase from 'camelcase' import pascalcase from 'pascalcase' import pluralize from 'pluralize' @@ -16,18 +16,57 @@ import { format } from 'prettier' import c from './colors' -export const asyncForEach = async (array, callback) => { +export const asyncForEach = async (array: any[], callback: Function) => { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array) } } +export const readFile = (target: Parameters[0]) => + fs.readFileSync(target) + +/** + * This wraps the core version of getPaths into something that catches the exception + * and displays a helpful error message. + */ +export const getPaths = () => { + try { + return getRedwoodPaths() + } catch (e) { + console.error(c.error(e.message)) + process.exit(0) + } +} + +/* + * Returns the DMMF defined by `prisma` resolving the relevant `shema.prisma` path. + */ +export const getSchemaDefinitions = async () => { + const schemaPath = path.join(getPaths().api.db, 'schema.prisma') + const metadata = await getDMMF({ + datamodel: readFile(schemaPath).toString(), + }) + + return metadata +} + +/** + * This returns the config present in `prettier.config.js` of a Redwood project. + */ +export const prettierOptions = () => { + try { + return require(path.join(getPaths().base, 'prettier.config.js')) + } catch (e) { + return undefined + } +} + /** * Returns the database schema for the given `name` database table parsed from * the schema.prisma of the target applicaiton. If no `name` is given then the * entire schema is returned. */ -export const getSchema = async (name) => { +export const getSchema = async (name: string) => { const schema = await getSchemaDefinitions() if (name) { @@ -49,10 +88,10 @@ export const getSchema = async (name) => { /** * Returns the enum defined with the given `name` parsed from - * the schema.prisma of the target applicaiton. If no `name` is given then the + * the schema.prisma of the target application. If no `name` is given then the * all enum definitions are returned */ -export const getEnum = async (name) => { +export const getEnum = async (name: string) => { const schema = await getSchemaDefinitions() if (name) { @@ -72,18 +111,6 @@ export const getEnum = async (name) => { return schema.metadata.datamodel.enums } -/* - * Returns the DMMF defined by `prisma` resolving the relevant `shema.prisma` path. - */ -export const getSchemaDefinitions = async () => { - const schemaPath = path.join(getPaths().api.db, 'schema.prisma') - const metadata = await getDMMF({ - datamodel: readFile(schemaPath.toString()), - }) - - return metadata -} - /** * Returns variants of the passed `name` for usage in templates. If the given * name was "fooBar" then these would be: @@ -98,7 +125,7 @@ export const getSchemaDefinitions = async () => { * singularConstantName: FOO_BAR * pluralConstantName: FOO_BARS */ -export const nameVariants = (name) => { +export const nameVariants = (name: string) => { const normalizedName = pascalcase(paramCase(pluralize.singular(name))) return { @@ -117,7 +144,10 @@ export const nameVariants = (name) => { export const templateRoot = path.resolve(__dirname, '../commands/generate') -export const generateTemplate = (templateFilename, { name, root, ...rest }) => { +export const generateTemplate = ( + templateFilename: string, + { name, root, ...rest }: { [key: string]: any } +) => { const templatePath = path.join(root || templateRoot, templateFilename) const template = lodash.template(readFile(templatePath).toString()) @@ -133,7 +163,7 @@ export const generateTemplate = (templateFilename, { name, root, ...rest }) => { const parser = { '.css': 'css', '.js': 'babel', - }[path.extname(templateFilename)] + }[path.extname(templateFilename) as '.css' | '.js'] if (typeof parser === 'undefined') { return renderedTemplate @@ -145,11 +175,9 @@ export const generateTemplate = (templateFilename, { name, root, ...rest }) => { }) } -export const readFile = (target) => fs.readFileSync(target) - export const writeFile = async ( - target, - contents, + target: string, + contents: string | object, { overwriteExisting = false } = {} ) => { if (!overwriteExisting && fs.existsSync(target)) { @@ -162,38 +190,18 @@ export const writeFile = async ( fs.writeFileSync(target, contents) } -export const bytes = (contents) => Buffer.byteLength(contents, 'utf8') - -/** - * This wraps the core version of getPaths into something that catches the exception - * and displays a helpful error message. - */ -export const getPaths = () => { - try { - return getRedwoodPaths() - } catch (e) { - console.error(c.error(e.message)) - process.exit(0) - } -} - -/** - * This returns the config present in `prettier.config.js` of a Redwood project. - */ -export const prettierOptions = () => { - try { - return require(path.join(getPaths().base, 'prettier.config.js')) - } catch (e) { - return undefined - } -} +export const bytes = (contents: Parameters[0]) => + Buffer.byteLength(contents, 'utf8') /** * Creates a list of tasks that write files to the disk. * * @param files - {[filepath]: contents} */ -export const writeFilesTask = (files, options) => { +export const writeFilesTask = ( + files: { [filepath: string]: string }, + options: { overwriteExisting: boolean } +) => { const { base } = getPaths() return new Listr( Object.keys(files).map((file) => { @@ -209,7 +217,7 @@ export const writeFilesTask = (files, options) => { /** * Update the project's routes file. */ -export const addRoutesToRouterTask = (routes) => { +export const addRoutesToRouterTask = (routes: string[]) => { const redwoodPaths = getPaths() const routesContent = readFile(redwoodPaths.web.routes).toString() const newRoutesContent = routes.reverse().reduce((content, route) => { @@ -223,7 +231,15 @@ export const addRoutesToRouterTask = (routes) => { }) } -export const runCommandTask = async (commands, { verbose }) => { +export const runCommandTask = async ( + commands: { + title: string + cmd: string + args: string[] + opts: execa.Options + }[], + { verbose }: { verbose: boolean } +) => { const tasks = new Listr( commands.map(({ title, cmd, args, opts = {} }) => ({ title, @@ -240,6 +256,7 @@ export const runCommandTask = async (commands, { verbose }) => { })), { renderer: verbose && VerboseRenderer, + // @ts-ignore TODO dateFormat comes from listr-verbose-renderer dateFormat: false, } ) diff --git a/packages/cli/src/lib/test.js b/packages/cli/src/lib/test.ts similarity index 93% rename from packages/cli/src/lib/test.js rename to packages/cli/src/lib/test.ts index 0861fb4afa1b..34419f753bb5 100644 --- a/packages/cli/src/lib/test.js +++ b/packages/cli/src/lib/test.ts @@ -43,6 +43,11 @@ export const generatorsRootPath = path.join( 'generate' ) +// Returns the contents of a text file suffixed with ".fixture" +export const loadFixture = (filepath: string) => { + return fs.readFileSync(filepath).toString() +} + // Loads the fixture for a generator by assuming a lot of the path structure automatically: // // loadGeneratorFixture('scaffold', 'NamePage.js') @@ -50,7 +55,7 @@ export const generatorsRootPath = path.join( // will return the contents of: // // cli/src/commands/generate/scaffold/test/fixtures/NamePage.js.fixture -export const loadGeneratorFixture = (generator, name) => { +export const loadGeneratorFixture = (generator: string, name: string) => { return loadFixture( path.join( __dirname, @@ -64,8 +69,3 @@ export const loadGeneratorFixture = (generator, name) => { ) ) } - -// Returns the contents of a text file suffixed with ".fixture" -export const loadFixture = (filepath) => { - return fs.readFileSync(filepath).toString() -} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 000000000000..18ddc1f00878 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.compilerOption.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "esModuleInterop": true + }, + "include": ["src", "index.d.ts"], +} diff --git a/packages/internal/src/paths.ts b/packages/internal/src/paths.ts index c857326ac668..b9fa95d2f4d7 100644 --- a/packages/internal/src/paths.ts +++ b/packages/internal/src/paths.ts @@ -22,6 +22,8 @@ const PATH_WEB_DIR_COMPONENTS = 'web/src/components' const PATH_WEB_DIR_SRC = 'web/src' const PATH_WEB_DIR_CONFIG = 'web/config/webpack.config.js' +export type { Paths } from './types' + /** * Search the parent directories for the Redwood configuration file. */ diff --git a/yarn.lock b/yarn.lock index 691ec3670c95..fbfc12ff711a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2695,6 +2695,14 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/listr@^0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@types/listr/-/listr-0.14.2.tgz#2e5f80fbc3ca8dceb9940ce9bf8e3113ab452545" + integrity sha512-wCipMbQr3t2UHTm90LldVp+oTBj1TX6zvpkCJcWS4o8nn6kS8SN93oUvKJAgueIRZ5M36yOlFmScqBxYH8Ajig== + dependencies: + "@types/node" "*" + rxjs "^6.5.1" + "@types/lodash.merge@^4.6.6": version "4.6.6" resolved "https://registry.yarnpkg.com/@types/lodash.merge/-/lodash.merge-4.6.6.tgz#b84b403c1d31bc42d51772d1cd5557fa008cd3d6" @@ -2714,6 +2722,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.150.tgz#649fe44684c3f1fcb6164d943c5a61977e8cf0bd" integrity sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w== +"@types/lodash@^4.14.151": + version "4.14.151" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.151.tgz#7d58cac32bedb0ec37cb7f99094a167d6176c9d5" + integrity sha512-Zst90IcBX5wnwSu7CAS0vvJkTjTELY4ssKbHiTnGcJgi170uiS8yQDdc3v6S77bRqYQIN1App5a1Pc2lceE5/g== + "@types/long@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" @@ -2779,6 +2792,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/pluralize@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/pluralize/-/pluralize-0.0.29.tgz#6ffa33ed1fc8813c469b859681d09707eb40d03c" + integrity sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA== + "@types/prettier@^1.19.0": version "1.19.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" @@ -2924,6 +2942,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^15.0.5": + version "15.0.5" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.5.tgz#947e9a6561483bdee9adffc983e91a6902af8b79" + integrity sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w== + dependencies: + "@types/yargs-parser" "*" + "@types/zen-observable@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" @@ -12400,7 +12425,7 @@ rx@4.1.0: resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I= -rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.5.3: +rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.1, rxjs@^6.5.2, rxjs@^6.5.3: version "6.5.5" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==