@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
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @attention agents
|
|
3
|
+
* This file serves as a reference example for accessing Salesforce data via GraphQL.
|
|
4
|
+
* Use this pattern when implementing new data fetching utilities:
|
|
5
|
+
* 1. Define TypeScript interfaces for the response shape
|
|
6
|
+
* 2. Write the GraphQL query using UIAPI syntax
|
|
7
|
+
* 3. Use executeGraphQL<T>() with proper typing
|
|
8
|
+
* 4. Extract and return the relevant data from the response
|
|
9
|
+
*/
|
|
10
|
+
export interface Account {
|
|
11
|
+
Id: string;
|
|
12
|
+
Name: {
|
|
13
|
+
value: string;
|
|
14
|
+
};
|
|
15
|
+
AnnualRevenue: {
|
|
16
|
+
value: number | null;
|
|
17
|
+
};
|
|
18
|
+
Industry: {
|
|
19
|
+
value: string | null;
|
|
20
|
+
};
|
|
21
|
+
Website: {
|
|
22
|
+
value: string | null;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fetch accounts with annual revenue greater than the specified amount
|
|
27
|
+
*
|
|
28
|
+
* @param minRevenue - Minimum annual revenue threshold (default: 100000)
|
|
29
|
+
* @returns Array of accounts matching the criteria
|
|
30
|
+
*/
|
|
31
|
+
export declare function getHighRevenueAccounts(minRevenue?: number): Promise<Account[]>;
|
|
32
|
+
//# sourceMappingURL=accounts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../../src/api/utils/accounts.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACxB,aAAa,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACxC,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACnC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CAClC;AAsCD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,SAAS,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAMpF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @attention agents
|
|
3
|
+
* This file serves as a reference example for accessing Salesforce data via GraphQL.
|
|
4
|
+
* Use this pattern when implementing new data fetching utilities:
|
|
5
|
+
* 1. Define TypeScript interfaces for the response shape
|
|
6
|
+
* 2. Write the GraphQL query using UIAPI syntax
|
|
7
|
+
* 3. Use executeGraphQL<T>() with proper typing
|
|
8
|
+
* 4. Extract and return the relevant data from the response
|
|
9
|
+
*/
|
|
10
|
+
import { executeGraphQL } from "../graphql.js";
|
|
11
|
+
const HIGH_REVENUE_ACCOUNTS_QUERY = `
|
|
12
|
+
query GetHighRevenueAccounts($minRevenue: Currency) {
|
|
13
|
+
uiapi {
|
|
14
|
+
query {
|
|
15
|
+
Account(
|
|
16
|
+
where: { AnnualRevenue: { gt: $minRevenue } }
|
|
17
|
+
orderBy: { AnnualRevenue: { order: DESC } }
|
|
18
|
+
first: 50
|
|
19
|
+
) {
|
|
20
|
+
edges {
|
|
21
|
+
node {
|
|
22
|
+
Id
|
|
23
|
+
Name { value }
|
|
24
|
+
AnnualRevenue { value }
|
|
25
|
+
Industry { value }
|
|
26
|
+
Website { value }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
/**
|
|
35
|
+
* Fetch accounts with annual revenue greater than the specified amount
|
|
36
|
+
*
|
|
37
|
+
* @param minRevenue - Minimum annual revenue threshold (default: 100000)
|
|
38
|
+
* @returns Array of accounts matching the criteria
|
|
39
|
+
*/
|
|
40
|
+
export async function getHighRevenueAccounts(minRevenue = 100000) {
|
|
41
|
+
const response = await executeGraphQL(HIGH_REVENUE_ACCOUNTS_QUERY, {
|
|
42
|
+
minRevenue,
|
|
43
|
+
});
|
|
44
|
+
return response.uiapi.query.Account.edges.map((edge) => edge.node);
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface RecordResponse {
|
|
2
|
+
fields: Record<string, {
|
|
3
|
+
value: string;
|
|
4
|
+
}>;
|
|
5
|
+
}
|
|
6
|
+
export declare function getRecord(recordId: string, params?: Record<string, string>): Promise<RecordResponse>;
|
|
7
|
+
export declare function createRecord(apiName: string, fields: Record<string, unknown>): Promise<RecordResponse>;
|
|
8
|
+
export declare function updateRecord(recordId: string, fields: Record<string, unknown>): Promise<RecordResponse>;
|
|
9
|
+
export declare function deleteRecord(recordId: string): Promise<boolean>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../../src/api/utils/records.ts"],"names":[],"mappings":"AAEA,UAAU,cAAc;IACvB,MAAM,EAAE,MAAM,CACb,MAAM,EACN;QACC,KAAK,EAAE,MAAM,CAAC;KACd,CACD,CAAC;CACF;AAED,wBAAsB,SAAS,CAC9B,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACjC,OAAO,CAAC,cAAc,CAAC,CAOzB;AAED,wBAAsB,YAAY,CACjC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,cAAc,CAAC,CAGzB;AAED,wBAAsB,YAAY,CACjC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,cAAc,CAAC,CAGzB;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAGrE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { uiApiClient } from "../clients.js";
|
|
2
|
+
export async function getRecord(recordId, params = {}) {
|
|
3
|
+
const searchParams = new URLSearchParams(params);
|
|
4
|
+
const queryString = searchParams.toString();
|
|
5
|
+
const url = `/records/${recordId}${queryString ? `?${queryString}` : ""}`;
|
|
6
|
+
const response = await uiApiClient.get(url);
|
|
7
|
+
return response.json();
|
|
8
|
+
}
|
|
9
|
+
export async function createRecord(apiName, fields) {
|
|
10
|
+
const response = await uiApiClient.post(`/records`, { apiName, fields });
|
|
11
|
+
return response.json();
|
|
12
|
+
}
|
|
13
|
+
export async function updateRecord(recordId, fields) {
|
|
14
|
+
const response = await uiApiClient.patch(`/records/${recordId}`, { fields });
|
|
15
|
+
return response.json();
|
|
16
|
+
}
|
|
17
|
+
export async function deleteRecord(recordId) {
|
|
18
|
+
await uiApiClient.delete(`/records/${recordId}`);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records.test.d.ts","sourceRoot":"","sources":["../../../src/api/utils/records.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { getRecord, createRecord, updateRecord, deleteRecord } from "./records.js";
|
|
3
|
+
// Mock the uiApiClient
|
|
4
|
+
vi.mock("../clients.js", () => {
|
|
5
|
+
const mockGet = vi.fn();
|
|
6
|
+
const mockPost = vi.fn();
|
|
7
|
+
const mockPatch = vi.fn();
|
|
8
|
+
const mockDelete = vi.fn();
|
|
9
|
+
return {
|
|
10
|
+
uiApiClient: {
|
|
11
|
+
get: mockGet,
|
|
12
|
+
post: mockPost,
|
|
13
|
+
patch: mockPatch,
|
|
14
|
+
delete: mockDelete,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
// Get references to the mocked functions for assertions
|
|
19
|
+
const { uiApiClient } = await import("../clients.js");
|
|
20
|
+
const mockGet = vi.mocked(uiApiClient.get);
|
|
21
|
+
const mockPost = vi.mocked(uiApiClient.post);
|
|
22
|
+
const mockPatch = vi.mocked(uiApiClient.patch);
|
|
23
|
+
const mockDelete = vi.mocked(uiApiClient.delete);
|
|
24
|
+
describe("Records API Utils", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
describe("getRecord", () => {
|
|
29
|
+
const data = {
|
|
30
|
+
fields: {
|
|
31
|
+
Name: { value: "Test Account" },
|
|
32
|
+
Industry: { value: "Technology" },
|
|
33
|
+
Phone: { value: "555-0123" },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const mockRecordResponse = { json: vi.fn().mockResolvedValue(data) };
|
|
37
|
+
it("fetches a record by ID with default params", async () => {
|
|
38
|
+
mockGet.mockResolvedValue(mockRecordResponse);
|
|
39
|
+
const result = await getRecord("001000000000001");
|
|
40
|
+
expect(mockGet).toHaveBeenCalledWith("/records/001000000000001");
|
|
41
|
+
expect(result).toEqual(data);
|
|
42
|
+
});
|
|
43
|
+
it("fetches a record by ID with custom params", async () => {
|
|
44
|
+
mockGet.mockResolvedValue(mockRecordResponse);
|
|
45
|
+
const params = { fields: "Name,Industry,Phone" };
|
|
46
|
+
const result = await getRecord("001000000000001", params);
|
|
47
|
+
expect(mockGet).toHaveBeenCalledWith("/records/001000000000001?fields=Name%2CIndustry%2CPhone");
|
|
48
|
+
expect(result).toEqual(data);
|
|
49
|
+
});
|
|
50
|
+
it("handles API errors correctly", async () => {
|
|
51
|
+
const apiError = new Error("Record not found");
|
|
52
|
+
mockGet.mockRejectedValue(apiError);
|
|
53
|
+
await expect(getRecord("001000000000001")).rejects.toThrow("Record not found");
|
|
54
|
+
expect(mockGet).toHaveBeenCalledWith("/records/001000000000001");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe("createRecord", () => {
|
|
58
|
+
const data = {
|
|
59
|
+
id: "001000000000002",
|
|
60
|
+
success: true,
|
|
61
|
+
};
|
|
62
|
+
const mockCreateResponse = { json: vi.fn().mockResolvedValue(data) };
|
|
63
|
+
it("creates a record with correct API name and fields", async () => {
|
|
64
|
+
mockPost.mockResolvedValue(mockCreateResponse);
|
|
65
|
+
const apiName = "Account";
|
|
66
|
+
const fields = {
|
|
67
|
+
Name: "New Test Account",
|
|
68
|
+
Industry: "Healthcare",
|
|
69
|
+
Phone: "555-0456",
|
|
70
|
+
};
|
|
71
|
+
const result = await createRecord(apiName, fields);
|
|
72
|
+
expect(mockPost).toHaveBeenCalledWith("/records", { apiName, fields });
|
|
73
|
+
expect(result).toEqual(data);
|
|
74
|
+
});
|
|
75
|
+
it("handles empty fields object", async () => {
|
|
76
|
+
mockPost.mockResolvedValue(mockCreateResponse);
|
|
77
|
+
const result = await createRecord("Account", {});
|
|
78
|
+
expect(mockPost).toHaveBeenCalledWith("/records", {
|
|
79
|
+
apiName: "Account",
|
|
80
|
+
fields: {},
|
|
81
|
+
});
|
|
82
|
+
expect(result).toEqual(data);
|
|
83
|
+
});
|
|
84
|
+
it("handles API errors during creation", async () => {
|
|
85
|
+
const apiError = new Error("Invalid field value");
|
|
86
|
+
mockPost.mockRejectedValue(apiError);
|
|
87
|
+
const fields = { Name: "" }; // Invalid empty name
|
|
88
|
+
await expect(createRecord("Account", fields)).rejects.toThrow("Invalid field value");
|
|
89
|
+
expect(mockPost).toHaveBeenCalledWith("/records", {
|
|
90
|
+
apiName: "Account",
|
|
91
|
+
fields,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe("updateRecord", () => {
|
|
96
|
+
const data = { success: true };
|
|
97
|
+
const mockUpdateResponse = { json: vi.fn().mockResolvedValue(data) };
|
|
98
|
+
it("updates a record with correct ID and fields", async () => {
|
|
99
|
+
mockPatch.mockResolvedValue(mockUpdateResponse);
|
|
100
|
+
const recordId = "001000000000001";
|
|
101
|
+
const fields = {
|
|
102
|
+
Name: "Updated Account Name",
|
|
103
|
+
Industry: "Finance",
|
|
104
|
+
};
|
|
105
|
+
const result = await updateRecord(recordId, fields);
|
|
106
|
+
expect(mockPatch).toHaveBeenCalledWith(`/records/${recordId}`, {
|
|
107
|
+
fields,
|
|
108
|
+
});
|
|
109
|
+
expect(result).toEqual(data);
|
|
110
|
+
});
|
|
111
|
+
it("handles partial field updates", async () => {
|
|
112
|
+
mockPatch.mockResolvedValue(mockUpdateResponse);
|
|
113
|
+
const recordId = "001000000000001";
|
|
114
|
+
const fields = { Industry: "Manufacturing" };
|
|
115
|
+
const result = await updateRecord(recordId, fields);
|
|
116
|
+
expect(mockPatch).toHaveBeenCalledWith(`/records/${recordId}`, {
|
|
117
|
+
fields,
|
|
118
|
+
});
|
|
119
|
+
expect(result).toEqual(data);
|
|
120
|
+
});
|
|
121
|
+
it("handles API errors during update", async () => {
|
|
122
|
+
const apiError = new Error("Record is locked");
|
|
123
|
+
mockPatch.mockRejectedValue(apiError);
|
|
124
|
+
const recordId = "001000000000001";
|
|
125
|
+
const fields = { Name: "New Name" };
|
|
126
|
+
await expect(updateRecord(recordId, fields)).rejects.toThrow("Record is locked");
|
|
127
|
+
expect(mockPatch).toHaveBeenCalledWith(`/records/${recordId}`, {
|
|
128
|
+
fields,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
it("handles empty fields object for update", async () => {
|
|
132
|
+
mockPatch.mockResolvedValue(mockUpdateResponse);
|
|
133
|
+
const result = await updateRecord("001000000000001", {});
|
|
134
|
+
expect(mockPatch).toHaveBeenCalledWith("/records/001000000000001", {
|
|
135
|
+
fields: {},
|
|
136
|
+
});
|
|
137
|
+
expect(result).toEqual(data);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe("deleteRecord", () => {
|
|
141
|
+
it("deletes a record by ID and returns true", async () => {
|
|
142
|
+
mockDelete.mockResolvedValue({});
|
|
143
|
+
const result = await deleteRecord("001000000000001");
|
|
144
|
+
expect(mockDelete).toHaveBeenCalledWith("/records/001000000000001");
|
|
145
|
+
expect(result).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
it("handles API errors during deletion", async () => {
|
|
148
|
+
const apiError = new Error("Cannot delete record");
|
|
149
|
+
mockDelete.mockRejectedValue(apiError);
|
|
150
|
+
await expect(deleteRecord("001000000000001")).rejects.toThrow("Cannot delete record");
|
|
151
|
+
expect(mockDelete).toHaveBeenCalledWith("/records/001000000000001");
|
|
152
|
+
});
|
|
153
|
+
it("always returns true on successful deletion", async () => {
|
|
154
|
+
mockDelete.mockResolvedValue({ status: 204 });
|
|
155
|
+
const result = await deleteRecord("001000000000001");
|
|
156
|
+
expect(result).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe("Error Handling", () => {
|
|
160
|
+
it("propagates network errors from getRecord", async () => {
|
|
161
|
+
const networkError = new Error("Network timeout");
|
|
162
|
+
networkError.name = "NetworkError";
|
|
163
|
+
mockGet.mockRejectedValue(networkError);
|
|
164
|
+
await expect(getRecord("001000000000001")).rejects.toThrow("Network timeout");
|
|
165
|
+
});
|
|
166
|
+
it("propagates validation errors from createRecord", async () => {
|
|
167
|
+
const validationError = new Error("Required field missing");
|
|
168
|
+
validationError.name = "ValidationError";
|
|
169
|
+
mockPost.mockRejectedValue(validationError);
|
|
170
|
+
await expect(createRecord("Account", {})).rejects.toThrow("Required field missing");
|
|
171
|
+
});
|
|
172
|
+
it("propagates permission errors from updateRecord", async () => {
|
|
173
|
+
const permissionError = new Error("Insufficient permissions");
|
|
174
|
+
permissionError.name = "PermissionError";
|
|
175
|
+
mockPatch.mockRejectedValue(permissionError);
|
|
176
|
+
await expect(updateRecord("001000000000001", { Name: "Test" })).rejects.toThrow("Insufficient permissions");
|
|
177
|
+
});
|
|
178
|
+
it("propagates not found errors from deleteRecord", async () => {
|
|
179
|
+
const notFoundError = new Error("Record not found");
|
|
180
|
+
notFoundError.name = "NotFoundError";
|
|
181
|
+
mockDelete.mockRejectedValue(notFoundError);
|
|
182
|
+
await expect(deleteRecord("001000000000001")).rejects.toThrow("Record not found");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface User {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Fetch current user information from Salesforce
|
|
7
|
+
* Uses Chatter API to get current user details
|
|
8
|
+
* @returns User info or null if no session
|
|
9
|
+
*/
|
|
10
|
+
export declare function getCurrentUser(): Promise<User | null>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=user.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../../src/api/utils/user.ts"],"names":[],"mappings":"AAEA,UAAU,IAAI;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACb;AASD;;;;GAIG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAkB3D"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { baseDataClient } from "../clients.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetch current user information from Salesforce
|
|
4
|
+
* Uses Chatter API to get current user details
|
|
5
|
+
* @returns User info or null if no session
|
|
6
|
+
*/
|
|
7
|
+
export async function getCurrentUser() {
|
|
8
|
+
try {
|
|
9
|
+
const response = await baseDataClient.get("/chatter/users/me");
|
|
10
|
+
const userData = await response.json();
|
|
11
|
+
if (!userData || !userData.id) {
|
|
12
|
+
throw new Error("No user data found in Chatter API response");
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
id: userData.id,
|
|
16
|
+
name: userData.name || "User",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error("Error fetching user data:", error);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user.test.d.ts","sourceRoot":"","sources":["../../../src/api/utils/user.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { getCurrentUser } from "./user.js";
|
|
3
|
+
// Mock the baseDataClient
|
|
4
|
+
vi.mock("../clients.js", () => {
|
|
5
|
+
const mockGet = vi.fn();
|
|
6
|
+
return {
|
|
7
|
+
baseDataClient: {
|
|
8
|
+
get: mockGet,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
// Get reference to the mocked function for assertions
|
|
13
|
+
const { baseDataClient } = await import("../clients.js");
|
|
14
|
+
const mockGet = vi.mocked(baseDataClient.get);
|
|
15
|
+
// Mock console.error to avoid noise in test output
|
|
16
|
+
const mockConsoleError = vi
|
|
17
|
+
.spyOn(console, "error")
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
19
|
+
.mockImplementation(() => { });
|
|
20
|
+
describe("User API Utils", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
mockConsoleError.mockClear();
|
|
26
|
+
});
|
|
27
|
+
describe("getCurrentUser", () => {
|
|
28
|
+
const mockChatterUserResponse = {
|
|
29
|
+
json: vi.fn().mockResolvedValue({
|
|
30
|
+
id: "005000000000001",
|
|
31
|
+
name: "John Doe",
|
|
32
|
+
email: "john.doe@company.com",
|
|
33
|
+
username: "john.doe@company.com",
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
it("successfully fetches current user with complete data", async () => {
|
|
37
|
+
mockGet.mockResolvedValue(mockChatterUserResponse);
|
|
38
|
+
const result = await getCurrentUser();
|
|
39
|
+
expect(mockGet).toHaveBeenCalledWith("/chatter/users/me");
|
|
40
|
+
expect(result).toEqual({
|
|
41
|
+
id: "005000000000001",
|
|
42
|
+
name: "John Doe",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it("handles user with minimal data", async () => {
|
|
46
|
+
mockGet.mockResolvedValue({
|
|
47
|
+
json: vi.fn().mockResolvedValue({
|
|
48
|
+
id: "005000000000002",
|
|
49
|
+
name: "Jane Smith",
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
const result = await getCurrentUser();
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
id: "005000000000002",
|
|
55
|
+
name: "Jane Smith",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
it('falls back to "User" when name is empty', async () => {
|
|
59
|
+
mockGet.mockResolvedValue({
|
|
60
|
+
json: vi.fn().mockResolvedValue({
|
|
61
|
+
id: "005000000000003",
|
|
62
|
+
name: "",
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
const result = await getCurrentUser();
|
|
66
|
+
expect(result).toEqual({
|
|
67
|
+
id: "005000000000003",
|
|
68
|
+
name: "User",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('falls back to "User" when name is null', async () => {
|
|
72
|
+
mockGet.mockResolvedValue({
|
|
73
|
+
json: vi.fn().mockResolvedValue({
|
|
74
|
+
id: "005000000000004",
|
|
75
|
+
name: null,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
const result = await getCurrentUser();
|
|
79
|
+
expect(result).toEqual({
|
|
80
|
+
id: "005000000000004",
|
|
81
|
+
name: "User",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it('falls back to "User" when name is undefined', async () => {
|
|
85
|
+
mockGet.mockResolvedValue({
|
|
86
|
+
json: vi.fn().mockResolvedValue({
|
|
87
|
+
id: "005000000000005",
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
const result = await getCurrentUser();
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
id: "005000000000005",
|
|
93
|
+
name: "User",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it("throws error when Chatter API request fails", async () => {
|
|
97
|
+
const apiError = new Error("Chatter API error");
|
|
98
|
+
mockGet.mockRejectedValue(apiError);
|
|
99
|
+
await expect(getCurrentUser()).rejects.toThrow("Chatter API error");
|
|
100
|
+
expect(mockConsoleError).toHaveBeenCalledWith("Error fetching user data:", apiError);
|
|
101
|
+
});
|
|
102
|
+
it("throws error when no user data is found", async () => {
|
|
103
|
+
mockGet.mockResolvedValue({
|
|
104
|
+
json: vi.fn().mockResolvedValue(null),
|
|
105
|
+
});
|
|
106
|
+
await expect(getCurrentUser()).rejects.toThrow("No user data found in Chatter API response");
|
|
107
|
+
expect(mockConsoleError).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
it("throws error when user data is undefined", async () => {
|
|
110
|
+
mockGet.mockResolvedValue({
|
|
111
|
+
json: vi.fn().mockResolvedValue(undefined),
|
|
112
|
+
});
|
|
113
|
+
await expect(getCurrentUser()).rejects.toThrow("No user data found in Chatter API response");
|
|
114
|
+
expect(mockConsoleError).toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
it("throws error when no user ID is found", async () => {
|
|
117
|
+
mockGet.mockResolvedValue({
|
|
118
|
+
json: vi.fn().mockResolvedValue({
|
|
119
|
+
name: "John Doe",
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
await expect(getCurrentUser()).rejects.toThrow("No user data found in Chatter API response");
|
|
123
|
+
expect(mockConsoleError).toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
it("throws error when user ID is empty string", async () => {
|
|
126
|
+
mockGet.mockResolvedValue({
|
|
127
|
+
json: vi.fn().mockResolvedValue({
|
|
128
|
+
id: "",
|
|
129
|
+
name: "John Doe",
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
await expect(getCurrentUser()).rejects.toThrow("No user data found in Chatter API response");
|
|
133
|
+
expect(mockConsoleError).toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
it("handles network errors gracefully", async () => {
|
|
136
|
+
const networkError = new Error("Network timeout");
|
|
137
|
+
networkError.name = "NetworkError";
|
|
138
|
+
mockGet.mockRejectedValue(networkError);
|
|
139
|
+
await expect(getCurrentUser()).rejects.toThrow("Network timeout");
|
|
140
|
+
expect(mockConsoleError).toHaveBeenCalledWith("Error fetching user data:", networkError);
|
|
141
|
+
});
|
|
142
|
+
it("handles 401 unauthorized errors", async () => {
|
|
143
|
+
const unauthorizedError = new Error("Unauthorized");
|
|
144
|
+
unauthorizedError.name = "UnauthorizedError";
|
|
145
|
+
mockGet.mockRejectedValue(unauthorizedError);
|
|
146
|
+
await expect(getCurrentUser()).rejects.toThrow("Unauthorized");
|
|
147
|
+
expect(mockConsoleError).toHaveBeenCalledWith("Error fetching user data:", unauthorizedError);
|
|
148
|
+
});
|
|
149
|
+
it("handles response with invalid data structure", async () => {
|
|
150
|
+
mockGet.mockResolvedValue({
|
|
151
|
+
json: vi.fn().mockResolvedValue("invalid data"),
|
|
152
|
+
});
|
|
153
|
+
await expect(getCurrentUser()).rejects.toThrow("No user data found in Chatter API response");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { OrgInfo } from "./org.js";
|
|
2
|
+
export { getOrgInfo, refreshOrgAuth } from "./org.js";
|
|
3
|
+
export type { WebAppManifest, RoutingConfig, RewriteRule, RedirectRule } from "./manifest.js";
|
|
4
|
+
export { loadManifest } from "./manifest.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC9F,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface WebAppManifest {
|
|
2
|
+
name: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
version: string;
|
|
6
|
+
outputDir: string;
|
|
7
|
+
routing?: RoutingConfig;
|
|
8
|
+
}
|
|
9
|
+
export interface RoutingConfig {
|
|
10
|
+
rewrites?: RewriteRule[];
|
|
11
|
+
redirects?: RedirectRule[];
|
|
12
|
+
trailingSlash?: "always" | "never" | "auto";
|
|
13
|
+
fallback?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RewriteRule {
|
|
16
|
+
route: string;
|
|
17
|
+
target: string;
|
|
18
|
+
}
|
|
19
|
+
export interface RedirectRule {
|
|
20
|
+
route: string;
|
|
21
|
+
target: string;
|
|
22
|
+
statusCode: 301 | 302 | 307 | 308;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load and parse webapp.json manifest
|
|
26
|
+
*
|
|
27
|
+
* @param manifestPath - Path to the webapp.json file
|
|
28
|
+
* @returns Promise resolving to the parsed manifest
|
|
29
|
+
* @throws Error if manifest file not found or validation fails
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadManifest(manifestPath: string): Promise<WebAppManifest>;
|
|
32
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/app/manifest.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,cAAc;IAE9B,IAAI,EAAE,MAAM,CAAC;IAEb,KAAK,EAAE,MAAM,CAAC;IAEd,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,OAAO,EAAE,MAAM,CAAC;IAEhB,SAAS,EAAE,MAAM,CAAC;IAElB,OAAO,CAAC,EAAE,aAAa,CAAC;CACxB;AAGD,MAAM,WAAW,aAAa;IAE7B,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IAEzB,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAE3B,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IAE5C,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,MAAM,WAAW,WAAW;IAE3B,KAAK,EAAE,MAAM,CAAC;IAEd,MAAM,EAAE,MAAM,CAAC;CACf;AAGD,MAAM,WAAW,YAAY;IAE5B,KAAK,EAAE,MAAM,CAAC;IAEd,MAAM,EAAE,MAAM,CAAC;IAEf,UAAU,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;CAClC;AAkCD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAchF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
/**
|
|
3
|
+
* Validate required fields in webapp manifest
|
|
4
|
+
*
|
|
5
|
+
* @param manifest - The manifest object to validate
|
|
6
|
+
* @throws Error if any required field is missing
|
|
7
|
+
*/
|
|
8
|
+
function validateManifest(manifest) {
|
|
9
|
+
const errors = [];
|
|
10
|
+
if (!manifest.name) {
|
|
11
|
+
errors.push("name");
|
|
12
|
+
}
|
|
13
|
+
if (!manifest.label) {
|
|
14
|
+
errors.push("label");
|
|
15
|
+
}
|
|
16
|
+
if (!manifest.version) {
|
|
17
|
+
errors.push("version");
|
|
18
|
+
}
|
|
19
|
+
if (!manifest.outputDir) {
|
|
20
|
+
errors.push("outputDir");
|
|
21
|
+
}
|
|
22
|
+
if (errors.length > 0) {
|
|
23
|
+
throw new Error(`Manifest missing required field${errors.length > 1 ? "s" : ""}: ${errors.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Load and parse webapp.json manifest
|
|
28
|
+
*
|
|
29
|
+
* @param manifestPath - Path to the webapp.json file
|
|
30
|
+
* @returns Promise resolving to the parsed manifest
|
|
31
|
+
* @throws Error if manifest file not found or validation fails
|
|
32
|
+
*/
|
|
33
|
+
export async function loadManifest(manifestPath) {
|
|
34
|
+
try {
|
|
35
|
+
const content = await readFile(manifestPath, "utf-8");
|
|
36
|
+
const manifest = JSON.parse(content);
|
|
37
|
+
validateManifest(manifest);
|
|
38
|
+
return manifest;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
42
|
+
throw new Error(`Manifest not found at: ${manifestPath}`);
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface OrgInfo {
|
|
2
|
+
orgId: string;
|
|
3
|
+
apiVersion: string;
|
|
4
|
+
instanceUrl: string;
|
|
5
|
+
username: string;
|
|
6
|
+
accessToken: string;
|
|
7
|
+
orgAlias?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get Salesforce org info and authentication details
|
|
11
|
+
*
|
|
12
|
+
* @param orgAlias - Optional org alias or username, uses default org if not provided
|
|
13
|
+
* @returns Promise resolving to org info or null if authentication fails
|
|
14
|
+
*/
|
|
15
|
+
export declare function getOrgInfo(orgAlias?: string): Promise<OrgInfo | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* Refresh Salesforce org authentication
|
|
18
|
+
*
|
|
19
|
+
* @param orgAlias
|
|
20
|
+
*/
|
|
21
|
+
export declare function refreshOrgAuth(orgAlias: string): Promise<OrgInfo | undefined>;
|
|
22
|
+
//# sourceMappingURL=org.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org.d.ts","sourceRoot":"","sources":["../../src/app/org.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,OAAO;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAkBhF;AAcD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,gCAKpD"}
|