Skip to content
This repository has been archived by the owner on Jun 27, 2019. It is now read-only.

Convert visual apps to CLI. #427

Merged
merged 9 commits into from
May 23, 2019
Merged

Convert visual apps to CLI. #427

merged 9 commits into from
May 23, 2019

Conversation

xavdid
Copy link
Contributor

@xavdid xavdid commented May 8, 2019

Standing on the shoulders of other great work, this ended up being pretty straightforward!

I liked the idea of re-using the command. That, plus adding the version option, makes it pretty straightforward.

There are a couple of different methods based on differences between what we need for visual/legacy, but the similarity made that part really easy.

I've been testing with the following:

zapier convert 17741 a --version=1.0.1, which is a dev-app-school wistia app. I've confirmed that you can convert, push a new version, and make a new zap (auth + trigger) successfully.

@@ -2,7 +2,7 @@ const utils = require('../utils');
const constants = require('../constants');

const convert = (context, appid, location) => {
context.line('Welcome to the Zapier Platform! :-D');
context.line('Welcome to the Zapier Platform!');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we even need this message at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, not really!

package.json Outdated
"lint-snippets": "eslint snippets --rule 'no-unused-vars: 0' --ignore-pattern snippets/next.js --ignore-pattern 'snippets/dynamic-dropdowns-*'",
"lint": "eslint zapier.js src",
"postlint": "npm run lint-snippets",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

Copy link
Contributor

@stevelikesmusic stevelikesmusic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @xavdid! I like the leveraging of the current convert tool—great decision. We will still need to tweak its internals a bit. Especially for getting dynamicInputs which are in a broken state as is. I'm marking as Request Changes for that, but the rest is suggestions, questions, and such.

A few thoughts:

  • Would we want devs to run without passing a version? I would expect most would want to use the latest version to convert.
  • Do we want to order the fields in index.js? It'd be nice if they were properly sorted like the minimal example app.
  • What about instead of using conditionals to fork logic, what if we had handlers for each source? (legacyHandler, vbHandler). Might be a little easier to follow, although we only have two choices right now. Other app sources could use an import command.

Still wrapping my head around what the rest of convert is doing and where we'd need to add some new logic.

Love the tests! 💪

}
}
utils.endSpinner();
return utils.convertVisualApp(appInfo, versionInfo, tempAppDir);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're going to throw in the catch why not move these two lines into the try. Then we won't need to set empty vars.

try {
  [appInfo, versionInfo] = await Promise.all([
    utils.callAPI(null, { url: appInfoUrl }, true),
    utils.callAPI(null, { url: versionInfoUrl }, true)
  ]);
      
  utils.endSpinner();
  return utils.convertVisualApp(appInfo, versionInfo, tempAppDir);
} catch (e) {
  throw ...

const convertApp = async (appInfo, appDefinition, newAppDir, opts = {}) => {
const defaultOpts = { silent: false, legacy: true };
// need to bump babel so this works
// const { silent, legacy } = { ...defaultOpts, ...opts };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator works in node v8. We can either lose these comments or switch to that syntax.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for sure. Our current version of babel throws an error here, so I think i'll:

i'll do that after we're 👍 though to keep this PR cleaner. I think the babel one will make it loud

};

const convertApp = async (appInfo, appDefinition, newAppDir, opts = {}) => {
const defaultOpts = { silent: false, legacy: true };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to even have silent? I may be missing where it's actually used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the only reason is the tests.

with silent:

$ NODE_ENV=test mocha -t 50000 --recursive lib/tests


  convert
    visual apps apps
      ✓ should create separate files (110ms)


  1 passing (125ms)

✨  Done in 3.65s.

without silent:

$ NODE_ENV=test mocha -t 50000 --recursive lib/tests


  convert
    visual apps apps
✔ Writing .gitignore
✔ Writing triggers/project.js
✔ Writing package.json
✔ Writing creates/create_project.js
✔ Writing .env
✔ Writing authentication.js
✔ Writing .zapierapprc
✔ Writing .zapierapprc
✔ Writing triggers/codemode.js
✔ Writing index.js
✔ Writing test/creates/create_project.js
✔ Writing test/triggers/project.js
✔ Writing test/triggers/codemode.js
      ✓ should create separate files (115ms)


  1 passing (132ms)

✨  Done in 3.66s.

Could get a similar effect by only logging if NODE_ENV !== 'TEST' if we wanted to simplify it a little bit

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @xavdid! That makes sense.

NODE_ENV could be a good option. No strong feelings one way or the other.

});
});

describe.only('visual apps apps', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to add eslint-jest to prevent these from making it into PRs. I've snuck those into PRs a few times and didn't catch until later.

params: {},
method: 'POST'
},
inputFields: [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 We should add a dynamic input field for testing. These are currently getting written as a giant string:

Copy link
Contributor Author

@xavdid xavdid May 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you clarify what you mean by dynamic input field? I know in CLI we can have custom fields, but I didn't think that was something that was possible in godzilla

was only looking in triggers, not actions. found it and fixed it

utils.callAPI(null, { url: cliDumpUrl })
]);
utils.startSpinner('Downloading app from Zapier');
let [legacyApp, appDefinition] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here as the vb-way above.

