zapier-platform-cli 15.18.0 → 15.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import zapier from 'zapier-platform-core';
3
+
4
+ import App from '../../index';
5
+
6
+ const appTester = zapier.createAppTester(App);
7
+ // read the `.env` file into the environment, if available
8
+ zapier.tools.env.inject();
9
+
10
+ describe('<%= ACTION_PLURAL %>.<%= KEY %>', () => {
11
+ it('should run', async () => {
12
+ const bundle = { inputData: {} };
13
+
14
+ const results = await appTester(App.<%= ACTION_PLURAL %>['<%= KEY %>'].<%= MAYBE_RESOURCE %>operation.perform, bundle);
15
+ expect(results).toBeDefined();
16
+ // TODO: add more assertions
17
+ });
18
+ });
@@ -0,0 +1,58 @@
1
+ import type { PerformFunction, Trigger } from 'zapier-platform-core';
2
+
3
+ // triggers on a new <%= LOWER_NOUN %> with a certain tag
4
+ const perform: PerformFunction = async (z, bundle) => {
5
+ const response = await z.request({
6
+ url: 'https://jsonplaceholder.typicode.com/posts',
7
+ params: {
8
+ tag: bundle.inputData.tagName,
9
+ },
10
+ });
11
+ // this should return an array of objects
12
+ return response.data;
13
+ };
14
+
15
+ export default {
16
+ // see here for a full list of available properties:
17
+ // https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#triggerschema
18
+ key: '<%= KEY %>' as const,
19
+ noun: '<%= NOUN %>',
20
+
21
+ display: {
22
+ label: 'New <%= NOUN %>',
23
+ description: 'Triggers when a new <%= LOWER_NOUN %> is created.',
24
+ },
25
+
26
+ operation: {
27
+ type: 'polling',
28
+ perform,
29
+
30
+ <%= INCLUDE_INTRO_COMMENTS ? [
31
+ '// `inputFields` defines the fields a user could provide',
32
+ '// Zapier will pass them in as `bundle.inputData` later. They\'re optional.'
33
+ ].join('\n ') : '' %>
34
+ inputFields: [],
35
+
36
+ <%= INCLUDE_INTRO_COMMENTS ? [
37
+ '// In cases where Zapier needs to show an example record to the user, but we are unable to get a live example',
38
+ '// from the API, Zapier will fallback to this hard-coded sample. It should reflect the data structure of',
39
+ '// returned records, and have obvious placeholder values that we can show to any user.'
40
+ ].join('\n ') : '' %>
41
+ sample: {
42
+ id: 1,
43
+ name: 'Test',
44
+ },
45
+
46
+ <%= INCLUDE_INTRO_COMMENTS ? [
47
+ '// If fields are custom to each user (like spreadsheet columns), `outputFields` can create human labels',
48
+ '// For a more complete example of using dynamic fields see',
49
+ '// https://github.com/zapier/zapier-platform/tree/main/packages/cli#customdynamic-fields',
50
+ '// Alternatively, a static field definition can be provided, to specify labels for the fields'
51
+ ].join('\n ') : '' %>
52
+ outputFields: [
53
+ // these are placeholders to match the example `perform` above
54
+ // {key: 'id', label: 'Person ID'},
55
+ // {key: 'name', label: 'Person Name'}
56
+ ],
57
+ },
58
+ } satisfies Trigger;
@@ -249,6 +249,7 @@ const testAuth = async (authData, meta, zcacheTestObj) => {
249
249
  },
250
250
  zcacheTestObj,
251
251
  customLogger,
252
+ calledFromCliInvoke: true,
252
253
  });
253
254
  endSpinner();
254
255
  return result;
@@ -367,7 +368,7 @@ class InvokeCommand extends BaseCommand {
367
368
  ]);
368
369
  }
369
370
 
