@zapier/zapier-sdk 0.5.2 → 0.6.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +107 -83
  3. package/dist/index.cjs +320 -50
  4. package/dist/index.d.mts +409 -340
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +0 -1
  7. package/dist/index.mjs +320 -50
  8. package/dist/plugins/api/index.js +1 -1
  9. package/dist/plugins/findFirstAuthentication/index.d.ts.map +1 -1
  10. package/dist/plugins/findFirstAuthentication/index.js +1 -0
  11. package/dist/plugins/findUniqueAuthentication/index.d.ts.map +1 -1
  12. package/dist/plugins/findUniqueAuthentication/index.js +1 -0
  13. package/dist/plugins/getAction/index.d.ts.map +1 -1
  14. package/dist/plugins/getAction/index.js +1 -0
  15. package/dist/plugins/getAction/index.test.js +1 -1
  16. package/dist/plugins/getApp/index.d.ts +6 -3
  17. package/dist/plugins/getApp/index.d.ts.map +1 -1
  18. package/dist/plugins/getApp/index.js +8 -18
  19. package/dist/plugins/getApp/index.test.js +2 -0
  20. package/dist/plugins/getAuthentication/index.d.ts.map +1 -1
  21. package/dist/plugins/getAuthentication/index.js +1 -0
  22. package/dist/plugins/getAuthentication/index.test.js +12 -1
  23. package/dist/plugins/getProfile/index.d.ts.map +1 -1
  24. package/dist/plugins/getProfile/index.js +1 -0
  25. package/dist/plugins/listActions/index.d.ts +5 -3
  26. package/dist/plugins/listActions/index.d.ts.map +1 -1
  27. package/dist/plugins/listActions/index.js +6 -6
  28. package/dist/plugins/listActions/index.test.js +26 -74
  29. package/dist/plugins/listActions/schemas.d.ts +4 -4
  30. package/dist/plugins/listApps/index.d.ts.map +1 -1
  31. package/dist/plugins/listApps/index.js +1 -0
  32. package/dist/plugins/listApps/schemas.d.ts +2 -2
  33. package/dist/plugins/listAuthentications/index.d.ts +4 -2
  34. package/dist/plugins/listAuthentications/index.d.ts.map +1 -1
  35. package/dist/plugins/listAuthentications/index.js +9 -12
  36. package/dist/plugins/listAuthentications/index.test.js +33 -40
  37. package/dist/plugins/listAuthentications/schemas.d.ts +4 -4
  38. package/dist/plugins/listInputFields/index.d.ts +3 -1
  39. package/dist/plugins/listInputFields/index.d.ts.map +1 -1
  40. package/dist/plugins/listInputFields/index.js +5 -5
  41. package/dist/plugins/listInputFields/index.test.js +10 -8
  42. package/dist/plugins/listInputFields/schemas.d.ts +4 -4
  43. package/dist/plugins/lockVersion/index.d.ts +24 -0
  44. package/dist/plugins/lockVersion/index.d.ts.map +1 -0
  45. package/dist/plugins/lockVersion/index.js +72 -0
  46. package/dist/plugins/lockVersion/index.test.d.ts +2 -0
  47. package/dist/plugins/lockVersion/index.test.d.ts.map +1 -0
  48. package/dist/plugins/lockVersion/index.test.js +129 -0
  49. package/dist/plugins/lockVersion/schemas.d.ts +10 -0
  50. package/dist/plugins/lockVersion/schemas.d.ts.map +1 -0
  51. package/dist/plugins/lockVersion/schemas.js +6 -0
  52. package/dist/plugins/manifest/index.d.ts +24 -0
  53. package/dist/plugins/manifest/index.d.ts.map +1 -0
  54. package/dist/plugins/manifest/index.js +119 -0
  55. package/dist/plugins/manifest/index.test.d.ts +2 -0
  56. package/dist/plugins/manifest/index.test.d.ts.map +1 -0
  57. package/dist/plugins/manifest/index.test.js +331 -0
  58. package/dist/plugins/manifest/schemas.d.ts +64 -0
  59. package/dist/plugins/manifest/schemas.d.ts.map +1 -0
  60. package/dist/plugins/manifest/schemas.js +25 -0
  61. package/dist/plugins/registry/index.d.ts +9 -1
  62. package/dist/plugins/registry/index.d.ts.map +1 -1
  63. package/dist/plugins/registry/index.js +68 -3
  64. package/dist/plugins/request/index.d.ts.map +1 -1
  65. package/dist/plugins/request/index.js +1 -0
  66. package/dist/plugins/request/index.test.js +6 -1
  67. package/dist/plugins/request/schemas.d.ts +4 -4
  68. package/dist/plugins/runAction/index.d.ts +2 -0
  69. package/dist/plugins/runAction/index.d.ts.map +1 -1
  70. package/dist/plugins/runAction/index.js +5 -5
  71. package/dist/plugins/runAction/index.test.js +9 -8
  72. package/dist/plugins/runAction/schemas.d.ts +4 -4
  73. package/dist/sdk.d.ts +3 -3
  74. package/dist/sdk.d.ts.map +1 -1
  75. package/dist/sdk.js +18 -7
  76. package/dist/sdk.test.js +1 -1
  77. package/dist/types/plugin.d.ts +10 -2
  78. package/dist/types/plugin.d.ts.map +1 -1
  79. package/dist/types/sdk.d.ts +13 -2
  80. package/dist/types/sdk.d.ts.map +1 -1
  81. package/package.json +1 -1
  82. package/src/index.ts +0 -2
  83. package/src/plugins/api/index.ts +1 -1
  84. package/src/plugins/findFirstAuthentication/index.ts +1 -0
  85. package/src/plugins/findUniqueAuthentication/index.ts +1 -0
  86. package/src/plugins/getAction/index.test.ts +1 -1
  87. package/src/plugins/getAction/index.ts +1 -0
  88. package/src/plugins/getApp/index.test.ts +2 -0
  89. package/src/plugins/getApp/index.ts +12 -24
  90. package/src/plugins/getAuthentication/index.test.ts +13 -3
  91. package/src/plugins/getAuthentication/index.ts +1 -0
  92. package/src/plugins/getProfile/index.ts +1 -0
  93. package/src/plugins/listActions/index.test.ts +30 -89
  94. package/src/plugins/listActions/index.ts +13 -9
  95. package/src/plugins/listApps/index.ts +1 -0
  96. package/src/plugins/listAuthentications/index.test.ts +38 -47
  97. package/src/plugins/listAuthentications/index.ts +21 -18
  98. package/src/plugins/listInputFields/index.test.ts +12 -9
  99. package/src/plugins/listInputFields/index.ts +10 -6
  100. package/src/plugins/lockVersion/index.test.ts +176 -0
  101. package/src/plugins/lockVersion/index.ts +112 -0
  102. package/src/plugins/lockVersion/schemas.ts +9 -0
  103. package/src/plugins/manifest/index.test.ts +439 -0
  104. package/src/plugins/manifest/index.ts +171 -0
  105. package/src/plugins/manifest/schemas.ts +53 -0
  106. package/src/plugins/registry/index.ts +89 -8
  107. package/src/plugins/request/index.test.ts +8 -4
  108. package/src/plugins/request/index.ts +1 -0
  109. package/src/plugins/runAction/index.test.ts +9 -8
  110. package/src/plugins/runAction/index.ts +13 -7
  111. package/src/sdk.test.ts +1 -1
  112. package/src/sdk.ts +22 -7
  113. package/src/types/plugin.ts +14 -2
  114. package/src/types/sdk.ts +15 -1
  115. package/tsconfig.tsbuildinfo +1 -1
