@salesforce/webapp-experimental 0.2.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.
- package/README.md +3 -0
- package/dist/api/apex.d.ts +9 -0
- package/dist/api/apex.d.ts.map +1 -0
- package/dist/api/apex.js +18 -0
- package/dist/api/apex.test.d.ts +2 -0
- package/dist/api/apex.test.d.ts.map +1 -0
- package/dist/api/apex.test.js +61 -0
- package/dist/api/clients.d.ts +27 -0
- package/dist/api/clients.d.ts.map +1 -0
- package/dist/api/clients.js +116 -0
- package/dist/api/clients.test.d.ts +2 -0
- package/dist/api/clients.test.d.ts.map +1 -0
- package/dist/api/clients.test.js +237 -0
- package/dist/api/graphql.d.ts +20 -0
- package/dist/api/graphql.d.ts.map +1 -0
- package/dist/api/graphql.js +20 -0
- package/dist/api/graphql.test.d.ts +2 -0
- package/dist/api/graphql.test.d.ts.map +1 -0
- package/dist/api/graphql.test.js +70 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +10 -0
- package/dist/api/utils/accounts.d.ts +32 -0
- package/dist/api/utils/accounts.d.ts.map +1 -0
- package/dist/api/utils/accounts.js +45 -0
- package/dist/api/utils/records.d.ts +11 -0
- package/dist/api/utils/records.d.ts.map +1 -0
- package/dist/api/utils/records.js +20 -0
- package/dist/api/utils/records.test.d.ts +2 -0
- package/dist/api/utils/records.test.d.ts.map +1 -0
- package/dist/api/utils/records.test.js +185 -0
- package/dist/api/utils/user.d.ts +12 -0
- package/dist/api/utils/user.d.ts.map +1 -0
- package/dist/api/utils/user.js +23 -0
- package/dist/api/utils/user.test.d.ts +2 -0
- package/dist/api/utils/user.test.d.ts.map +1 -0
- package/dist/api/utils/user.test.js +156 -0
- package/dist/app/index.d.ts +5 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +2 -0
- package/dist/app/manifest.d.ts +32 -0
- package/dist/app/manifest.d.ts.map +1 -0
- package/dist/app/manifest.js +46 -0
- package/dist/app/org.d.ts +22 -0
- package/dist/app/org.d.ts.map +1 -0
- package/dist/app/org.js +62 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/proxy/handler.d.ts +23 -0
- package/dist/proxy/handler.d.ts.map +1 -0
- package/dist/proxy/handler.js +210 -0
- package/dist/proxy/index.d.ts +3 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +1 -0
- package/dist/proxy/routing.d.ts +34 -0
- package/dist/proxy/routing.d.ts.map +1 -0
- package/dist/proxy/routing.js +100 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for querying or mutating data via an Apex class methods that have
|
|
3
|
+
* been annotated as @AuraEnabled
|
|
4
|
+
*/
|
|
5
|
+
export declare const apexClient: {
|
|
6
|
+
get: (className: string, methodName: string, params?: Record<string, unknown>) => Promise<unknown>;
|
|
7
|
+
post: (className: string, methodName: string, params?: Record<string, unknown>) => Promise<unknown>;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=apex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apex.d.ts","sourceRoot":"","sources":["../../src/api/apex.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,eAAO,MAAM,UAAU;qBACL,MAAM,cAAc,MAAM,WAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;sBAG3D,MAAM,cAAc,MAAM,WAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAG9E,CAAC"}
|
package/dist/api/apex.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { API_VERSION, createClient } from "./clients.js";
|
|
2
|
+
const client = createClient(`/lwr/apex/v${API_VERSION}`);
|
|
3
|
+
/**
|
|
4
|
+
* Client for querying or mutating data via an Apex class methods that have
|
|
5
|
+
* been annotated as @AuraEnabled
|
|
6
|
+
*/
|
|
7
|
+
export const apexClient = {
|
|
8
|
+
get: (className, methodName, params) => {
|
|
9
|
+
return executeApex("get", className, methodName, params);
|
|
10
|
+
},
|
|
11
|
+
post: (className, methodName, params) => {
|
|
12
|
+
return executeApex("post", className, methodName, params);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
async function executeApex(method, className, methodName, params) {
|
|
16
|
+
const res = await client[method](`/${className}/${methodName}`, params);
|
|
17
|
+
return await res.json();
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apex.test.d.ts","sourceRoot":"","sources":["../../src/api/apex.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { apexClient } from "./apex.js";
|
|
3
|
+
import { createClient } from "./clients.js";
|
|
4
|
+
vi.mock("./clients.js", () => ({
|
|
5
|
+
API_VERSION: "65.0",
|
|
6
|
+
createClient: vi.fn().mockReturnValue({
|
|
7
|
+
get: vi.fn(),
|
|
8
|
+
post: vi.fn(),
|
|
9
|
+
}),
|
|
10
|
+
}));
|
|
11
|
+
describe("apexClient", () => {
|
|
12
|
+
let mockClient;
|
|
13
|
+
let mockResponse;
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
mockResponse = {
|
|
16
|
+
json: vi.fn(),
|
|
17
|
+
};
|
|
18
|
+
mockClient = vi.mocked(createClient).mock.results[0].value;
|
|
19
|
+
mockClient.get.mockResolvedValue(mockResponse);
|
|
20
|
+
mockClient.post.mockResolvedValue(mockResponse);
|
|
21
|
+
vi.mocked(createClient).mockReturnValue(mockClient);
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
mockClient.get.mockClear();
|
|
25
|
+
mockClient.post.mockClear();
|
|
26
|
+
});
|
|
27
|
+
describe("get", () => {
|
|
28
|
+
it("should pass parameters to GET request", async () => {
|
|
29
|
+
const params = { id: "123", name: "test" };
|
|
30
|
+
const mockData = { result: "success" };
|
|
31
|
+
mockResponse.json.mockResolvedValue(mockData);
|
|
32
|
+
const result = await apexClient.get("TestClass", "testMethod", params);
|
|
33
|
+
expect(mockClient.get).toHaveBeenCalledWith("/TestClass/testMethod", params);
|
|
34
|
+
expect(result).toEqual(mockData);
|
|
35
|
+
});
|
|
36
|
+
it("should handle empty parameters", async () => {
|
|
37
|
+
const mockData = { result: "success" };
|
|
38
|
+
mockResponse.json.mockResolvedValue(mockData);
|
|
39
|
+
const result = await apexClient.get("TestClass", "testMethod", {});
|
|
40
|
+
expect(mockClient.get).toHaveBeenCalledWith("/TestClass/testMethod", {});
|
|
41
|
+
expect(result).toEqual(mockData);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("post", () => {
|
|
45
|
+
it("should pass parameters to POST request", async () => {
|
|
46
|
+
const params = { name: "new item", value: 42 };
|
|
47
|
+
const mockData = { id: "abc123", result: "created" };
|
|
48
|
+
mockResponse.json.mockResolvedValue(mockData);
|
|
49
|
+
const result = await apexClient.post("TestClass", "createMethod", params);
|
|
50
|
+
expect(mockClient.post).toHaveBeenCalledWith("/TestClass/createMethod", params);
|
|
51
|
+
expect(result).toEqual(mockData);
|
|
52
|
+
});
|
|
53
|
+
it("should handle empty parameters", async () => {
|
|
54
|
+
const mockData = { result: "success" };
|
|
55
|
+
mockResponse.json.mockResolvedValue(mockData);
|
|
56
|
+
const result = await apexClient.post("TestClass", "testMethod", {});
|
|
57
|
+
expect(mockClient.post).toHaveBeenCalledWith("/TestClass/testMethod", {});
|
|
58
|
+
expect(result).toEqual(mockData);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const API_VERSION: string;
|
|
2
|
+
declare class ApiClient {
|
|
3
|
+
private readonly baseURL;
|
|
4
|
+
private readonly defaultOptions;
|
|
5
|
+
constructor(baseURL: string, defaultOptions?: RequestInit);
|
|
6
|
+
private isJsonContentType;
|
|
7
|
+
private fetch;
|
|
8
|
+
private processBody;
|
|
9
|
+
post(url: string, body?: unknown, options?: RequestInit): Promise<Response>;
|
|
10
|
+
get(url: string, options?: RequestInit): Promise<Response>;
|
|
11
|
+
put(url: string, body?: unknown, options?: RequestInit): Promise<Response>;
|
|
12
|
+
patch(url: string, body?: unknown, options?: RequestInit): Promise<Response>;
|
|
13
|
+
delete(url: string, options?: RequestInit): Promise<Response>;
|
|
14
|
+
}
|
|
15
|
+
export declare const baseClient: ApiClient;
|
|
16
|
+
export declare const baseDataClient: ApiClient;
|
|
17
|
+
export declare const uiApiClient: ApiClient;
|
|
18
|
+
/**
|
|
19
|
+
* Creates an ApiClient with the provided base URL and default options.
|
|
20
|
+
*
|
|
21
|
+
* @param baseURL - The base URL for the client
|
|
22
|
+
* @param defaultOptions - Default RequestInit options
|
|
23
|
+
* @returns A configured ApiClient
|
|
24
|
+
*/
|
|
25
|
+
export declare function createClient(baseURL: string, defaultOptions?: RequestInit): ApiClient;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=clients.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.d.ts","sourceRoot":"","sources":["../../src/api/clients.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,QAA+B,CAAC;AAmBxD,cAAM,SAAS;IACd,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAc;gBAEjC,OAAO,EAAE,MAAM,EAAE,cAAc,GAAE,WAAgB;IAY7D,OAAO,CAAC,iBAAiB;YASX,KAAK;IAsBnB,OAAO,CAAC,WAAW;IAgBb,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQ/E,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAI9D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQ9E,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQhF,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;CAGvE;AAGD,eAAO,MAAM,UAAU,WAAyB,CAAC;AAGjD,eAAO,MAAM,cAAc,WAA8B,CAAC;AAG1D,eAAO,MAAM,WAAW,WAA2B,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,GAAE,WAAgB,GAAG,SAAS,CAEzF"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ConduitClient } from "@conduit-client/salesforce-lightning-service-worker";
|
|
2
|
+
const BASE_URL = __SF_SERVER_BASE_PATH__ || "";
|
|
3
|
+
// Project standard API version from environment variable with fallback
|
|
4
|
+
export const API_VERSION = __SF_API_VERSION__ || "65.0";
|
|
5
|
+
const BASE_SERVICES_URL = `${BASE_URL}/services`;
|
|
6
|
+
const BASE_DATA_URL = `${BASE_SERVICES_URL}/data/v${API_VERSION}`;
|
|
7
|
+
const UI_API_URL = `${BASE_DATA_URL}/ui-api`;
|
|
8
|
+
const TOKEN_ENDPOINT = `${UI_API_URL}/session/csrf`;
|
|
9
|
+
const CACHE_VERSION = 1;
|
|
10
|
+
const CACHE_NAME = `vibe-coding-starter-${CACHE_VERSION}`;
|
|
11
|
+
ConduitClient.initialize({
|
|
12
|
+
csrf: {
|
|
13
|
+
endpoint: TOKEN_ENDPOINT,
|
|
14
|
+
cacheName: CACHE_NAME,
|
|
15
|
+
protectedUrls: ["/services/data/v", "/lwr/apex/v"],
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const conduitClient = ConduitClient.instance();
|
|
19
|
+
class ApiClient {
|
|
20
|
+
baseURL;
|
|
21
|
+
defaultOptions;
|
|
22
|
+
constructor(baseURL, defaultOptions = {}) {
|
|
23
|
+
this.baseURL = baseURL;
|
|
24
|
+
this.defaultOptions = {
|
|
25
|
+
...defaultOptions,
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
Accept: "application/json",
|
|
29
|
+
...(defaultOptions.headers || {}),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
isJsonContentType(headers) {
|
|
34
|
+
// default to JSON if no headers specified
|
|
35
|
+
if (!headers)
|
|
36
|
+
return true;
|
|
37
|
+
const headersObj = new Headers(headers);
|
|
38
|
+
const contentType = headersObj.get("content-type");
|
|
39
|
+
return !contentType || contentType.includes("application/json");
|
|
40
|
+
}
|
|
41
|
+
async fetch(info, options = {}) {
|
|
42
|
+
const fullURL = info instanceof URL || info.startsWith("http") ? info : `${this.baseURL}${info}`;
|
|
43
|
+
const mergedOptions = {
|
|
44
|
+
...this.defaultOptions,
|
|
45
|
+
...options,
|
|
46
|
+
headers: {
|
|
47
|
+
...this.defaultOptions.headers,
|
|
48
|
+
...(options.headers || {}),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
const response = await conduitClient.fetch(fullURL, mergedOptions);
|
|
52
|
+
// handle 401/403 with page reload
|
|
53
|
+
if (response.status === 401 || response.status === 403) {
|
|
54
|
+
window.location.reload();
|
|
55
|
+
}
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
processBody(body, options) {
|
|
59
|
+
if (body === undefined || body === null) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
if (body instanceof FormData) {
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
if (typeof body === "string") {
|
|
66
|
+
return body;
|
|
67
|
+
}
|
|
68
|
+
if (typeof body === "object" && this.isJsonContentType(options.headers)) {
|
|
69
|
+
return JSON.stringify(body);
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
async post(url, body, options = {}) {
|
|
74
|
+
return this.fetch(url, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
body: this.processBody(body, options),
|
|
77
|
+
...options,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async get(url, options = {}) {
|
|
81
|
+
return this.fetch(url, { method: "GET", ...options });
|
|
82
|
+
}
|
|
83
|
+
async put(url, body, options = {}) {
|
|
84
|
+
return this.fetch(url, {
|
|
85
|
+
method: "PUT",
|
|
86
|
+
body: this.processBody(body, options),
|
|
87
|
+
...options,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async patch(url, body, options = {}) {
|
|
91
|
+
return this.fetch(url, {
|
|
92
|
+
method: "PATCH",
|
|
93
|
+
body: this.processBody(body, options),
|
|
94
|
+
...options,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async delete(url, options = {}) {
|
|
98
|
+
return this.fetch(url, { method: "DELETE", ...options });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Base fetch client
|
|
102
|
+
export const baseClient = createClient(BASE_URL);
|
|
103
|
+
// Extension of the base client that by default points to this project's standard version of the Salesforce API
|
|
104
|
+
export const baseDataClient = createClient(BASE_DATA_URL);
|
|
105
|
+
// Client for Salesforce UI API access
|
|
106
|
+
export const uiApiClient = createClient(UI_API_URL);
|
|
107
|
+
/**
|
|
108
|
+
* Creates an ApiClient with the provided base URL and default options.
|
|
109
|
+
*
|
|
110
|
+
* @param baseURL - The base URL for the client
|
|
111
|
+
* @param defaultOptions - Default RequestInit options
|
|
112
|
+
* @returns A configured ApiClient
|
|
113
|
+
*/
|
|
114
|
+
export function createClient(baseURL, defaultOptions = {}) {
|
|
115
|
+
return new ApiClient(baseURL, defaultOptions);
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.test.d.ts","sourceRoot":"","sources":["../../src/api/clients.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { ConduitClient } from "@conduit-client/salesforce-lightning-service-worker";
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
4
|
+
import { createClient, baseClient, baseDataClient, uiApiClient } from "./clients.js";
|
|
5
|
+
vi.mock("@conduit-client/salesforce-lightning-service-worker", () => {
|
|
6
|
+
return {
|
|
7
|
+
ConduitClient: {
|
|
8
|
+
initialize: vi.fn(),
|
|
9
|
+
instance: vi.fn().mockReturnValue({ fetch: vi.fn() }),
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
// Mock window.location.reload
|
|
14
|
+
const mockReload = vi.fn();
|
|
15
|
+
Object.defineProperty(window, "location", {
|
|
16
|
+
value: { reload: mockReload },
|
|
17
|
+
writable: true,
|
|
18
|
+
});
|
|
19
|
+
describe("clients", () => {
|
|
20
|
+
let mockFetch;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
const mockClient = vi.mocked(ConduitClient.instance());
|
|
23
|
+
mockFetch = mockClient.fetch;
|
|
24
|
+
mockFetch.mockClear();
|
|
25
|
+
mockReload.mockClear();
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
it("should have configured ConduitClient", () => {
|
|
31
|
+
expect(ConduitClient.initialize).toHaveBeenCalledWith({
|
|
32
|
+
csrf: {
|
|
33
|
+
endpoint: "/services/data/v99.0/ui-api/session/csrf",
|
|
34
|
+
cacheName: "vibe-coding-starter-1",
|
|
35
|
+
protectedUrls: ["/services/data/v", "/lwr/apex/v"],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("constructor and default options", () => {
|
|
40
|
+
it("should create client with default JSON headers", () => {
|
|
41
|
+
const client = createClient("/api");
|
|
42
|
+
expect(client).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
it("should merge custom headers with defaults", async () => {
|
|
45
|
+
const client = createClient("/api", {
|
|
46
|
+
headers: { "Custom-Header": "value" },
|
|
47
|
+
});
|
|
48
|
+
mockFetch.mockResolvedValueOnce({
|
|
49
|
+
status: 200,
|
|
50
|
+
json: () => Promise.resolve({ success: true }),
|
|
51
|
+
});
|
|
52
|
+
await client.get("/test");
|
|
53
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/test", {
|
|
54
|
+
method: "GET",
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
Accept: "application/json",
|
|
58
|
+
"Custom-Header": "value",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("URL construction", () => {
|
|
64
|
+
it("should combine base URL with relative paths", async () => {
|
|
65
|
+
const client = createClient("/api/v1");
|
|
66
|
+
mockFetch.mockResolvedValueOnce({
|
|
67
|
+
status: 200,
|
|
68
|
+
json: () => Promise.resolve({}),
|
|
69
|
+
});
|
|
70
|
+
await client.get("/users");
|
|
71
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/v1/users", expect.any(Object));
|
|
72
|
+
});
|
|
73
|
+
it("should use absolute URLs as-is", async () => {
|
|
74
|
+
const client = createClient("/api");
|
|
75
|
+
mockFetch.mockResolvedValueOnce({
|
|
76
|
+
status: 200,
|
|
77
|
+
json: () => Promise.resolve({}),
|
|
78
|
+
});
|
|
79
|
+
await client.get("https://external.com/data");
|
|
80
|
+
expect(mockFetch).toHaveBeenCalledWith("https://external.com/data", expect.any(Object));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("HTTP methods", () => {
|
|
84
|
+
let client;
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
client = createClient("/api");
|
|
87
|
+
mockFetch.mockResolvedValue({
|
|
88
|
+
status: 200,
|
|
89
|
+
json: () => Promise.resolve({ success: true }),
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it("should make GET requests", async () => {
|
|
93
|
+
await client.get("/users");
|
|
94
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/users", {
|
|
95
|
+
method: "GET",
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
Accept: "application/json",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it("should make POST requests with JSON body", async () => {
|
|
103
|
+
const data = { name: "John" };
|
|
104
|
+
await client.post("/users", data);
|
|
105
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/users", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
body: JSON.stringify(data),
|
|
108
|
+
headers: {
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
Accept: "application/json",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
it("should make PUT requests with JSON body", async () => {
|
|
115
|
+
const data = { name: "John Updated" };
|
|
116
|
+
await client.put("/users/1", data);
|
|
117
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/users/1", {
|
|
118
|
+
method: "PUT",
|
|
119
|
+
body: JSON.stringify(data),
|
|
120
|
+
headers: {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
Accept: "application/json",
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
it("should make PATCH requests with JSON body", async () => {
|
|
127
|
+
const data = { name: "John Patched" };
|
|
128
|
+
await client.patch("/users/1", data);
|
|
129
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/users/1", {
|
|
130
|
+
method: "PATCH",
|
|
131
|
+
body: JSON.stringify(data),
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
Accept: "application/json",
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
it("should make DELETE requests", async () => {
|
|
139
|
+
await client.delete("/users/1");
|
|
140
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/users/1", {
|
|
141
|
+
method: "DELETE",
|
|
142
|
+
headers: {
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
Accept: "application/json",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe("body processing", () => {
|
|
150
|
+
let client;
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
client = createClient("/api");
|
|
153
|
+
mockFetch.mockResolvedValue({
|
|
154
|
+
status: 200,
|
|
155
|
+
json: () => Promise.resolve({}),
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
it("should stringify object bodies for JSON content-type", async () => {
|
|
159
|
+
const data = { name: "John" };
|
|
160
|
+
await client.post("/users", data);
|
|
161
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/users", expect.objectContaining({
|
|
162
|
+
body: JSON.stringify(data),
|
|
163
|
+
}));
|
|
164
|
+
});
|
|
165
|
+
it("should not stringify non-object bodies", async () => {
|
|
166
|
+
const formData = new FormData();
|
|
167
|
+
await client.post("/upload", formData);
|
|
168
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/upload", expect.objectContaining({
|
|
169
|
+
body: formData,
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
172
|
+
it("should not stringify when content-type is not JSON", async () => {
|
|
173
|
+
const data = { name: "John" };
|
|
174
|
+
await client.post("/users", data, {
|
|
175
|
+
headers: { "Content-Type": "multipart/form-data" },
|
|
176
|
+
});
|
|
177
|
+
// When content-type is not JSON and body is a plain object (not FormData/string),
|
|
178
|
+
// processBody returns undefined since it can't serialize non-JSON content
|
|
179
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/users", expect.objectContaining({
|
|
180
|
+
body: undefined,
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe("error handling", () => {
|
|
185
|
+
let client;
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
client = createClient("/api");
|
|
188
|
+
});
|
|
189
|
+
it("should reload page on 401 status", async () => {
|
|
190
|
+
mockFetch.mockResolvedValueOnce({
|
|
191
|
+
status: 401,
|
|
192
|
+
json: () => Promise.resolve({}),
|
|
193
|
+
});
|
|
194
|
+
await client.get("/protected");
|
|
195
|
+
expect(mockReload).toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
it("should reload page on 403 status", async () => {
|
|
198
|
+
mockFetch.mockResolvedValueOnce({
|
|
199
|
+
status: 403,
|
|
200
|
+
json: () => Promise.resolve({}),
|
|
201
|
+
});
|
|
202
|
+
await client.get("/protected");
|
|
203
|
+
expect(mockReload).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
it("should not reload page on other status codes", async () => {
|
|
206
|
+
mockFetch.mockResolvedValueOnce({
|
|
207
|
+
status: 404,
|
|
208
|
+
json: () => Promise.resolve({}),
|
|
209
|
+
});
|
|
210
|
+
await client.get("/notfound");
|
|
211
|
+
expect(mockReload).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe("exported clients", () => {
|
|
215
|
+
it("should export baseClient with correct base URL", () => {
|
|
216
|
+
expect(baseClient).toBeDefined();
|
|
217
|
+
});
|
|
218
|
+
it("should export baseDataClient with correct base URL", () => {
|
|
219
|
+
expect(baseDataClient).toBeDefined();
|
|
220
|
+
});
|
|
221
|
+
it("should export uiApiClient with correct base URL", () => {
|
|
222
|
+
expect(uiApiClient).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe("createClient factory", () => {
|
|
226
|
+
it("should create client with provided base URL", () => {
|
|
227
|
+
const client = createClient("/custom");
|
|
228
|
+
expect(client).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
it("should create client with default options", () => {
|
|
231
|
+
const client = createClient("/api", {
|
|
232
|
+
headers: { Authorization: "Bearer token" },
|
|
233
|
+
});
|
|
234
|
+
expect(client).toBeDefined();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface GraphQLResponse<T> {
|
|
2
|
+
data: T;
|
|
3
|
+
errors?: {
|
|
4
|
+
message: string;
|
|
5
|
+
locations?: {
|
|
6
|
+
line: number;
|
|
7
|
+
column: number;
|
|
8
|
+
}[];
|
|
9
|
+
path?: string[];
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Execute a GraphQL query against the Salesforce GraphQL API
|
|
14
|
+
*
|
|
15
|
+
* @param query - The GraphQL query string
|
|
16
|
+
* @param variables - Optional variables for the query
|
|
17
|
+
* @returns The GraphQL response data
|
|
18
|
+
*/
|
|
19
|
+
export declare function executeGraphQL<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
|
|
20
|
+
//# sourceMappingURL=graphql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/api/graphql.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe,CAAC,CAAC;IACjC,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,CAAC,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC/C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KAChB,EAAE,CAAC;CACJ;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,CAAC,EACrC,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,CAAC,CAAC,CAaZ"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { baseDataClient } from "./clients.js";
|
|
2
|
+
/**
|
|
3
|
+
* Execute a GraphQL query against the Salesforce GraphQL API
|
|
4
|
+
*
|
|
5
|
+
* @param query - The GraphQL query string
|
|
6
|
+
* @param variables - Optional variables for the query
|
|
7
|
+
* @returns The GraphQL response data
|
|
8
|
+
*/
|
|
9
|
+
export async function executeGraphQL(query, variables) {
|
|
10
|
+
const res = await baseDataClient.post("/graphql", {
|
|
11
|
+
query,
|
|
12
|
+
variables,
|
|
13
|
+
});
|
|
14
|
+
const json = await res.json();
|
|
15
|
+
if (json.errors?.length) {
|
|
16
|
+
const errorMessages = json.errors.map((e) => e.message).join("; ");
|
|
17
|
+
throw new Error(`GraphQL Error: ${errorMessages}`);
|
|
18
|
+
}
|
|
19
|
+
return json.data;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql.test.d.ts","sourceRoot":"","sources":["../../src/api/graphql.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { baseDataClient } from "./clients.js";
|
|
3
|
+
import { executeGraphQL } from "./graphql.js";
|
|
4
|
+
vi.mock("./clients.js", () => ({
|
|
5
|
+
baseDataClient: {
|
|
6
|
+
post: vi.fn(),
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
describe("executeGraphQL", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
it("should successfully execute a GraphQL query", async () => {
|
|
14
|
+
const mockData = {
|
|
15
|
+
uiapi: {
|
|
16
|
+
query: {
|
|
17
|
+
Account: {
|
|
18
|
+
edges: [
|
|
19
|
+
{
|
|
20
|
+
node: {
|
|
21
|
+
Id: "001",
|
|
22
|
+
Name: { value: "Test Account" },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
vi.mocked(baseDataClient.post).mockResolvedValue({
|
|
31
|
+
json: () => Promise.resolve({ data: mockData }),
|
|
32
|
+
});
|
|
33
|
+
const query = "query { uiapi { query { Account { edges { node { Id Name { value } } } } } } }";
|
|
34
|
+
const result = await executeGraphQL(query);
|
|
35
|
+
expect(baseDataClient.post).toHaveBeenCalledWith("/graphql", {
|
|
36
|
+
query,
|
|
37
|
+
variables: undefined,
|
|
38
|
+
});
|
|
39
|
+
expect(result).toEqual(mockData);
|
|
40
|
+
});
|
|
41
|
+
it("should handle GraphQL errors", async () => {
|
|
42
|
+
const mockErrors = [{ message: "Field not found" }, { message: "Invalid query" }];
|
|
43
|
+
vi.mocked(baseDataClient.post).mockResolvedValue({
|
|
44
|
+
json: () => Promise.resolve({ errors: mockErrors }),
|
|
45
|
+
});
|
|
46
|
+
const query = "query { invalid }";
|
|
47
|
+
await expect(executeGraphQL(query)).rejects.toThrow("GraphQL Error: Field not found; Invalid query");
|
|
48
|
+
});
|
|
49
|
+
it("should handle network errors", async () => {
|
|
50
|
+
vi.mocked(baseDataClient.post).mockRejectedValue({
|
|
51
|
+
status: 500,
|
|
52
|
+
message: "Network error",
|
|
53
|
+
});
|
|
54
|
+
const query = "query { test }";
|
|
55
|
+
await expect(executeGraphQL(query)).rejects.toThrow("Network error");
|
|
56
|
+
});
|
|
57
|
+
it("should pass variables to the GraphQL query", async () => {
|
|
58
|
+
const mockData = { test: "data" };
|
|
59
|
+
vi.mocked(baseDataClient.post).mockResolvedValue({
|
|
60
|
+
json: () => Promise.resolve({ data: mockData }),
|
|
61
|
+
});
|
|
62
|
+
const query = "query GetAccount($id: ID!) { account(id: $id) { name } }";
|
|
63
|
+
const variables = { id: "001" };
|
|
64
|
+
await executeGraphQL(query, variables);
|
|
65
|
+
expect(baseDataClient.post).toHaveBeenCalledWith("/graphql", {
|
|
66
|
+
query,
|
|
67
|
+
variables,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { baseClient, baseDataClient, uiApiClient, createClient } from "./clients.js";
|
|
2
|
+
export { executeGraphQL, type GraphQLResponse } from "./graphql.js";
|
|
3
|
+
export { apexClient } from "./apex.js";
|
|
4
|
+
export { getHighRevenueAccounts, type Account } from "./utils/accounts.js";
|
|
5
|
+
export { getRecord, createRecord, updateRecord, deleteRecord } from "./utils/records.js";
|
|
6
|
+
export { getCurrentUser } from "./utils/user.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGrF,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC,OAAO,EAAE,sBAAsB,EAAE,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE3E,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEzF,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Core clients
|
|
2
|
+
export { baseClient, baseDataClient, uiApiClient, createClient } from "./clients.js";
|
|
3
|
+
// GraphQL
|
|
4
|
+
export { executeGraphQL } from "./graphql.js";
|
|
5
|
+
// Apex
|
|
6
|
+
export { apexClient } from "./apex.js";
|
|
7
|
+
// Utility functions
|
|
8
|
+
export { getHighRevenueAccounts } from "./utils/accounts.js";
|
|
9
|
+
export { getRecord, createRecord, updateRecord, deleteRecord } from "./utils/records.js";
|
|
10
|
+
export { getCurrentUser } from "./utils/user.js";
|