370
- async startCustomAuth(authFields) {
371
+ async startCustomAuth(authFields, zcacheTestObj) {
371
372
  if (this.nonInteractive) {
372
373
  throw new Error(
373
374
  'The `auth start` subcommand for "custom" authentication type only works in interactive mode.'
@@ -376,7 +377,7 @@ class InvokeCommand extends BaseCommand {
376
377
  return this.promptForAuthFields(authFields);
377
378
  }
378
379
 
379
- async startOAuth2(appDefinition) {
380
+ async startOAuth2(appDefinition, zcacheTestObj) {
380
381
  const redirectUri = this.flags['redirect-uri'];
381
382
  const port = this.flags['local-port'];
382
383
  const env = {};
@@ -428,6 +429,9 @@ class InvokeCommand extends BaseCommand {
428
429
  state: stateParam,
429
430
  },
430
431
  },
432
+ zcacheTestObj,
433
+ customLogger,
434
+ calledFromCliInvoke: true,
431
435
  });
432
436
  if (!authorizeUrl.includes('&scope=')) {
433
437
  const scope = appDefinition.authentication.oauth2Config.scope;
@@ -499,13 +503,16 @@ class InvokeCommand extends BaseCommand {
499
503
  redirect_uri: redirectUri,
500
504
  },
501
505
  },
506
+ zcacheTestObj,
507
+ customLogger,
508
+ calledFromCliInvoke: true,
502
509
  });
503
510
 
504
511
  endSpinner();
505
512
  return authData;
506
513
  }
507
514
 
508
- async startSessionAuth(appDefinition) {
515
+ async startSessionAuth(appDefinition, zcacheTestObj) {
509
516
  if (this.nonInteractive) {
510
517
  throw new Error(
511
518
  'The `auth start` subcommand for "session" authentication type only works in interactive mode.'
@@ -522,13 +529,16 @@ class InvokeCommand extends BaseCommand {
522
529
  bundle: {
523
530
  authData,
524
531
  },
532
+ zcacheTestObj,
533
+ customLogger,
534
+ calledFromCliInvoke: true,
525
535
  });
526
536
  endSpinner();
527
537
 
528
538
  return { ...authData, ...sessionData };
529
539
  }
530
540
 
531
- async startAuth(appDefinition) {
541
+ async startAuth(appDefinition, zcacheTestObj) {
532
542
  const authentication = appDefinition.authentication;
533
543
  if (!authentication) {
534
544
  console.warn(
@@ -542,11 +552,11 @@ class InvokeCommand extends BaseCommand {
542
552
  case 'basic':
543
553
  return this.startBasicAuth(authentication.fields);
544
554
  case 'custom':
545
- return this.startCustomAuth(authentication.fields);
555
+ return this.startCustomAuth(authentication.fields, zcacheTestObj);
546
556
  case 'oauth2':
547
- return this.startOAuth2(appDefinition);
557
+ return this.startOAuth2(appDefinition, zcacheTestObj);
548
558
  case 'session':
549
- return this.startSessionAuth(appDefinition);
559
+ return this.startSessionAuth(appDefinition, zcacheTestObj);
550
560
  default:
551
561
  // TODO: Add support for 'digest' and 'oauth1'
552
562
  throw new Error(
@@ -555,7 +565,7 @@ class InvokeCommand extends BaseCommand {
555
565
  }
556
566
  }
557
567
 
558
- async refreshOAuth2(appDefinition, authData) {
568
+ async refreshOAuth2(appDefinition, authData, zcacheTestObj) {
559
569
  startSpinner('Invoking authentication.oauth2Config.refreshAccessToken');
560
570
 
561
571
  const newAuthData = await localAppCommand({
@@ -564,13 +574,16 @@ class InvokeCommand extends BaseCommand {
564
574
  bundle: {
565
575
  authData,
566
576
  },
577
+ zcacheTestObj,
578
+ customLogger,
579
+ calledFromCliInvoke: true,
567
580
  });
568
581
 
569
582
  endSpinner();
570
583
  return newAuthData;
571
584
  }
572
585
 
573
- async refreshSessionAuth(appDefinition, authData) {
586
+ async refreshSessionAuth(appDefinition, authData, zcacheTestObj) {
574
587
  startSpinner('Invoking authentication.sessionConfig.perform');
575
588
 
576
589
  const sessionData = await localAppCommand({
@@ -579,13 +592,16 @@ class InvokeCommand extends BaseCommand {
579
592
  bundle: {
580
593
  authData,
581
594
  },
595
+ zcacheTestObj,
596
+ customLogger,
597
+ calledFromCliInvoke: true,
582
598
  });
583
599
 
584
600
  endSpinner();
585
601
  return sessionData;
586
602
  }
587
603
 
588
- async refreshAuth(appDefinition, authData) {
604
+ async refreshAuth(appDefinition, authData, zcacheTestObj) {
589
605
  const authentication = appDefinition.authentication;
590
606
  if (!authentication) {
591
607
  console.warn(
@@ -602,9 +618,9 @@ class InvokeCommand extends BaseCommand {
602
618
  }
603
619
  switch (authentication.type) {
604
620
  case 'oauth2':
605
- return this.refreshOAuth2(appDefinition, authData);
621
+ return this.refreshOAuth2(appDefinition, authData, zcacheTestObj);
606
622
  case 'session':
607
- return this.refreshSessionAuth(appDefinition, authData);
623
+ return this.refreshSessionAuth(appDefinition, authData, zcacheTestObj);
608
624
  default:
609
625
  throw new Error(
610
626
  `This command doesn't support refreshing authentication type "${authentication.type}".`
@@ -840,6 +856,7 @@ class InvokeCommand extends BaseCommand {
840
856
  zcacheTestObj,
841
857
  cursorTestObj,
842
858
  customLogger,
859
+ calledFromCliInvoke: true,
843
860
  });
844
861
  endSpinner();
845
862
 
@@ -873,6 +890,7 @@ class InvokeCommand extends BaseCommand {
873
890
  zcacheTestObj,
874
891
  cursorTestObj,
875
892
  customLogger,
893
+ calledFromCliInvoke: true,
876
894
  });
877
895
  endSpinner();
878
896
 
@@ -952,7 +970,10 @@ class InvokeCommand extends BaseCommand {
952
970
  };
953
971
  switch (actionKey) {
954
972
  case 'start': {
955
- const newAuthData = await this.startAuth(appDefinition);
973
+ const newAuthData = await this.startAuth(
974
+ appDefinition,
975
+ zcacheTestObj
976
+ );
956
977
  if (_.isEmpty(newAuthData)) {
957
978
  return;
958
979
  }
@@ -963,7 +984,11 @@ class InvokeCommand extends BaseCommand {
963
984
  return;
964
985
  }
965
986
  case 'refresh': {
966
- const newAuthData = await this.refreshAuth(appDefinition, authData);
987
+ const newAuthData = await this.refreshAuth(
988
+ appDefinition,
989
+ authData,
990
+ zcacheTestObj
991
+ );
967
992
  if (_.isEmpty(newAuthData)) {
968
993
  return;
969
994
  }
@@ -1,4 +1,8 @@
1
+ /* eslint-disable camelcase */
2
+ // @ts-check
3
+
1
4
  const path = require('path');
5
+ const fs = require('fs');
2
6
 
3
7
  const { flags } = require('@oclif/command');
4
8
 
@@ -6,8 +10,7 @@ const BaseCommand = require('../ZapierBaseCommand');
6
10
  const { buildFlags } = require('../buildFlags');
7
11
 
8
12
  const {
9
- createTemplateContext,
10
- getRelativeRequirePath,
13
+ createScaffoldingContext,
11
14
  plural,
12
15
  updateEntryFile,
13
16
  isValidEntryFileUpdate,
@@ -18,126 +21,89 @@ const { isValidAppInstall } = require('../../utils/misc');
18
21
  const { writeFile } = require('../../utils/files');
19
22
  const { ISSUES_URL } = require('../../constants');
20
23
 
21
- const getNewFileDirectory = (action, test = false) =>
22
- path.join(test ? 'test/' : '', plural(action));
23
-
24
- const getLocalFilePath = (directory, actionKey) =>
25
- path.join(directory, actionKey);
26
- /**
27
- * both the string to `require` and later, the filepath to write to
28
- */
29
- const getFullActionFilePath = (directory, actionKey) =>
30
- path.join(process.cwd(), getLocalFilePath(directory, actionKey));
31
-
32
- const getFullActionFilePathWithExtension = (directory, actionKey, isTest) =>
33
- `${getFullActionFilePath(directory, actionKey)}${isTest ? '.test' : ''}.js`;
34
-
35
24
  class ScaffoldCommand extends BaseCommand {
36
25
  async perform() {
37
26
  const { actionType, noun } = this.args;
38
-
39
- // TODO: interactive portion here?
27
+ const indexFileLocal = this.flags.entry ?? this.defaultIndexFileLocal();
40
28
  const {
41
- dest: newActionDir = getNewFileDirectory(actionType),
42
- testDest: newTestActionDir = getNewFileDirectory(actionType, true),
43
- entry = 'index.js',
29
+ dest: actionDirLocal = this.defaultActionDirLocal(indexFileLocal),
30
+ 'test-dest': testDirLocal = this.defaultTestDirLocal(indexFileLocal),
44
31
  force,
45
32
  } = this.flags;
46
33
 
47
- // this is possible, just extra work that's out of scope
48
- // const tsParser = j.withParser('ts')
49
- // tsParser(codeStr)
50
- // will have to change logic probably though
51
- if (entry.endsWith('ts')) {
52
- this.error(
53
- `Typescript isn't supported for scaffolding yet. Instead, try copying the example code at https://github.com/zapier/zapier-platform/blob/b8224ec9855be91c66c924b731199a068b1e913a/example-apps/typescript/src/resources/recipe.ts`
54
- );
55
- }
34
+ const language = indexFileLocal.endsWith('.ts') ? 'ts' : 'js';
56
35
 
57
- const shouldIncludeComments = !this.flags['no-help']; // when called from other commands (namely `init`) this will be false
58
- const templateContext = createTemplateContext(
36
+ const context = createScaffoldingContext({
59
37
  actionType,
60
38
  noun,
61
- shouldIncludeComments
62
- );
63
-
64
- const actionKey = templateContext.KEY;
39
+ language,
40
+ indexFileLocal,
41
+ actionDirLocal,
42
+ testDirLocal,
43
+ includeIntroComments: !this.flags['no-help'],
44
+ preventOverwrite: !force,
45
+ });
65
46
 
66
- const preventOverwrite = !force;
67
47
  // TODO: read from config file?
68
48
 
69
- this.startSpinner(
70
- `Creating new file: ${getLocalFilePath(newActionDir, actionKey)}.js`
71
- );
72
- await writeTemplateFile(
73
- actionType,
74
- templateContext,
75
- getFullActionFilePathWithExtension(newActionDir, actionKey),
76
- preventOverwrite
77
- );
49
+ this.startSpinner(`Creating new file: ${context.actionFileLocal}`);
50
+
51
+ await writeTemplateFile({
52
+ destinationPath: context.actionFileResolved,
53
+ templateType: context.actionType,
54
+ language: context.language,
55
+ preventOverwrite: context.preventOverwrite,
56
+ templateContext: context.templateContext,
57
+ });
78
58
  this.stopSpinner();
79
59
 
80
- this.startSpinner(
81
- `Creating new test file: ${getLocalFilePath(
82
- newTestActionDir,
83
- actionKey
84
- )}.js`
85
- );
86
- await writeTemplateFile(
87
- 'test',
88
- templateContext,
89
- getFullActionFilePathWithExtension(newTestActionDir, actionKey, true),
90
- preventOverwrite
91
- );
60
+ this.startSpinner(`Creating new test file: ${context.testFileLocal}`);
61
+ await writeTemplateFile({
62
+ destinationPath: context.testFileResolved,
63
+ templateType: 'test',
64
+ language: context.language,
65
+ preventOverwrite: context.preventOverwrite,
66
+ templateContext: context.templateContext,
67
+ });
92
68
  this.stopSpinner();
93
69
 
94
70
  // * rewire the index.js to point to the new file
95
- this.startSpinner(`Rewriting your ${entry}`);
71
+ this.startSpinner(`Rewriting your ${context.indexFileLocal}`);
96
72
 
97
- const entryFilePath = path.join(process.cwd(), entry);
98
-
99
- const originalContents = await updateEntryFile(
100
- entryFilePath,
101
- templateContext.VARIABLE,
102
- getFullActionFilePath(newActionDir, actionKey),
103
- actionType,
104
- templateContext.KEY
105
- );
73
+ const originalContents = await updateEntryFile({
74
+ language: context.language,
75
+ indexFileResolved: context.indexFileResolved,
76
+ actionRelativeImportPath: context.actionRelativeImportPath,
77
+ actionImportName: context.templateContext.VARIABLE,
78
+ actionType: context.actionType,
79
+ });
106
80
 
107
81
  if (isValidAppInstall().valid) {
108
82
  const success = isValidEntryFileUpdate(
109
- entryFilePath,
110
- actionType,
111
- templateContext.KEY
83
+ context.language,
84
+ context.indexFileResolved,
85
+ context.actionType,
86
+ context.templateContext.KEY
112
87
  );
113
88
 
114
89
  this.stopSpinner({ success });
115
90
 
116
91
  if (!success) {
117
- const entryName = splitFileFromPath(entryFilePath)[1];
92
+ const entryName = splitFileFromPath(context.indexFileResolved)[1];
118
93
 
119
94
  this.startSpinner(
120
95
  `Unable to successfully rewrite your ${entryName}. Rolling back...`
121
96
  );
122
- await writeFile(entryFilePath, originalContents);
97
+ await writeFile(context.indexFileResolved, originalContents);
123
98
  this.stopSpinner();
124
99
 
125
100
  this.error(
126
101
  [
127
- `\nPlease add the following lines to ${entryFilePath}:`,
128
- ` * \`const ${
129
- templateContext.VARIABLE
130
- } = require('./${getRelativeRequirePath(
131
- entryFilePath,
132
- getFullActionFilePath(newActionDir, actionKey)
133
- )}');\` at the top-level`,
134
- ` * \`[${templateContext.VARIABLE}.key]: ${
135
- templateContext.VARIABLE
136
- }\` in the "${plural(
137
- actionType
138
- )}" object in your exported integration definition.`,
102
+ `\nPlease add the following lines to ${context.indexFileResolved}:`,
103
+ ` * \`const ${context.templateContext.VARIABLE} = require('./${context.actionRelativeImportPath}');\` at the top-level`,
104
+ ` * \`[${context.templateContext.VARIABLE}.key]: ${context.templateContext.VARIABLE}\` in the "${context.actionTypePlural}" object in your exported integration definition.`,
139
105
  '',
140
- `Also, please file an issue at ${ISSUES_URL} with the contents of your ${entryFilePath}.`,
106
+ `Also, please file an issue at ${ISSUES_URL} with the contents of your ${context.indexFileResolved}.`,
141
107
  ].join('\n')
142
108
  );
143
109
  }
@@ -146,8 +112,51 @@ class ScaffoldCommand extends BaseCommand {
146
112
  this.stopSpinner();
147
113
 
148
114
  if (!this.flags.invokedFromAnotherCommand) {
149
- this.log(`\nAll done! Your new ${actionType} is ready to use.`);
115
+ this.log(`\nAll done! Your new ${context.actionType} is ready to use.`);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * If `--entry` is not provided, this will determine the path to the
121
+ * root index file. Notably, we'll look for tsconfig.json and
122
+ * src/index.ts first, because even TS apps have a root level plain
123
+ * index.js that we should ignore.
124
+ *
125
+ * @returns {string}
126
+ */
127
+ defaultIndexFileLocal() {
128
+ const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
129
+ const srcIndexTsPath = path.join(process.cwd(), 'src', 'index.ts');
130
+ if (fs.existsSync(tsConfigPath) && fs.existsSync(srcIndexTsPath)) {
131
+ this.log('Automatically detected TypeScript project');
132
+ return 'src/index.ts';
150
133
  }
134
+
135
+ return 'index.js';
136
+ }
137
+
138
+ /**
139
+ * If `--dest` is not provided, this will determine the directory for
140
+ * the new action file to be created in.
141
+ *
142
+ * @param {string} indexFileLocal - The path to the index file
143
+ * @returns {string}
144
+ */
145
+ defaultActionDirLocal(indexFileLocal) {
146
+ const parent = path.dirname(indexFileLocal);
147
+ return path.join(parent, plural(this.args.actionType));
148
+ }
149
+
150
+ /**
151
+ * If `--test-dest` is not provided, this will determine the directory
152
+ * for the new test file to be created in.
153
+ *
154
+ * @param {string} indexFileLocal - The path to the index file
155
+ * @returns {string}
156
+ */
157
+ defaultTestDirLocal(indexFileLocal) {
158
+ const parent = path.dirname(indexFileLocal);
159
+ return path.join(parent, 'test', plural(this.args.actionType));
151
160
  }
152
161
  }
153
162
 
@@ -179,13 +188,12 @@ ScaffoldCommand.flags = buildFlags({
179
188
  entry: flags.string({
180
189
  char: 'e',
181
190
  description:
182
- "Supply the path to your integration's root (`index.js`). Only needed if your `index.js` is in a subfolder, like `src`.",
183
- default: 'index.js',
191
+ "Supply the path to your integration's entry point (`index.js` or `src/index.ts`). This will try to automatically detect the correct file if not provided.",
184
192
  }),
185
193
  force: flags.boolean({
186
194
  char: 'f',
187
195
  description:
188
- 'Should we overwrite an exisiting trigger/search/create file?',
196
+ 'Should we overwrite an existing trigger/search/create file?',
189
197
  default: false,
190
198
  }),
191
199
  'no-help': flags.boolean({
package/src/utils/ast.js CHANGED
@@ -1,6 +1,9 @@
1
- // tools for modifiyng an AST
1
+ // @ts-check
2
+
3
+ // tools for modifying an AST
2
4
 
3
5
  const j = require('jscodeshift');
6
+ const ts = j.withParser('ts');
4
7
 
5
8
  // simple helper functions used for searching for nodes
6
9
  // can't use j.identifier(name) because it has extra properties and we have to have no extras to find nodes
@@ -21,7 +24,7 @@ const typeHelpers = {
21
24
  /**
22
25
  * adds a `const verName = require(path)` to the root of a codeStr
23
26
  */
24
- const createRootRequire = (codeStr, varName, path) => {
27
+ const importActionInJsApp = (codeStr, varName, path) => {
25
28
  if (codeStr.match(new RegExp(`${varName} ?= ?require`))) {
26
29
  // duplicate identifier, no need to re-add
27
30
  // this would fail if they used this variable name for something else; we'd keep going and double-declare that variable
@@ -61,7 +64,7 @@ const createRootRequire = (codeStr, varName, path) => {
61
64
  return root.toSource();
62
65
  };
63
66
 
64
- const addKeyToPropertyOnApp = (codeStr, property, varName) => {
67
+ const registerActionInJsApp = (codeStr, property, varName) => {
65
68
  // to play with this, use https://astexplorer.net/#/gist/cb4986b3f1c6eb975339608109a48e7d/0fbf2fabbcf27d0b6ebd8910f979bd5d97dd9404
66
69
 
67
70
  const root = j(codeStr);
@@ -136,4 +139,102 @@ const addKeyToPropertyOnApp = (codeStr, property, varName) => {
136
139
  return root.toSource();
137
140
  };
138
141
 
139
- module.exports = { createRootRequire, addKeyToPropertyOnApp };
142
+ /**
143
+ * Adds an import statement to the top of an index.ts file to import a
144
+ * new action, such as `import some_trigger from './triggers/some_trigger';`
145
+ *
146
+ * @param {string} codeStr - The code of the index.ts file to modify.
147
+ * @param {string} identifierName - The name of imported action used as a variable in the code.
148
+ * @param {string} actionRelativeImportPath - The relative path to import the action from
149
+ * @returns {string}
150
+ */
151
+ const importActionInTsApp = (
152
+ codeStr,
153
+ identifierName,
154
+ actionRelativeImportPath
155
+ ) => {
156
+ const root = ts(codeStr);
157
+
158
+ const imports = root.find(ts.ImportDeclaration);
159
+
160
+ const newImportStatement = j.importDeclaration(
161
+ [j.importDefaultSpecifier(j.identifier(identifierName))],
162
+ j.literal(actionRelativeImportPath)
163
+ );
164
+
165
+ if (imports.length) {
166
+ imports.at(-1).insertAfter(newImportStatement);
167
+ } else {
168
+ const body = root.find(ts.Program).get().node.body;
169
+ body.unshift(newImportStatement);
170
+ // Add newline after import?
171
+ }
172
+
173
+ return root.toSource({ quote: 'single' });
174
+ };
175
+
176
+ /**
177
+ *
178
+ * @param {string} codeStr
179
+ * @param {'creates' | 'searches' | 'triggers'} actionTypePlural - The type of action to register within the app
180
+ * @param {string} identifierName - Name of the action imported to be registered
181
+ * @returns {string}
182
+ */
183
+ const registerActionInTsApp = (codeStr, actionTypePlural, identifierName) => {
184
+ const root = ts(codeStr);
185
+
186
+ // the `[thing.key]: thing` entry we'd like to insert.
187
+ const newProperty = ts.property.from({
188
+ kind: 'init',
189
+ key: j.memberExpression(j.identifier(identifierName), j.identifier('key')),
190
+ value: j.identifier(identifierName),
191
+ computed: true,
192
+ });
193
+
194
+ // Find the top level app Object; the one with the `platformVersion`
195
+ // key. This is where we'll insert our new property.
196
+ const appObjectCandidates = root
197
+ .find(ts.ObjectExpression)
198
+ .filter((path) =>
199
+ path.value.properties.some(
200
+ (prop) => prop.key && prop.key.name === 'platformVersion'
201
+ )
202
+ );
203
+ if (appObjectCandidates.length !== 1) {
204
+ throw new Error('Unable to find the app definition to modify');
205
+ }
206
+ const appObj = appObjectCandidates.get().node;
207
+
208
+ // Now we have an app object to modify.
209
+
210
+ // Check if this object already has the actionType group inside it.
211
+ const existingProp = appObj.properties.find(
212
+ (props) => props.key.name === actionTypePlural
213
+ );
214
+ if (existingProp) {
215
+ const value = existingProp.value;
216
+ if (value.type !== 'ObjectExpression') {
217
+ throw new Error(
218
+ `Tried to edit the ${actionTypePlural} key, but the value wasn't an object`
219
+ );
220
+ }
221
+ value.properties.push(newProperty);
222
+ } else {
223
+ appObj.properties.push(
224
+ j.property(
225
+ 'init',
226
+ j.identifier(actionTypePlural),
227
+ j.objectExpression([newProperty])
228
+ )
229
+ );
230
+ }
231
+
232
+ return root.toSource({ quote: 'single' });
233
+ };
234
+
235
+ module.exports = {
236
+ importActionInJsApp,
237
+ registerActionInJsApp,
238
+ importActionInTsApp,
239
+ registerActionInTsApp,
240
+ };