@@ -12,7 +12,8 @@ import {
12
12
  ZapierAuthenticationError,
13
13
  } from "../../types/errors";
14
14
  import { createPaginatedFunction } from "../../utils/function-utils";
15
- import type { GetAppPluginProvides } from "../getApp";
15
+ import { ManifestPluginProvides } from "../manifest";
16
+ import { GetVersionedImplementationId } from "../manifest/schemas";
16
17
 
17
18
  export interface ListActionsPluginProvides {
18
19
  listActions: (options?: ListActionsOptions) => Promise<{
@@ -31,20 +32,22 @@ export interface ListActionsPluginProvides {
31
32
  }
32
33
 
33
34
  export const listActionsPlugin: Plugin<
34
- GetSdkType<GetAppPluginProvides>, // requires getApp in SDK
35
- { api: ApiClient }, // requires api in context
35
+ GetSdkType<ManifestPluginProvides>, // requires getApp in SDK
36
+ {
37
+ api: ApiClient;
38
+ getVersionedImplementationId: GetVersionedImplementationId;
39
+ }, // requires api and getVersionedImplementationId in context
36
40
  ListActionsPluginProvides
37
- > = ({ sdk, context }) => {
41
+ > = ({ context }) => {
38
42
  const listActions = createPaginatedFunction(async function listActionsPage(
39
43
  options: ListActionsOptions & { cursor?: string } & { pageSize: number },
40
44
  ): Promise<ListActionsPage> {
41
- const { api } = context;
45
+ const { api, getVersionedImplementationId } = context;
42
46
 
43
47
  // Use the getApp function from the SDK (dependency injection)
44
- const app = await sdk.getApp({ appKey: options.appKey });
48
+ const selectedApi = await getVersionedImplementationId(options.appKey);
45
49
 
46
- const implementationId = app.data.current_implementation_id?.split("@")[0];
47
- if (!implementationId) {
50
+ if (!selectedApi) {
48
51
  throw new ZapierConfigurationError(
49
52
  "No current_implementation_id found for app",
50
53
  { configType: "current_implementation_id" },
@@ -54,7 +57,7 @@ export const listActionsPlugin: Plugin<
54
57
  const searchParams: Record<string, string> = {
55
58
  global: "true",
56
59
  public_only: "true",
57
- selected_apis: implementationId,
60
+ selected_apis: selectedApi,
58
61
  };
59
62
 
60
63
  const data = await api.get<{ results: Implementation[] }>(
@@ -110,6 +113,7 @@ export const listActionsPlugin: Plugin<
110
113
  context: {
111
114
  meta: {
112
115
  listActions: {
116
+ categories: ["action"],
113
117
  inputSchema: ListActionsSchema,
114
118
  },
115
119
  },
@@ -113,6 +113,7 @@ export const listAppsPlugin: Plugin<
113
113
  context: {
114
114
  meta: {
115
115
  listApps: {
116
+ categories: ["app"],
116
117
  inputSchema: ListAppsSchema,
117
118
  },
118
119
  },
@@ -6,7 +6,7 @@ import {
6
6
  import { listAuthenticationsPlugin } from "./index";
7
7
  import { createSdk } from "../../sdk";
8
8
  import type { ApiClient } from "../../api";
9
- import type { App } from "../../api/types";
9
+ import type { AppItem } from "../../types/domain";
10
10
 
11
11
  const mockAuthenticationsResponse = {
12
12
  results: [
@@ -42,29 +42,18 @@ const mockAuthenticationsResponse = {
42
42
  previous: null,
43
43
  };
44
44
 
45
- const mockSlackApp = {
46
- data: {
47
- id: 1,
48
- key: "slack",
49
- title: "Slack",
50
- hex_color: "#000000",
51
- age_in_days: "100",
52
- api_docs_url: "https://slack.com/api-docs",
53
- app_profile_url: "https://slack.com",
54
- banner: "https://slack.com/banner.png",
55
- categories: [],
56
- canonical_id: "slack",
57
- zap_usage_count: 100,
58
- zap_usage_count_last_updated: "2021-01-01",
59
- zap_usage_count_last_updated_by: "user1",
60
- zap_usage_count_last_updated_by_id: 123,
61
- current_implementation_id: "SlackCLIAPI@1.21.1",
62
- } as Partial<App> as App,
45
+ const mockSlackApp: AppItem = {
46
+ title: "Slack",
47
+ key: "SlackCLIAPI",
48
+ current_implementation_id: "SlackCLIAPI@1.21.1",
49
+ version: "1.21.1",
50
+ description: "Team communication platform",
51
+ slug: "slack",
63
52
  };
64
53
 
65
54
  describe("listAuthentications plugin", () => {
66
55
  let mockApiClient: ApiClient;
67
- let mockGetApp: any;
56
+ let mockGetVersionedImplementationId: any;
68
57
 
69
58
  beforeEach(() => {
70
59
  vi.clearAllMocks();
@@ -72,14 +61,34 @@ describe("listAuthentications plugin", () => {
72
61
  get: vi.fn().mockResolvedValue(mockAuthenticationsResponse),
73
62
  } as Partial<ApiClient> as ApiClient;
74
63
 
75
- mockGetApp = vi.fn().mockResolvedValue(mockSlackApp);
64
+ mockGetVersionedImplementationId = vi
65
+ .fn()
66
+ .mockResolvedValue("SlackCLIAPI@1.21.1");
67
+ });
68
+
69
+ const apiPlugin = () => ({
70
+ context: {
71
+ api: mockApiClient,
72
+ },
73
+ });
74
+
75
+ const manifestPlugin = () => ({
76
+ context: {
77
+ manifest: null,
78
+ getVersionedImplementationId: mockGetVersionedImplementationId,
79
+ getManifestEntry: () => ({
80
+ implementationName: "SlackCLIAPI",
81
+ version: "1.21.1",
82
+ }),
83
+ getImplementation: vi.fn().mockResolvedValue(mockSlackApp),
84
+ },
76
85
  });
77
86
 
78
87
  function createTestSdk() {
79
- return createSdk(
80
- { getApp: mockGetApp }, // Provide getApp in SDK
81
- { api: mockApiClient, meta: {} },
82
- ).addPlugin(listAuthenticationsPlugin as any);
88
+ return createSdk()
89
+ .addPlugin(apiPlugin)
90
+ .addPlugin(manifestPlugin)
91
+ .addPlugin(listAuthenticationsPlugin);
83
92
  }
84
93
 
85
94
  describe("schema validation", () => {
@@ -129,7 +138,7 @@ describe("listAuthentications plugin", () => {
129
138
  const sdk = createTestSdk();
130
139
  expect(() => {
131
140
  sdk.listAuthentications({
132
- account_id: 123,
141
+ account_id: 123 as any,
133
142
  });
134
143
  }).toThrow(ZapierValidationError);
135
144
  });
@@ -192,7 +201,7 @@ describe("listAuthentications plugin", () => {
192
201
  describe("data mapping", () => {
193
202
  it("should map is_stale to is_expired and marked_stale_at to expired_at", async () => {
194
203
  const sdk = createTestSdk();
195
- const result = await sdk.listAuthentications({});
204
+ const result = await sdk.listAuthentications();
196
205
 
197
206
  expect(result.data[0].is_expired).toBe("false");
198
207
  expect(result.data[0].expired_at).toBe(null);
@@ -582,16 +591,7 @@ describe("listAuthentications plugin", () => {
582
591
 
583
592
  describe("app key integration", () => {
584
593
  it("should handle app without current_implementation_id", async () => {
585
- const appWithoutImplementation = {
586
- data: {
587
- id: 123,
588
- key: "slack",
589
- title: "Slack",
590
- current_implementation_id: undefined,
591
- } as Partial<App> as App,
592
- };
593
-
594
- mockGetApp.mockResolvedValue(appWithoutImplementation);
594
+ mockGetVersionedImplementationId.mockResolvedValue(null);
595
595
 
596
596
  const sdk = createTestSdk();
597
597
  await sdk.listAuthentications({ appKey: "slack" });
@@ -608,16 +608,7 @@ describe("listAuthentications plugin", () => {
608
608
  });
609
609
 
610
610
  it("should extract versionless API from versioned implementation ID", async () => {
611
- const appWithVersionedApi = {
612
- data: {
613
- id: 123,
614
- key: "slack",
615
- title: "Slack",
616
- current_implementation_id: "SlackCLIAPI@2.1.3",
617
- } as Partial<App> as App,
618
- };
619
-
620
- mockGetApp.mockResolvedValue(appWithVersionedApi);
611
+ mockGetVersionedImplementationId.mockResolvedValue("SlackCLIAPI@2.1.3");
621
612
 
622
613
  const sdk = createTestSdk();
623
614
  await sdk.listAuthentications({ appKey: "slack" });
@@ -7,13 +7,17 @@ import {
7
7
  type ListAuthenticationsOptions,
8
8
  type ListAuthenticationsPage,
9
9
  } from "./schemas";
10
- import { normalizeAuthenticationItem } from "../../utils/domain-utils";
10
+ import {
11
+ normalizeAuthenticationItem,
12
+ splitVersionedKey,
13
+ } from "../../utils/domain-utils";
11
14
  import { ZapierAuthenticationError } from "../../types/errors";
12
15
  import {
13
16
  createPaginatedFunction,
14
17
  extractCursor,
15
18
  } from "../../utils/function-utils";
16
- import type { GetAppPluginProvides } from "../getApp";
19
+ import { GetVersionedImplementationId } from "../manifest/schemas";
20
+ import { ManifestPluginProvides } from "../manifest";
17
21
 
18
22
  export interface ListAuthenticationsPluginProvides {
19
23
  listAuthentications: (options?: ListAuthenticationsOptions) => Promise<{
@@ -32,33 +36,31 @@ export interface ListAuthenticationsPluginProvides {
32
36
  }
33
37
 
34
38
  export const listAuthenticationsPlugin: Plugin<
35
- GetSdkType<GetAppPluginProvides>, // requires getApp in SDK
36
- { api: ApiClient }, // requires api in context
39
+ GetSdkType<ManifestPluginProvides>, // requires getApp in SDK
40
+ {
41
+ api: ApiClient;
42
+ getVersionedImplementationId: GetVersionedImplementationId;
43
+ }, // requires api in context
37
44
  ListAuthenticationsPluginProvides
38
- > = ({ sdk, context }) => {
45
+ > = ({ context }) => {
39
46
  const listAuthentications = createPaginatedFunction(
40
47
  async function listAuthenticationsPage(
41
48
  options: ListAuthenticationsOptions & { cursor?: string } & {
42
49
  pageSize: number;
43
50
  },
44
51
  ): Promise<ListAuthenticationsPage> {
45
- const { api } = context;
46
-
52
+ const { api, getVersionedImplementationId } = context;
47
53
  // Build search parameters
48
54
  const searchParams: Record<string, string> = {};
49
55
 
50
56
  // Handle appKey filtering by getting the selected_api first
51
57
  if (options.appKey) {
52
- const app = await sdk.getApp({
53
- appKey: options.appKey,
54
- });
55
-
56
- const selectedApi = app.data.current_implementation_id;
57
- if (selectedApi) {
58
- // Use versionless_selected_api to find auths across all app versions
59
- // Extract the base name without the version (e.g., "SlackCLIAPI" from "SlackCLIAPI@1.21.1")
60
- const versionlessApi = selectedApi.split("@")[0];
61
- searchParams.versionless_selected_api = versionlessApi;
58
+ const implementationId = await getVersionedImplementationId(
59
+ options.appKey,
60
+ );
61
+ if (implementationId) {
62
+ const [versionlessSelectedApi] = splitVersionedKey(implementationId);
63
+ searchParams.versionless_selected_api = versionlessSelectedApi;
62
64
  }
63
65
  }
64
66
 
@@ -81,7 +83,7 @@ export const listAuthenticationsPlugin: Plugin<
81
83
  // Convert cursor back to offset for the API
82
84
  searchParams.offset = options.cursor;
83
85
  }
84
-
86
+ console.log({ searchParams });
85
87
  const data: AuthenticationsResponse = await api.get(
86
88
  "/api/v4/authentications/",
87
89
  {
@@ -128,6 +130,7 @@ export const listAuthenticationsPlugin: Plugin<
128
130
  context: {
129
131
  meta: {
130
132
  listAuthentications: {
133
+ categories: ["authentication"],
131
134
  inputSchema: ListAuthenticationsSchema,
132
135
  },
133
136
  },
@@ -47,7 +47,7 @@ const mockNeedsResponse: NeedsResponse = {
47
47
 
48
48
  describe("listInputFields plugin", () => {
49
49
  let mockApiClient: ApiClient;
50
- let mockGetApp: any;
50
+ let mockGetVersionedImplementationId: any;
51
51
 
52
52
  beforeEach(() => {
53
53
  vi.clearAllMocks();
@@ -55,15 +55,20 @@ describe("listInputFields plugin", () => {
55
55
  post: vi.fn().mockResolvedValue(mockNeedsResponse),
56
56
  } as Partial<ApiClient> as ApiClient;
57
57
 
58
- mockGetApp = vi.fn().mockResolvedValue({
59
- data: { current_implementation_id: "SlackCLIAPI@1.21.1" },
60
- });
58
+ mockGetVersionedImplementationId = vi
59
+ .fn()
60
+ .mockResolvedValue("SlackCLIAPI@1.21.1");
61
61
  });
62
62
 
63
63
  function createTestSdk() {
64
64
  return createSdk(
65
- { getApp: mockGetApp },
66
- { api: mockApiClient, meta: {} },
65
+ {},
66
+ {},
67
+ {
68
+ api: mockApiClient,
69
+ meta: {},
70
+ getVersionedImplementationId: mockGetVersionedImplementationId,
71
+ },
67
72
  ).addPlugin(listInputFieldsPlugin as any);
68
73
  }
69
74
 
@@ -303,9 +308,7 @@ describe("listInputFields plugin", () => {
303
308
 
304
309
  describe("error handling", () => {
305
310
  it("should throw ZapierConfigurationError when app has no current_implementation_id", async () => {
306
- mockGetApp.mockResolvedValue({
307
- data: { current_implementation_id: undefined },
308
- });
311
+ mockGetVersionedImplementationId.mockResolvedValue(null);
309
312
 
310
313
  const sdk = createTestSdk();
311
314
  await expect(
@@ -10,6 +10,7 @@ import {
10
10
  import { ZapierConfigurationError, ZapierApiError } from "../../types/errors";
11
11
  import { createPaginatedFunction } from "../../utils/function-utils";
12
12
  import type { GetAppPluginProvides } from "../getApp";
13
+ import { GetVersionedImplementationId } from "../manifest/schemas";
13
14
 
14
15
  // Enums for input field transformation
15
16
  enum InputFieldType {
@@ -125,9 +126,12 @@ export interface ListInputFieldsPluginProvides {
125
126
 
126
127
  export const listInputFieldsPlugin: Plugin<
127
128
  GetSdkType<GetAppPluginProvides>, // requires getApp in SDK
128
- { api: ApiClient }, // requires api in context
129
+ {
130
+ api: ApiClient;
131
+ getVersionedImplementationId: GetVersionedImplementationId;
132
+ }, // requires api and getVersionedImplementationId in context
129
133
  ListInputFieldsPluginProvides
130
- > = ({ sdk, context }) => {
134
+ > = ({ context }) => {
131
135
  const listInputFields = createPaginatedFunction(
132
136
  async function listInputFieldsPage(
133
137
  options: ListInputFieldsOptions & { cursor?: string } & {
@@ -135,15 +139,14 @@ export const listInputFieldsPlugin: Plugin<
135
139
  },
136
140
  ): Promise<ListInputFieldsPage> {
137
141
  // Note: This function ignores pageSize and cursor since it's not actually paginated internally
138
- const { api } = context;
142
+ const { api, getVersionedImplementationId } = context;
139
143
 
140
144
  // Extract parameters
141
145
  const { appKey, actionKey, actionType, authenticationId, inputs } =
142
146
  options;
143
147
 
144
- // Use the getApp plugin
145
- const appData = await sdk.getApp({ appKey });
146
- const selectedApi = appData.data.current_implementation_id;
148
+ // Use the manifest plugin
149
+ const selectedApi = await getVersionedImplementationId(appKey);
147
150
 
148
151
  if (!selectedApi) {
149
152
  throw new ZapierConfigurationError(
@@ -196,6 +199,7 @@ export const listInputFieldsPlugin: Plugin<
196
199
  context: {
197
200
  meta: {
198
201
  listInputFields: {
202
+ categories: ["action"],
199
203
  inputSchema: ListInputFieldsSchema,
200
204
  },
201
205
  },
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { lockVersionPlugin } from "./index";
3
+ import { createSdk } from "../../sdk";
4
+
5
+ // Mock fs module
6
+ vi.mock("fs", () => ({
7
+ readFileSync: vi.fn(),
8
+ writeFileSync: vi.fn(),
9
+ existsSync: vi.fn(),
10
+ }));
11
+
12
+ // Mock path module
13
+ vi.mock("path", () => ({
14
+ resolve: vi.fn((path) => `/resolved/${path}`),
15
+ }));
16
+
17
+ import { readFileSync, writeFileSync, existsSync } from "fs";
18
+ import { resolve } from "path";
19
+ import { apiPlugin } from "../api";
20
+
21
+ const mockReadFileSync = vi.mocked(readFileSync);
22
+ const mockWriteFileSync = vi.mocked(writeFileSync);
23
+ const mockExistsSync = vi.mocked(existsSync);
24
+ const mockResolve = vi.mocked(resolve);
25
+
26
+ describe("lockVersion plugin", () => {
27
+ let mockSdk: any;
28
+
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+
32
+ mockSdk = {
33
+ listApps: vi.fn().mockReturnValue({
34
+ items: vi.fn().mockReturnValue(
35
+ [
36
+ {
37
+ title: "Gmail",
38
+ key: "GmailCLIAPI",
39
+ current_implementation_id: "GmailCLIAPI@2.1.0",
40
+ description: "Email service",
41
+ },
42
+ ][Symbol.iterator](),
43
+ ),
44
+ }),
45
+ };
46
+ });
47
+
48
+ const listAppsPlugin = () => ({
49
+ listApps: mockSdk.listApps,
50
+ context: {
51
+ meta: {
52
+ listApps: {
53
+ inputSchema: {} as any,
54
+ },
55
+ },
56
+ },
57
+ });
58
+
59
+ function createTestSdk() {
60
+ return createSdk()
61
+ .addPlugin(apiPlugin)
62
+ .addPlugin(listAppsPlugin)
63
+ .addPlugin(lockVersionPlugin);
64
+ }
65
+
66
+ describe("version locking", () => {
67
+ it("should extract implementation name and version and write to file", async () => {
68
+ mockExistsSync.mockReturnValue(false);
69
+ mockResolve.mockReturnValue("/resolved/.zapierrc");
70
+
71
+ const sdk = createTestSdk();
72
+ const result = await sdk.lockVersion({ appKey: "gmail" });
73
+
74
+ expect((result.data as any).implementationName).toBe("GmailCLIAPI");
75
+ expect((result.data as any).version).toBe("2.1.0");
76
+ expect(result.data.current_implementation_id).toBe("GmailCLIAPI@2.1.0");
77
+ expect(result.configPath).toBe("/resolved/.zapierrc");
78
+
79
+ // Verify file was written
80
+ expect(mockWriteFileSync).toHaveBeenCalledWith(
81
+ "/resolved/.zapierrc",
82
+ JSON.stringify(
83
+ {
84
+ apps: {
85
+ gmail: {
86
+ implementationName: "GmailCLIAPI",
87
+ version: "2.1.0",
88
+ },
89
+ },
90
+ },
91
+ null,
92
+ 2,
93
+ ),
94
+ );
95
+ });
96
+
97
+ it("should update existing .zapierrc file", async () => {
98
+ const existingConfig = {
99
+ apps: {
100
+ slack: {
101
+ implementationName: "SlackCLIAPI",
102
+ version: "1.27.1",
103
+ },
104
+ },
105
+ };
106
+
107
+ mockExistsSync.mockReturnValue(true);
108
+ mockReadFileSync.mockReturnValue(JSON.stringify(existingConfig));
109
+ mockResolve.mockReturnValue("/resolved/.zapierrc");
110
+
111
+ const sdk = createTestSdk();
112
+ const result = await sdk.lockVersion({ appKey: "gmail" });
113
+
114
+ expect(mockReadFileSync).toHaveBeenCalledWith(
115
+ "/resolved/.zapierrc",
116
+ "utf8",
117
+ );
118
+ expect(result.configPath).toBe("/resolved/.zapierrc");
119
+
120
+ // Verify file was updated with both apps
121
+ expect(mockWriteFileSync).toHaveBeenCalledWith(
122
+ "/resolved/.zapierrc",
123
+ JSON.stringify(
124
+ {
125
+ apps: {
126
+ slack: {
127
+ implementationName: "SlackCLIAPI",
128
+ version: "1.27.1",
129
+ },
130
+ gmail: {
131
+ implementationName: "GmailCLIAPI",
132
+ version: "2.1.0",
133
+ },
134
+ },
135
+ },
136
+ null,
137
+ 2,
138
+ ),
139
+ );
140
+ });
141
+
142
+ it("should handle invalid implementation ID format", async () => {
143
+ mockSdk.listApps = vi.fn().mockReturnValue({
144
+ items: vi.fn().mockReturnValue(
145
+ [
146
+ {
147
+ title: "Invalid App",
148
+ key: "InvalidApp",
149
+ current_implementation_id: "InvalidFormat",
150
+ },
151
+ ][Symbol.iterator](),
152
+ ),
153
+ });
154
+
155
+ const sdk = createTestSdk();
156
+
157
+ await expect(sdk.lockVersion({ appKey: "invalid" })).rejects.toThrow(
158
+ "Invalid implementation ID format: InvalidFormat. Expected format: <implementationName>@<version>",
159
+ );
160
+ });
161
+
162
+ it("should use custom config path when provided", async () => {
163
+ mockExistsSync.mockReturnValue(false);
164
+ mockResolve.mockReturnValue("/resolved/custom-config.json");
165
+
166
+ const sdk = createTestSdk();
167
+ const result = await sdk.lockVersion({
168
+ appKey: "gmail",
169
+ configPath: "custom-config.json",
170
+ });
171
+
172
+ expect(mockResolve).toHaveBeenCalledWith("custom-config.json");
173
+ expect(result.configPath).toBe("/resolved/custom-config.json");
174
+ });
175
+ });
176
+ });
@@ -0,0 +1,112 @@
1
+ import type { Plugin, GetSdkType } from "../../types/plugin";
2
+ import { createFunction } from "../../utils/function-utils";
3
+ import { LockVersionSchema } from "./schemas";
4
+ import type { LockVersionOptions } from "./schemas";
5
+ import type { AppItem } from "../../types/domain";
6
+ import { readFileSync, writeFileSync, existsSync } from "fs";
7
+ import { resolve } from "path";
8
+ import { z } from "zod";
9
+ import { ListAppsPluginProvides } from "../listApps";
10
+
11
+ export interface LockVersionPluginProvides {
12
+ lockVersion: (
13
+ options: LockVersionOptions & { configPath?: string },
14
+ ) => Promise<{ data: AppItem; configPath: string }>;
15
+ context: {
16
+ meta: {
17
+ lockVersion: {
18
+ inputSchema: typeof LockVersionSchema;
19
+ };
20
+ };
21
+ };
22
+ }
23
+
24
+ export const lockVersionPlugin: Plugin<
25
+ GetSdkType<ListAppsPluginProvides>, // requires getApp in SDK
26
+ {}, // requires manifest context
27
+ LockVersionPluginProvides
28
+ > = ({ sdk }) => {
29
+ const lockVersion = createFunction(
30
+ async function lockVersion(
31
+ options: LockVersionOptions & { configPath?: string },
32
+ ) {
33
+ const { appKey, configPath = ".zapierrc" } = options;
34
+ const resolvedPath = resolve(configPath);
35
+
36
+ // Get the current app information
37
+ const appsIterator = sdk.listApps({ appKeys: [appKey] }).items();
38
+ const apps = [];
39
+ for await (const app of appsIterator) {
40
+ apps.push(app);
41
+ break; // Only need the first result
42
+ }
43
+ const app = apps[0];
44
+
45
+ // Extract implementation name and version from current_implementation_id
46
+ const currentImplementationId = app.current_implementation_id;
47
+ const [implementationName, version] = currentImplementationId.split("@");
48
+
49
+ if (!implementationName || !version) {
50
+ throw new Error(
51
+ `Invalid implementation ID format: ${currentImplementationId}. Expected format: <implementationName>@<version>`,
52
+ );
53
+ }
54
+
55
+ // Read existing config or create new one
56
+ let config: { apps: Record<string, any> } = { apps: {} };
57
+
58
+ if (existsSync(resolvedPath)) {
59
+ try {
60
+ const configContent = readFileSync(resolvedPath, "utf8");
61
+ config = JSON.parse(configContent);
62
+
63
+ // Ensure apps property exists
64
+ if (!config.apps) {
65
+ config.apps = {};
66
+ }
67
+ } catch (error) {
68
+ console.warn(
69
+ `⚠️ Failed to parse existing config file, creating new one: ${error}`,
70
+ );
71
+ config = { apps: {} };
72
+ }
73
+ }
74
+
75
+ // Update the apps section
76
+ config.apps[appKey] = {
77
+ implementationName,
78
+ version,
79
+ };
80
+
81
+ // Write the updated config
82
+ writeFileSync(resolvedPath, JSON.stringify(config, null, 2));
83
+
84
+ return {
85
+ data: {
86
+ ...app,
87
+ implementationName,
88
+ version,
89
+ },
90
+ configPath: resolvedPath,
91
+ };
92
+ },
93
+ LockVersionSchema.extend({
94
+ configPath: z
95
+ .string()
96
+ .optional()
97
+ .describe("Path to .zapierrc file (defaults to '.zapierrc')"),
98
+ }),
99
+ );
100
+
101
+ return {
102
+ lockVersion,
103
+ context: {
104
+ meta: {
105
+ lockVersion: {
106
+ categories: ["utility"],
107
+ inputSchema: LockVersionSchema,
108
+ },
109
+ },
110
+ },
111
+ };
112
+ };
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ export const LockVersionSchema = z.object({
4
+ appKey: z
5
+ .string()
6
+ .describe("The app key to lock version for (e.g., 'slack', 'gmail')"),
7
+ });
8
+
9
+ export type LockVersionOptions = z.infer<typeof LockVersionSchema>;