create_project: {
operation: {
perform: {
body: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to keep shorthand requests in a CLI integration? Do we care about encouraging using curlies within CLI? Not a big deal now that we support it because of Godzilla. Just wanted to point out that we'll be encouraging devs to rely on curly string replacement rather in core than regular ol' JS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we do want to keep the shorthand requests. They provide some nice auto error-handling and other sugar that are perfectly reasonable to rely on if you don't need a more complex request. Curlies should also be fine, though #428 makes me a little worried about that.


return await Promise.all(promises);
};

const convertVisualApp = async (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you say to re-organizing this slightly? This assumes we drop silent, but we could still keep it if we really wanted. We could pass in definition_override rather than versionInfo and use partial application to simplify these to functions:

const makeConvertApp = options => (info, def, dir) => ..stuff

const convertVisual = makeConvertApp({ legacy: false });
const convertLegacy = makeConvertApp({ legacy: true });

or if we thought we might want to call convertApp directly in the future, we could still change the signature of convertApp and move options to the front

const convertApp = (options, info, def, dir) => ...stuff

const convertVisual = convertApp.bind(null, { legacy: false });
// or with lodash
const convertLegacy = _.partial(convertApp, { legacy: true });

Also, no need to wrap in async since we're not awaiting anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do you one better - we can _.partialRight and not move any of the args!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And boom goes the dynamite 💥


return await utils.convertApp(legacyApp, appDefinition, tempAppDir);
return utils.convertLegacyApp(legacyApp, appDefinition, tempAppDir);
}
};

return utils.initApp(context, location, createApp).then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not instead of asking for the project location, we use the same string that we put in name for the package.json? (kebab-cased app title) I think it'd be rare that a dev would want those names to be different.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly so that devs can run the command from anywhere. zapier convert 1234 ../../projects. Since we're writing (and potentially overwriting) files, seems good to be explicit about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hear you. I wonder if that's what most do?

It'd be nice to have the option to just run zapier convert, get a list of integrations, pick the integration I want, and confirm things like version and/or location. Not for this PR—just thinking out loud.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally. I think this is good to get it out and we can revisit it at some point.

});

describe.only('visual apps apps', () => {
it('should create separate files', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we need tests to verify the output of files 🤔 For instance, to ensure we get dynamic inputs, functions are correctly written, files are ordered correctly, etc.

Maybe overkill?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah, more tests are good! I can cover a couple of key cases in there.

@xavdid
Copy link
Contributor Author

xavdid commented May 21, 2019

Great questions!

  1. I like being explicit about the version. It also reduces the number of API calls, since we only check for WB or godzilla. We could probably make both calls every time because it's unlikely that a dev has access to both CLI app 1234 and WB app 1234. We could also do a nice dropdown selector if we wanted, though that's probably out of scope here (we'd want to switch our prompt library from custom to off-the-shelf which is something I want to do anyway, but maybe not here)
  2. ordering could be nice, I'll look at that!
  3. I like that idea, but it sounds harder to maintain. As far as I can tell, you're proposing the following:
const convertLegacyApp(...) => {
  a()
  b()
  c()
  d()
}

const convertVisualApp(...) => {
  a()
  b()
  j()
  k()
  d()
}

where we move a lot of the logic into the reused functions. I like that, but worry that if we decide we want to add something after b, we now have to add that in multiple places (currently 2, but maybe more in the future). IMO, sounds tougher. But let me know if I'm misunderstanding!

@xavdid
Copy link
Contributor Author

xavdid commented May 22, 2019

ok, addressed most of those. let me know what you feel strongly about and we can get this over the line!

Copy link
Contributor

@stevelikesmusic stevelikesmusic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes and tests @xavdid! I think this is looking good. Couple more questions, but nothing I'd call a blocker.

Re the handler approach, I was thinking more instead of:

if (isVb) {
  const preprocessed =  ...doSomeVbStuff
  convertVb(preprocessed);
} else {
  const preprocessed =  ...doSomeWbStuff
  convertLegacy(preprocessed);
}

we might do something like (total pseudo-code):

const convert = getConvertHandler(sourceType);
convert(appData);

The idea being, we'd encapsulate what would be a separate conditional block in each handler. Maybe a pre-mature optimization at this point.

const args = maybeFunc.args || ['z', 'bundle'];
// always increment the number, but only return a value if it's > 0
const funcName = `get${_.upperFirst(key)}${
funcNum ? funcNum++ : ++funcNum && ''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. So it would go as getInputFields, getInputField1, getInputFields2...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep! read then increment.

> let i = 0
undefined
> console.log(i++)
0
undefined
> i
1
> i
1
> console.log(i++)
1
undefined
> i
2
> console.log(++i)
3
undefined
> i
3
> console.log(++i)
4
undefined
> i
4

@@ -2,11 +2,7 @@ const utils = require('../utils');
const constants = require('../constants');

const convert = (context, appid, location) => {
context.line('Welcome to the Zapier Platform! :-D');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really cool here to clear out the location folder if it exists. I forgot to pass version to the tool, and it converted my vb app as if it was WB. When I ran it again, it just added to folder rather than replace it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call out. there's another place we check for an empty folder- we can borrow that behavior.

@stevelikesmusic
Copy link
Contributor

stevelikesmusic commented May 22, 2019

Do we still have a separate ticket to build out the interface for this command? I thought there was also a ticket that would output a list of integration you had before executing the convert.

It's PDE-797

@xavdid
Copy link
Contributor Author

xavdid commented May 22, 2019

We still have a separate ticket, but a lot of that work is in this one.

We can launch this as is ASAP, but for the next major we could change the UI to have a nice selection setup. So many plates in the air!

@xavdid
Copy link
Contributor Author

xavdid commented May 23, 2019

Ah, so it turns out sorting keys is possible, but non-trivial if you have nested objects (which we do, in spades): https://stackoverflow.com/a/16168003/1825390

I'll skip for now but it'd be a nice cleanup thing to come back to (or render manually sometime). I'm going to go ahead and merge this as is.

@xavdid xavdid merged commit e9f6d05 into master May 23, 2019
@xavdid xavdid deleted the convert-visual branch May 23, 2019 02:50
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants