zapier-platform-cli 16.3.0 → 16.4.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,24 @@
1
+ const { API_URL } = require('../constants');
2
+
3
+ const perform = async (z, bundle) => {
4
+ const response = await z.request({ url: `${API_URL}/models` });
5
+
6
+ const responseData = response.data;
7
+
8
+ return responseData.data.map((model) => ({
9
+ id: model.id,
10
+ name: model.id,
11
+ }));
12
+ };
13
+
14
+ module.exports = {
15
+ key: 'list_models',
16
+ noun: 'Model',
17
+ display: {
18
+ label: 'List of Models',
19
+ description:
20
+ 'This is a hidden trigger, and is used in a Dynamic Dropdown of another trigger.',
21
+ hidden: true,
22
+ },
23
+ operation: { perform },
24
+ };
@@ -0,0 +1,33 @@
1
+ /* eslint-disable camelcase */
2
+ const authentication = require('./authentication');
3
+ const middleware = require('./middleware');
4
+ const dynamic_dropdowns = require('./dynamic_dropdowns');
5
+ const creates = require('./creates');
6
+
7
+ module.exports = {
8
+ // This is just shorthand to reference the installed dependencies you have.
9
+ // Zapier will need to know these before we can upload.
10
+ version: require('./package.json').version,
11
+ platformVersion: require('zapier-platform-core').version,
12
+
13
+ authentication,
14
+
15
+ beforeRequest: [...middleware.befores],
16
+
17
+ afterResponse: [...middleware.afters],
18
+
19
+ // If you want your trigger to show up, you better include it here!
20
+ triggers: {
21
+ ...dynamic_dropdowns,
22
+ },
23
+
24
+ // If you want your searches to show up, you better include it here!
25
+ searches: {},
26
+
27
+ // If you want your creates to show up, you better include it here!
28
+ creates: {
29
+ ...creates,
30
+ },
31
+
32
+ resources: {},
33
+ };
@@ -0,0 +1,50 @@
1
+ /* eslint-disable camelcase */
2
+ // This function runs after every outbound request. You can use it to check for
3
+ // errors or modify the response. You can have as many as you need. They'll need
4
+ // to each be registered in your index.js file.
5
+ const handleBadResponses = (response, z, bundle) => {
6
+ if (response.data.error) {
7
+ throw new z.errors.Error(
8
+ response.data.error.message,
9
+ response.data.error.code,
10
+ response.status,
11
+ );
12
+ }
13
+
14
+ return response;
15
+ };
16
+
17
+ const includeOrgId = (request, z, bundle) => {
18
+ const { organization_id } = bundle.authData;
19
+ if (organization_id) {
20
+ request.headers['OpenAI-Organization'] = organization_id;
21
+ }
22
+ return request;
23
+ };
24
+
25
+ // This function runs before every outbound request. You can have as many as you
26
+ // need. They'll need to each be registered in your index.js file.
27
+ const includeApiKey = (request, z, bundle) => {
28
+ const { api_key } = bundle.authData;
29
+ if (api_key) {
30
+ // Use these lines to include the API key in the querystring
31
+ // request.params = request.params || {};
32
+ // request.params.api_key = api_key;
33
+
34
+ // If you want to include the API key in the header:
35
+ request.headers.Authorization = `Bearer ${api_key}`;
36
+ }
37
+
38
+ return request;
39
+ };
40
+
41
+ const jsonHeaders = (request) => {
42
+ request.headers['Content-Type'] = 'application/json';
43
+ request.headers.Accept = 'application/json';
44
+ return request;
45
+ };
46
+
47
+ module.exports = {
48
+ befores: [includeApiKey, includeOrgId, jsonHeaders],
49
+ afters: [handleBadResponses],
50
+ };
@@ -0,0 +1,29 @@
1
+ {
2
+ "id": "chatcmpl-123",
3
+ "object": "chat.completion",
4
+ "created": 1677652288,
5
+ "model": "gpt-4o-mini",
6
+ "system_fingerprint": "fp_44709d6fcb",
7
+ "choices": [
8
+ {
9
+ "index": 0,
10
+ "message": {
11
+ "role": "assistant",
12
+ "content": "\n\nHello there, how may I assist you today?"
13
+ },
14
+ "logprobs": null,
15
+ "finish_reason": "stop"
16
+ }
17
+ ],
18
+ "service_tier": "default",
19
+ "usage": {
20
+ "prompt_tokens": 9,
21
+ "completion_tokens": 12,
22
+ "total_tokens": 21,
23
+ "completion_tokens_details": {
24
+ "reasoning_tokens": 0,
25
+ "accepted_prediction_tokens": 0,
26
+ "rejected_prediction_tokens": 0
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,66 @@
1
+ /* globals describe, it, expect */
2
+ /* eslint-disable no-undef */
3
+
4
+ const App = require('../index');
5
+
6
+ describe('custom auth', () => {
7
+ beforeEach(() => {
8
+ jest.clearAllMocks();
9
+ });
10
+
11
+ it('passes authentication and returns json', async () => {
12
+ const bundle = {
13
+ authData: {
14
+ api_key: 'secret',
15
+ },
16
+ };
17
+
18
+ // Mock successful response
19
+ const mockResponse = {
20
+ status: 200,
21
+ data: {
22
+ email: 'test@example.com',
23
+ },
24
+ };
25
+
26
+ // Mock the request client
27
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
28
+ const z = {
29
+ request: mockRequest,
30
+ };
31
+
32
+ const response = await App.authentication.test(z, bundle);
33
+
34
+ expect(mockRequest).toHaveBeenCalledTimes(1);
35
+ expect(mockRequest).toHaveBeenCalledWith({
36
+ url: expect.stringContaining('/me'),
37
+ });
38
+ expect(response.status).toBe(200);
39
+ expect(response.data).toHaveProperty('email', 'test@example.com');
40
+ });
41
+
42
+ it('fails on bad auth', async () => {
43
+ const bundle = {
44
+ authData: {
45
+ api_key: 'bad',
46
+ },
47
+ };
48
+
49
+ // Mock failed response
50
+ const mockRequest = jest
51
+ .fn()
52
+ .mockRejectedValue(new Error('Incorrect API key provided'));
53
+ const z = {
54
+ request: mockRequest,
55
+ };
56
+
57
+ try {
58
+ await App.authentication.test(z, bundle);
59
+ } catch (error) {
60
+ expect(mockRequest).toHaveBeenCalledTimes(1);
61
+ expect(error.message).toContain('Incorrect API key provided');
62
+ return;
63
+ }
64
+ throw new Error('appTester should have thrown');
65
+ });
66
+ });
@@ -0,0 +1,150 @@
1
+ /* globals describe, it, expect */
2
+ /* eslint-disable no-undef */
3
+
4
+ const chatCompletion = require('../creates/chat_completion');
5
+ const { DEFAULT_MODEL } = require('../constants');
6
+
7
+ describe('chat_completion', () => {
8
+ beforeEach(() => {
9
+ jest.clearAllMocks();
10
+ });
11
+
12
+ it('creates a basic chat completion', async () => {
13
+ const bundle = {
14
+ inputData: {
15
+ user_message: 'Hello, how are you?',
16
+ model: DEFAULT_MODEL,
17
+ },
18
+ };
19
+
20
+ const mockResponse = {
21
+ data: {
22
+ id: 'chatcmpl-123',
23
+ model: DEFAULT_MODEL,
24
+ usage: {
25
+ prompt_tokens: 20,
26
+ completion_tokens: 15,
27
+ total_tokens: 35,
28
+ },
29
+ choices: [
30
+ {
31
+ message: {
32
+ content: 'I am doing well, thank you for asking!',
33
+ },
34
+ },
35
+ ],
36
+ },
37
+ };
38
+
39
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
40
+ const z = { request: mockRequest };
41
+
42
+ const result = await chatCompletion.operation.perform(z, bundle);
43
+
44
+ expect(mockRequest).toHaveBeenCalledTimes(1);
45
+ expect(mockRequest).toHaveBeenCalledWith({
46
+ url: expect.stringContaining('/chat/completions'),
47
+ method: 'POST',
48
+ body: expect.stringContaining(bundle.inputData.user_message),
49
+ });
50
+
51
+ expect(result).toEqual(mockResponse.data);
52
+ });
53
+
54
+ it('creates a chat completion with advanced options', async () => {
55
+ const bundle = {
56
+ inputData: {
57
+ user_message: 'Write a story',
58
+ model: DEFAULT_MODEL,
59
+ developer_message: 'You are a creative writer',
60
+ temperature: 0.9,
61
+ max_completion_tokens: 100,
62
+ },
63
+ };
64
+
65
+ const mockResponse = {
66
+ data: {
67
+ id: 'chatcmpl-456',
68
+ model: DEFAULT_MODEL,
69
+ usage: {
70
+ prompt_tokens: 25,
71
+ completion_tokens: 50,
72
+ total_tokens: 75,
73
+ },
74
+ },
75
+ };
76
+
77
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
78
+ const z = { request: mockRequest };
79
+
80
+ const result = await chatCompletion.operation.perform(z, bundle);
81
+
82
+ expect(mockRequest).toHaveBeenCalledTimes(1);
83
+ expect(mockRequest).toHaveBeenCalledWith({
84
+ url: expect.stringContaining('/chat/completions'),
85
+ method: 'POST',
86
+ body: expect.stringMatching(/temperature.*0.9/),
87
+ });
88
+
89
+ expect(result).toEqual(mockResponse.data);
90
+ });
91
+
92
+ it('creates a chat completion with image', async () => {
93
+ const bundle = {
94
+ inputData: {
95
+ user_message: 'Describe this image',
96
+ model: DEFAULT_MODEL,
97
+ files: ['https://example.com/image.jpg'],
98
+ },
99
+ };
100
+
101
+ const mockResponse = {
102
+ data: {
103
+ id: 'chatcmpl-789',
104
+ model: DEFAULT_MODEL,
105
+ usage: {
106
+ prompt_tokens: 30,
107
+ completion_tokens: 20,
108
+ total_tokens: 50,
109
+ },
110
+ },
111
+ };
112
+
113
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
114
+ const z = { request: mockRequest };
115
+
116
+ const result = await chatCompletion.operation.perform(z, bundle);
117
+
118
+ expect(mockRequest).toHaveBeenCalledTimes(1);
119
+ expect(mockRequest).toHaveBeenCalledWith({
120
+ url: expect.stringContaining('/chat/completions'),
121
+ method: 'POST',
122
+ body: expect.stringMatching(/image_url.*example.com/),
123
+ });
124
+
125
+ expect(result).toEqual(mockResponse.data);
126
+ });
127
+
128
+ it('handles API errors', async () => {
129
+ const bundle = {
130
+ inputData: {
131
+ user_message: 'Hello',
132
+ model: DEFAULT_MODEL,
133
+ },
134
+ };
135
+
136
+ const mockRequest = jest
137
+ .fn()
138
+ .mockRejectedValue(new Error('Invalid request'));
139
+ const z = { request: mockRequest };
140
+
141
+ try {
142
+ await chatCompletion.operation.perform(z, bundle);
143
+ } catch (error) {
144
+ expect(mockRequest).toHaveBeenCalledTimes(1);
145
+ expect(error.message).toContain('Invalid request');
146
+ return;
147
+ }
148
+ throw new Error('Should have thrown an error');
149
+ });
150
+ });
@@ -0,0 +1,51 @@
1
+ /* globals describe, it, expect */
2
+ /* eslint-disable no-undef */
3
+
4
+ const listModels = require('../dynamic_dropdowns/list_models');
5
+
6
+ describe('list_models', () => {
7
+ beforeEach(() => {
8
+ jest.clearAllMocks();
9
+ });
10
+
11
+ it('returns formatted list of models', async () => {
12
+ const mockResponse = {
13
+ data: {
14
+ data: [{ id: 'gpt-4' }, { id: 'gpt-3.5-turbo' }],
15
+ },
16
+ };
17
+
18
+ const mockRequest = jest.fn().mockResolvedValue(mockResponse);
19
+ const z = { request: mockRequest };
20
+ const bundle = {};
21
+
22
+ const results = await listModels.operation.perform(z, bundle);
23
+
24
+ expect(mockRequest).toHaveBeenCalledTimes(1);
25
+ expect(mockRequest).toHaveBeenCalledWith({
26
+ url: expect.stringContaining('/models'),
27
+ });
28
+
29
+ expect(results).toEqual([
30
+ { id: 'gpt-4', name: 'gpt-4' },
31
+ { id: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo' },
32
+ ]);
33
+ });
34
+
35
+ it('handles API errors', async () => {
36
+ const mockRequest = jest
37
+ .fn()
38
+ .mockRejectedValue(new Error('Failed to fetch models'));
39
+ const z = { request: mockRequest };
40
+ const bundle = {};
41
+
42
+ try {
43
+ await listModels.operation.perform(z, bundle);
44
+ } catch (error) {
45
+ expect(mockRequest).toHaveBeenCalledTimes(1);
46
+ expect(error.message).toContain('Failed to fetch models');
47
+ return;
48
+ }
49
+ throw new Error('Should have thrown an error');
50
+ });
51
+ });
@@ -18,7 +18,7 @@ class ZapierBaseCommand extends Command {
18
18
 
19
19
  if (this.flags.debug) {
20
20
  this.debug.enabled = true; // enables this.debug on the command
21
- require('debug').enable('zapier:*'); // enables all further spawned functions, like API
21
+ require('debug').enable('zapier:*,oclif:zapier:*'); // enables all further spawned functions, like API
22
22
  }
23
23
 
24
24
  this.debug('argv is', this.argv);
@@ -1,15 +1,38 @@
1
1
  const BaseCommand = require('../ZapierBaseCommand');
2
- const { Args } = require('@oclif/core');
2
+ const { Args, Flags } = require('@oclif/core');
3
3
  const { buildFlags } = require('../buildFlags');
4
+ const colors = require('colors/safe');
4
5
 
5
- const { callAPI } = require('../../utils/api');
6
+ const { callAPI, getSpecificVersionInfo } = require('../../utils/api');
6
7
 
7
8
  class DeprecateCommand extends BaseCommand {
8
9
  async perform() {
9
10
  const app = await this.getWritableApp();
10
11
  const { version, date } = this.args;
12
+
13
+ const versionInfo = await getSpecificVersionInfo(version);
14
+ const hasActiveUsers = versionInfo.user_count && versionInfo.user_count > 0;
15
+
16
+ this.log(
17
+ `${colors.yellow('Warning: Deprecation is an irreversible action that will eventually block access to this version.')}\n` +
18
+ `${colors.yellow('If all your changes are non-breaking, use `zapier migrate` instead to move users over to a newer version.')}\n`,
19
+ );
20
+
21
+ if (
22
+ !this.flags.force &&
23
+ !(await this.confirm(
24
+ 'Are you sure you want to deprecate this version? This will notify users that their Zaps or other automations will stop working after the specified date.' +
25
+ (hasActiveUsers
26
+ ? `\n\nThis version has ${versionInfo.user_count} active user(s). Strongly consider migrating users to another version before deprecating!`
27
+ : ''),
28
+ ))
29
+ ) {
30
+ this.log('\nDeprecation cancelled.');
31
+ return;
32
+ }
33
+
11
34
  this.log(
12
- `Preparing to deprecate version ${version} your app "${app.title}".\n`,
35
+ `\nPreparing to deprecate version ${version} your app "${app.title}".\n`,
13
36
  );
14
37
  const url = `/apps/${app.id}/versions/${version}/deprecate`;
15
38
  this.startSpinner(`Deprecating ${version}`);
@@ -21,12 +44,19 @@ class DeprecateCommand extends BaseCommand {
21
44
  });
22
45
  this.stopSpinner();
23
46
  this.log(
24
- `\nWe'll let users know that this version is no longer recommended and will cease to work on ${date}.`,
47
+ `\nWe'll let users know that this version will cease to work on ${date}.`,
25
48
  );
26
49
  }
27
50
  }
28
51
 
29
- DeprecateCommand.flags = buildFlags();
52
+ DeprecateCommand.flags = buildFlags({
53
+ commandFlags: {
54
+ force: Flags.boolean({
55
+ char: 'f',
56
+ description: 'Skip confirmation prompt. Use with caution.',
57
+ }),
58
+ },
59
+ });
30
60
  DeprecateCommand.args = {
31
61
  version: Args.string({
32
62
  description: 'The version to deprecate.',
@@ -43,11 +73,15 @@ DeprecateCommand.description = `Mark a non-production version of your integratio
43
73
 
44
74
  Use this when an integration version will not be supported or start breaking at a known date.
45
75
 
46
- Zapier will send an email warning users of the deprecation once a date is set, they'll start seeing it as "Deprecated" in the UI, and once the deprecation date arrives, if the Zaps weren't updated, they'll be paused and the users will be emailed again explaining what happened.
76
+ Zapier will immediately send emails warning users of the deprecation if a date less than 30 days in the future is set, otherwise the emails will be sent exactly 30 days before the configured deprecation date.
47
77
 
48
- After the deprecation date has passed it will be safe to delete that integration version.
78
+ There are other side effects: they'll start seeing it as "Deprecated" in the UI, and once the deprecation date arrives, if the Zaps weren't updated, they'll be paused and the users will be emailed again explaining what happened.
49
79
 
50
- Do not use this if you have non-breaking changes, such as fixing help text.`;
80
+ Do not use deprecation if you only have non-breaking changes, such as:
81
+ - Fixing help text
82
+ - Adding new triggers/actions
83
+ - Improving existing functionality
84
+ - other bug fixes that don't break existing automations.`;
51
85
  DeprecateCommand.skipValidInstallCheck = true;
52
86
 
53
87
  module.exports = DeprecateCommand;