runline 0.11.2 → 0.11.3
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/dist/plugin/loader.js +5 -0
- package/dist/plugins/linear/src/issues.js +5 -1
- package/dist/plugins/salesforce/src/index.js +33 -219
- package/dist/plugins/salesforce/src/metadata.js +50 -0
- package/dist/plugins/salesforce/src/query.js +52 -0
- package/dist/plugins/salesforce/src/queryResult.js +4 -0
- package/dist/plugins/salesforce/src/shared.js +122 -0
- package/dist/plugins/salesforce/src/sobjects.js +152 -0
- package/package.json +1 -1
package/dist/plugin/loader.js
CHANGED
|
@@ -55,6 +55,9 @@ export async function loadPluginFromPath(path) {
|
|
|
55
55
|
: mod;
|
|
56
56
|
return resolvePluginExport(pluginExport, pluginId);
|
|
57
57
|
}
|
|
58
|
+
function isPrivatePluginDirectory(entry) {
|
|
59
|
+
return entry.startsWith("_") || entry.startsWith(".");
|
|
60
|
+
}
|
|
58
61
|
async function loadFromDirectory(dir) {
|
|
59
62
|
const plugins = [];
|
|
60
63
|
if (!existsSync(dir))
|
|
@@ -75,6 +78,8 @@ async function loadFromDirectory(dir) {
|
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
80
|
else if (stat.isDirectory()) {
|
|
81
|
+
if (isPrivatePluginDirectory(entry))
|
|
82
|
+
continue;
|
|
78
83
|
const candidates = [
|
|
79
84
|
join(fullPath, "index.ts"),
|
|
80
85
|
join(fullPath, "index.js"),
|
|
@@ -34,7 +34,11 @@ export function registerIssueActions(rl) {
|
|
|
34
34
|
});
|
|
35
35
|
rl.registerAction("issue.get", {
|
|
36
36
|
description: "Get an issue by ID or identifier (e.g. 'THE-154')",
|
|
37
|
-
inputSchema: t.Object({
|
|
37
|
+
inputSchema: t.Object({
|
|
38
|
+
issueId: t.String({
|
|
39
|
+
description: "Required parameter name. Pass either the Linear issue UUID or issue identifier, e.g. 'THE-154'.",
|
|
40
|
+
}),
|
|
41
|
+
}),
|
|
38
42
|
async execute(input, ctx) {
|
|
39
43
|
const data = await gql(key(ctx), `query($id: String!) { issue(id: $id) { ${ISSUE_FIELDS} } }`, { id: input.issueId });
|
|
40
44
|
const issue = data.issue;
|
|
@@ -1,224 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
instanceUrl: c.instanceUrl.replace(/\/$/, ""),
|
|
6
|
-
accessToken: c.accessToken,
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
async function api(conn, method, endpoint, body, qs) {
|
|
10
|
-
const url = new URL(`${conn.instanceUrl}/services/data/${API_VERSION}${endpoint}`);
|
|
11
|
-
if (qs) {
|
|
12
|
-
for (const [k, v] of Object.entries(qs)) {
|
|
13
|
-
if (v !== undefined && v !== null)
|
|
14
|
-
url.searchParams.set(k, String(v));
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
const init = {
|
|
18
|
-
method,
|
|
19
|
-
headers: {
|
|
20
|
-
Authorization: `Bearer ${conn.accessToken}`,
|
|
21
|
-
"Content-Type": "application/json",
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
if (body && Object.keys(body).length > 0)
|
|
25
|
-
init.body = JSON.stringify(body);
|
|
26
|
-
const res = await fetch(url.toString(), init);
|
|
27
|
-
if (res.status === 204)
|
|
28
|
-
return { success: true };
|
|
29
|
-
if (!res.ok)
|
|
30
|
-
throw new Error(`Salesforce error ${res.status}: ${await res.text()}`);
|
|
31
|
-
return res.json();
|
|
32
|
-
}
|
|
33
|
-
const SOBJECTS = [
|
|
34
|
-
"Account",
|
|
35
|
-
"Contact",
|
|
36
|
-
"Lead",
|
|
37
|
-
"Opportunity",
|
|
38
|
-
"Case",
|
|
39
|
-
"Task",
|
|
40
|
-
"User",
|
|
41
|
-
];
|
|
42
|
-
const DEFAULT_FIELDS = {
|
|
43
|
-
Account: "Id,Name,Type",
|
|
44
|
-
Contact: "Id,FirstName,LastName,Email",
|
|
45
|
-
Lead: "Id,Company,FirstName,LastName,Email,Status",
|
|
46
|
-
Opportunity: "Id,AccountId,Amount,Probability,StageName",
|
|
47
|
-
Case: "Id,AccountId,ContactId,Priority,Status,Subject",
|
|
48
|
-
Task: "Id,Subject,Status,Priority",
|
|
49
|
-
User: "Id,Name,Email",
|
|
50
|
-
};
|
|
51
|
-
function registerSObject(rl, sObject, conn) {
|
|
52
|
-
const lower = sObject.toLowerCase();
|
|
53
|
-
rl.registerAction(`${lower}.create`, {
|
|
54
|
-
description: `Create a ${sObject}`,
|
|
55
|
-
inputSchema: {
|
|
56
|
-
data: {
|
|
57
|
-
type: "object",
|
|
58
|
-
required: true,
|
|
59
|
-
description: `${sObject} field values`,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
async execute(input, ctx) {
|
|
63
|
-
return api(conn(ctx), "POST", `/sobjects/${sObject}`, input.data);
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
rl.registerAction(`${lower}.get`, {
|
|
67
|
-
description: `Get a ${sObject} by ID`,
|
|
68
|
-
inputSchema: { id: { type: "string", required: true } },
|
|
69
|
-
async execute(input, ctx) {
|
|
70
|
-
return api(conn(ctx), "GET", `/sobjects/${sObject}/${input.id}`);
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
rl.registerAction(`${lower}.update`, {
|
|
74
|
-
description: `Update a ${sObject}`,
|
|
75
|
-
inputSchema: {
|
|
76
|
-
id: { type: "string", required: true },
|
|
77
|
-
data: { type: "object", required: true },
|
|
78
|
-
},
|
|
79
|
-
async execute(input, ctx) {
|
|
80
|
-
const p = input;
|
|
81
|
-
await api(conn(ctx), "PATCH", `/sobjects/${sObject}/${p.id}`, p.data);
|
|
82
|
-
return { success: true, id: p.id };
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
rl.registerAction(`${lower}.delete`, {
|
|
86
|
-
description: `Delete a ${sObject}`,
|
|
87
|
-
inputSchema: { id: { type: "string", required: true } },
|
|
88
|
-
async execute(input, ctx) {
|
|
89
|
-
await api(conn(ctx), "DELETE", `/sobjects/${sObject}/${input.id}`);
|
|
90
|
-
return { success: true };
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
rl.registerAction(`${lower}.query`, {
|
|
94
|
-
description: `Query ${sObject}s with SOQL`,
|
|
95
|
-
inputSchema: {
|
|
96
|
-
fields: {
|
|
97
|
-
type: "string",
|
|
98
|
-
required: false,
|
|
99
|
-
description: `Comma-separated (default: ${DEFAULT_FIELDS[sObject] ?? "Id"})`,
|
|
100
|
-
},
|
|
101
|
-
where: {
|
|
102
|
-
type: "string",
|
|
103
|
-
required: false,
|
|
104
|
-
description: "SOQL WHERE clause",
|
|
105
|
-
},
|
|
106
|
-
limit: { type: "number", required: false },
|
|
107
|
-
},
|
|
108
|
-
async execute(input, ctx) {
|
|
109
|
-
const p = (input ?? {});
|
|
110
|
-
const fields = p.fields || DEFAULT_FIELDS[sObject] || "Id";
|
|
111
|
-
let q = `SELECT ${fields} FROM ${sObject}`;
|
|
112
|
-
if (p.where)
|
|
113
|
-
q += ` WHERE ${p.where}`;
|
|
114
|
-
if (p.limit)
|
|
115
|
-
q += ` LIMIT ${p.limit}`;
|
|
116
|
-
const data = (await api(conn(ctx), "GET", "/query", undefined, {
|
|
117
|
-
q,
|
|
118
|
-
}));
|
|
119
|
-
return data.records;
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
rl.registerAction(`${lower}.upsert`, {
|
|
123
|
-
description: `Upsert a ${sObject} by external ID`,
|
|
124
|
-
inputSchema: {
|
|
125
|
-
externalIdField: { type: "string", required: true },
|
|
126
|
-
externalIdValue: { type: "string", required: true },
|
|
127
|
-
data: { type: "object", required: true },
|
|
128
|
-
},
|
|
129
|
-
async execute(input, ctx) {
|
|
130
|
-
const p = input;
|
|
131
|
-
return api(conn(ctx), "PATCH", `/sobjects/${sObject}/${p.externalIdField}/${p.externalIdValue}`, p.data);
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { registerMetadataActions } from "./metadata.js";
|
|
3
|
+
import { registerQueryActions } from "./query.js";
|
|
4
|
+
import { registerGenericSObjectActions, registerStandardSObjectActions } from "./sobjects.js";
|
|
135
5
|
export default function salesforce(rl) {
|
|
136
6
|
rl.setName("salesforce");
|
|
137
|
-
rl.setVersion("0.
|
|
138
|
-
rl.setConnectionSchema({
|
|
139
|
-
instanceUrl: {
|
|
140
|
-
|
|
141
|
-
required: true,
|
|
142
|
-
description: "Salesforce instance URL (e.g. https://yourorg.my.salesforce.com)",
|
|
7
|
+
rl.setVersion("0.2.0");
|
|
8
|
+
rl.setConnectionSchema(t.Object({
|
|
9
|
+
instanceUrl: t.Optional(t.String({
|
|
10
|
+
description: "Salesforce API instance URL, e.g. https://your-domain.my.salesforce.com. Required for static accessToken auth; optional for client credentials when loginUrl is set.",
|
|
143
11
|
env: "SALESFORCE_INSTANCE_URL",
|
|
144
|
-
},
|
|
145
|
-
accessToken: {
|
|
146
|
-
|
|
147
|
-
required: true,
|
|
148
|
-
description: "Salesforce OAuth2 access token",
|
|
12
|
+
})),
|
|
13
|
+
accessToken: t.Optional(t.String({
|
|
14
|
+
description: "Salesforce OAuth2 access token. Use for static-token auth.",
|
|
149
15
|
env: "SALESFORCE_ACCESS_TOKEN",
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
},
|
|
173
|
-
async execute(input, ctx) {
|
|
174
|
-
const p = input;
|
|
175
|
-
return api(getConn(ctx), "GET", `/sobjects/${p.sObject}/${p.id}`);
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
rl.registerAction("sobject.update", {
|
|
179
|
-
description: "Update any sObject record",
|
|
180
|
-
inputSchema: {
|
|
181
|
-
sObject: { type: "string", required: true },
|
|
182
|
-
id: { type: "string", required: true },
|
|
183
|
-
data: { type: "object", required: true },
|
|
184
|
-
},
|
|
185
|
-
async execute(input, ctx) {
|
|
186
|
-
const p = input;
|
|
187
|
-
await api(getConn(ctx), "PATCH", `/sobjects/${p.sObject}/${p.id}`, p.data);
|
|
188
|
-
return { success: true };
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
rl.registerAction("sobject.delete", {
|
|
192
|
-
description: "Delete any sObject record",
|
|
193
|
-
inputSchema: {
|
|
194
|
-
sObject: { type: "string", required: true },
|
|
195
|
-
id: { type: "string", required: true },
|
|
196
|
-
},
|
|
197
|
-
async execute(input, ctx) {
|
|
198
|
-
const p = input;
|
|
199
|
-
await api(getConn(ctx), "DELETE", `/sobjects/${p.sObject}/${p.id}`);
|
|
200
|
-
return { success: true };
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
|
-
// ── SOQL Query ──────────────────────────────────────
|
|
204
|
-
rl.registerAction("soql.query", {
|
|
205
|
-
description: "Execute a raw SOQL query",
|
|
206
|
-
inputSchema: {
|
|
207
|
-
query: { type: "string", required: true, description: "Full SOQL query" },
|
|
208
|
-
},
|
|
209
|
-
async execute(input, ctx) {
|
|
210
|
-
const data = (await api(getConn(ctx), "GET", "/query", undefined, {
|
|
211
|
-
q: input.query,
|
|
212
|
-
}));
|
|
213
|
-
return data.records;
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
// ── Describe ────────────────────────────────────────
|
|
217
|
-
rl.registerAction("sobject.describe", {
|
|
218
|
-
description: "Describe an sObject's metadata/fields",
|
|
219
|
-
inputSchema: { sObject: { type: "string", required: true } },
|
|
220
|
-
async execute(input, ctx) {
|
|
221
|
-
return api(getConn(ctx), "GET", `/sobjects/${input.sObject}/describe`);
|
|
222
|
-
},
|
|
223
|
-
});
|
|
16
|
+
})),
|
|
17
|
+
loginUrl: t.Optional(t.String({
|
|
18
|
+
description: "Salesforce OAuth token host for client credentials, usually your My Domain URL such as https://your-domain.my.salesforce.com.",
|
|
19
|
+
env: "SALESFORCE_LOGIN_URL",
|
|
20
|
+
})),
|
|
21
|
+
clientId: t.Optional(t.String({
|
|
22
|
+
description: "Salesforce Connected App consumer key for OAuth client credentials flow.",
|
|
23
|
+
env: "SALESFORCE_CLIENT_ID",
|
|
24
|
+
})),
|
|
25
|
+
clientSecret: t.Optional(t.String({
|
|
26
|
+
description: "Salesforce Connected App consumer secret for OAuth client credentials flow.",
|
|
27
|
+
env: "SALESFORCE_CLIENT_SECRET",
|
|
28
|
+
})),
|
|
29
|
+
apiVersion: t.Optional(t.String({
|
|
30
|
+
description: "Salesforce REST API version (default v59.0). Accepts vXX.0 or XX.0.",
|
|
31
|
+
env: "SALESFORCE_API_VERSION",
|
|
32
|
+
})),
|
|
33
|
+
}));
|
|
34
|
+
registerMetadataActions(rl);
|
|
35
|
+
registerQueryActions(rl);
|
|
36
|
+
registerStandardSObjectActions(rl);
|
|
37
|
+
registerGenericSObjectActions(rl);
|
|
224
38
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { api, getSession, identity } from "./shared.js";
|
|
3
|
+
export function registerMetadataActions(rl) {
|
|
4
|
+
rl.registerAction("connection.test", {
|
|
5
|
+
description: "Validate Salesforce auth and return safe connection metadata",
|
|
6
|
+
inputSchema: t.Object({}),
|
|
7
|
+
async execute(_input, ctx) {
|
|
8
|
+
const sessionPromise = getSession(ctx);
|
|
9
|
+
const session = await sessionPromise;
|
|
10
|
+
let limits;
|
|
11
|
+
let limitsError;
|
|
12
|
+
try {
|
|
13
|
+
limits = (await api(ctx, "GET", "/limits", undefined, undefined, sessionPromise));
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
limitsError = error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
instanceUrl: session.instanceUrl,
|
|
21
|
+
tokenType: session.tokenType,
|
|
22
|
+
scope: session.scope,
|
|
23
|
+
id: session.id,
|
|
24
|
+
limits,
|
|
25
|
+
limitsError,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
rl.registerAction("auth.identity", {
|
|
30
|
+
description: "Return the Salesforce OAuth identity for the current connection",
|
|
31
|
+
inputSchema: t.Object({}),
|
|
32
|
+
async execute(_input, ctx) {
|
|
33
|
+
return identity(ctx);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
rl.registerAction("limits.get", {
|
|
37
|
+
description: "Return Salesforce org REST API limits",
|
|
38
|
+
inputSchema: t.Object({}),
|
|
39
|
+
async execute(_input, ctx) {
|
|
40
|
+
return api(ctx, "GET", "/limits");
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
rl.registerAction("metadata.objects", {
|
|
44
|
+
description: "List available Salesforce sObjects and metadata summaries",
|
|
45
|
+
inputSchema: t.Object({}),
|
|
46
|
+
async execute(_input, ctx) {
|
|
47
|
+
return api(ctx, "GET", "/sobjects");
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { records } from "./queryResult.js";
|
|
3
|
+
import { api, rest } from "./shared.js";
|
|
4
|
+
const QueryInput = t.Object({
|
|
5
|
+
query: t.String({ description: "Full SOQL query" }),
|
|
6
|
+
});
|
|
7
|
+
async function queryPage(ctx, endpoint, query) {
|
|
8
|
+
return (await api(ctx, "GET", endpoint, undefined, {
|
|
9
|
+
q: query,
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
export function registerQueryActions(rl) {
|
|
13
|
+
rl.registerAction("soql.query", {
|
|
14
|
+
description: "Execute a raw SOQL query and return the first page of records",
|
|
15
|
+
inputSchema: QueryInput,
|
|
16
|
+
async execute(input, ctx) {
|
|
17
|
+
return records(await queryPage(ctx, "/query", input.query));
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
rl.registerAction("soql.queryPage", {
|
|
21
|
+
description: "Execute a raw SOQL query and return Salesforce pagination metadata",
|
|
22
|
+
inputSchema: QueryInput,
|
|
23
|
+
async execute(input, ctx) {
|
|
24
|
+
return queryPage(ctx, "/query", input.query);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
rl.registerAction("soql.queryAll", {
|
|
28
|
+
description: "Execute a raw SOQL query including deleted and archived records; returns the first page of records",
|
|
29
|
+
inputSchema: QueryInput,
|
|
30
|
+
async execute(input, ctx) {
|
|
31
|
+
return records(await queryPage(ctx, "/queryAll", input.query));
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
rl.registerAction("soql.queryAllPage", {
|
|
35
|
+
description: "Execute a raw SOQL query including deleted and archived records and return Salesforce pagination metadata",
|
|
36
|
+
inputSchema: QueryInput,
|
|
37
|
+
async execute(input, ctx) {
|
|
38
|
+
return queryPage(ctx, "/queryAll", input.query);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
rl.registerAction("soql.nextPage", {
|
|
42
|
+
description: "Fetch the next page from a Salesforce nextRecordsUrl returned by soql.queryPage or soql.queryAllPage",
|
|
43
|
+
inputSchema: t.Object({
|
|
44
|
+
nextRecordsUrl: t.String({
|
|
45
|
+
description: "Salesforce nextRecordsUrl, e.g. /services/data/v59.0/query/01g...",
|
|
46
|
+
}),
|
|
47
|
+
}),
|
|
48
|
+
async execute(input, ctx) {
|
|
49
|
+
return rest(ctx, "GET", input.nextRecordsUrl);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export const DEFAULT_API_VERSION = "v59.0";
|
|
2
|
+
export function config(ctx) {
|
|
3
|
+
return ctx.connection.config;
|
|
4
|
+
}
|
|
5
|
+
export function trimTrailingSlash(url) {
|
|
6
|
+
return url.replace(/\/+$/, "");
|
|
7
|
+
}
|
|
8
|
+
function requireString(value, name) {
|
|
9
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
10
|
+
throw new Error(`Salesforce connection is missing ${name}`);
|
|
11
|
+
}
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
export function apiVersion(ctx) {
|
|
15
|
+
const raw = config(ctx).apiVersion;
|
|
16
|
+
if (typeof raw !== "string" || raw.trim() === "")
|
|
17
|
+
return DEFAULT_API_VERSION;
|
|
18
|
+
const version = raw.trim();
|
|
19
|
+
return version.startsWith("v") ? version : `v${version}`;
|
|
20
|
+
}
|
|
21
|
+
export function validateInstanceUrl(url) {
|
|
22
|
+
const normalized = trimTrailingSlash(url.trim());
|
|
23
|
+
if (/\.lightning\.force\.com$/i.test(new URL(normalized).hostname)) {
|
|
24
|
+
throw new Error("Salesforce instanceUrl/loginUrl must be the API My Domain URL, not the Lightning UI URL. Use a URL like https://your-domain.my.salesforce.com.");
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
export async function getSession(ctx) {
|
|
29
|
+
const c = config(ctx);
|
|
30
|
+
if (c.accessToken) {
|
|
31
|
+
return {
|
|
32
|
+
instanceUrl: validateInstanceUrl(requireString(c.instanceUrl, "instanceUrl")),
|
|
33
|
+
accessToken: requireString(c.accessToken, "accessToken"),
|
|
34
|
+
tokenType: "Bearer",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const loginUrl = validateInstanceUrl(requireString(c.loginUrl ?? c.instanceUrl, "loginUrl or instanceUrl"));
|
|
38
|
+
const clientId = requireString(c.clientId, "clientId");
|
|
39
|
+
const clientSecret = requireString(c.clientSecret, "clientSecret");
|
|
40
|
+
const body = new URLSearchParams();
|
|
41
|
+
body.set("grant_type", "client_credentials");
|
|
42
|
+
body.set("client_id", clientId);
|
|
43
|
+
body.set("client_secret", clientSecret);
|
|
44
|
+
const res = await fetch(`${loginUrl}/services/oauth2/token`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
Accept: "application/json",
|
|
48
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
49
|
+
},
|
|
50
|
+
body,
|
|
51
|
+
});
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
let data;
|
|
54
|
+
try {
|
|
55
|
+
data = text ? JSON.parse(text) : {};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
data = { raw: text };
|
|
59
|
+
}
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const err = typeof data.error === "string" ? data.error : res.status;
|
|
62
|
+
const description = typeof data.error_description === "string"
|
|
63
|
+
? `: ${data.error_description}`
|
|
64
|
+
: "";
|
|
65
|
+
throw new Error(`Salesforce token error ${err}${description}`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
instanceUrl: validateInstanceUrl(requireString(data.instance_url, "token response instance_url")),
|
|
69
|
+
accessToken: requireString(data.access_token, "token response access_token"),
|
|
70
|
+
tokenType: typeof data.token_type === "string" ? data.token_type : "Bearer",
|
|
71
|
+
scope: typeof data.scope === "string" ? data.scope : undefined,
|
|
72
|
+
id: typeof data.id === "string" ? data.id : undefined,
|
|
73
|
+
issuedAt: typeof data.issued_at === "string" ? data.issued_at : undefined,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function api(ctx, method, endpoint, body, qs, session = getSession(ctx)) {
|
|
77
|
+
return rest(ctx, method, `/services/data/${apiVersion(ctx)}${endpoint}`, body, qs, session);
|
|
78
|
+
}
|
|
79
|
+
export async function rest(ctx, method, path, body, qs, session = getSession(ctx)) {
|
|
80
|
+
const resolvedSession = await session;
|
|
81
|
+
const url = new URL(path, resolvedSession.instanceUrl);
|
|
82
|
+
if (qs) {
|
|
83
|
+
for (const [k, v] of Object.entries(qs)) {
|
|
84
|
+
if (v !== undefined && v !== null)
|
|
85
|
+
url.searchParams.set(k, String(v));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const init = {
|
|
89
|
+
method,
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: `Bearer ${resolvedSession.accessToken}`,
|
|
92
|
+
Accept: "application/json",
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
if (body && Object.keys(body).length > 0)
|
|
97
|
+
init.body = JSON.stringify(body);
|
|
98
|
+
const res = await fetch(url.toString(), init);
|
|
99
|
+
if (res.status === 204)
|
|
100
|
+
return { success: true };
|
|
101
|
+
const text = await res.text();
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
throw new Error(`Salesforce error ${res.status}: ${text}`);
|
|
104
|
+
return text ? JSON.parse(text) : null;
|
|
105
|
+
}
|
|
106
|
+
export async function identity(ctx) {
|
|
107
|
+
const session = await getSession(ctx);
|
|
108
|
+
if (!session.id) {
|
|
109
|
+
return { instanceUrl: session.instanceUrl, tokenType: session.tokenType };
|
|
110
|
+
}
|
|
111
|
+
const res = await fetch(session.id, {
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: `Bearer ${session.accessToken}`,
|
|
114
|
+
Accept: "application/json",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const text = await res.text();
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
throw new Error(`Salesforce identity error ${res.status}: ${text}`);
|
|
120
|
+
}
|
|
121
|
+
return text ? JSON.parse(text) : {};
|
|
122
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { records } from "./queryResult.js";
|
|
3
|
+
import { api } from "./shared.js";
|
|
4
|
+
export const SOBJECTS = [
|
|
5
|
+
"Account",
|
|
6
|
+
"Contact",
|
|
7
|
+
"Lead",
|
|
8
|
+
"Opportunity",
|
|
9
|
+
"Case",
|
|
10
|
+
"Task",
|
|
11
|
+
"User",
|
|
12
|
+
];
|
|
13
|
+
export const DEFAULT_FIELDS = {
|
|
14
|
+
Account: "Id,Name,Type",
|
|
15
|
+
Contact: "Id,FirstName,LastName,Email",
|
|
16
|
+
Lead: "Id,Company,FirstName,LastName,Email,Status",
|
|
17
|
+
Opportunity: "Id,AccountId,Amount,Probability,StageName",
|
|
18
|
+
Case: "Id,AccountId,ContactId,Priority,Status,Subject",
|
|
19
|
+
Task: "Id,Subject,Status,Priority",
|
|
20
|
+
User: "Id,Name,Email",
|
|
21
|
+
};
|
|
22
|
+
const Data = t.Record(t.String(), t.Unknown(), {
|
|
23
|
+
description: "Salesforce field values",
|
|
24
|
+
});
|
|
25
|
+
const QueryInput = (sObject) => t.Object({
|
|
26
|
+
fields: t.Optional(t.String({
|
|
27
|
+
description: `Comma-separated fields (default: ${DEFAULT_FIELDS[sObject] ?? "Id"})`,
|
|
28
|
+
})),
|
|
29
|
+
where: t.Optional(t.String({ description: "SOQL WHERE clause" })),
|
|
30
|
+
limit: t.Optional(t.Number({ description: "Max records" })),
|
|
31
|
+
});
|
|
32
|
+
function buildQuery(sObject, input) {
|
|
33
|
+
const p = (input ?? {});
|
|
34
|
+
const fields = p.fields || DEFAULT_FIELDS[sObject] || "Id";
|
|
35
|
+
let q = `SELECT ${fields} FROM ${sObject}`;
|
|
36
|
+
if (p.where)
|
|
37
|
+
q += ` WHERE ${p.where}`;
|
|
38
|
+
if (p.limit)
|
|
39
|
+
q += ` LIMIT ${p.limit}`;
|
|
40
|
+
return q;
|
|
41
|
+
}
|
|
42
|
+
function registerSObject(rl, sObject) {
|
|
43
|
+
const lower = sObject.toLowerCase();
|
|
44
|
+
rl.registerAction(`${lower}.create`, {
|
|
45
|
+
description: `Create a ${sObject}`,
|
|
46
|
+
inputSchema: t.Object({ data: Data }),
|
|
47
|
+
async execute(input, ctx) {
|
|
48
|
+
return api(ctx, "POST", `/sobjects/${sObject}`, input.data);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
rl.registerAction(`${lower}.get`, {
|
|
52
|
+
description: `Get a ${sObject} by ID`,
|
|
53
|
+
inputSchema: t.Object({ id: t.String() }),
|
|
54
|
+
async execute(input, ctx) {
|
|
55
|
+
return api(ctx, "GET", `/sobjects/${sObject}/${input.id}`);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
rl.registerAction(`${lower}.update`, {
|
|
59
|
+
description: `Update a ${sObject}`,
|
|
60
|
+
inputSchema: t.Object({ id: t.String(), data: Data }),
|
|
61
|
+
async execute(input, ctx) {
|
|
62
|
+
const p = input;
|
|
63
|
+
await api(ctx, "PATCH", `/sobjects/${sObject}/${p.id}`, p.data);
|
|
64
|
+
return { success: true, id: p.id };
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
rl.registerAction(`${lower}.delete`, {
|
|
68
|
+
description: `Delete a ${sObject}`,
|
|
69
|
+
inputSchema: t.Object({ id: t.String() }),
|
|
70
|
+
async execute(input, ctx) {
|
|
71
|
+
await api(ctx, "DELETE", `/sobjects/${sObject}/${input.id}`);
|
|
72
|
+
return { success: true };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
rl.registerAction(`${lower}.query`, {
|
|
76
|
+
description: `Query ${sObject}s with SOQL`,
|
|
77
|
+
inputSchema: QueryInput(sObject),
|
|
78
|
+
async execute(input, ctx) {
|
|
79
|
+
return records(await api(ctx, "GET", "/query", undefined, {
|
|
80
|
+
q: buildQuery(sObject, input),
|
|
81
|
+
}));
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
rl.registerAction(`${lower}.queryPage`, {
|
|
85
|
+
description: `Query ${sObject}s with SOQL and return Salesforce pagination metadata`,
|
|
86
|
+
inputSchema: QueryInput(sObject),
|
|
87
|
+
async execute(input, ctx) {
|
|
88
|
+
return api(ctx, "GET", "/query", undefined, {
|
|
89
|
+
q: buildQuery(sObject, input),
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
rl.registerAction(`${lower}.upsert`, {
|
|
94
|
+
description: `Upsert a ${sObject} by external ID`,
|
|
95
|
+
inputSchema: t.Object({
|
|
96
|
+
externalIdField: t.String(),
|
|
97
|
+
externalIdValue: t.String(),
|
|
98
|
+
data: Data,
|
|
99
|
+
}),
|
|
100
|
+
async execute(input, ctx) {
|
|
101
|
+
const p = input;
|
|
102
|
+
return api(ctx, "PATCH", `/sobjects/${sObject}/${p.externalIdField}/${p.externalIdValue}`, p.data);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export function registerStandardSObjectActions(rl) {
|
|
107
|
+
for (const sObject of SOBJECTS)
|
|
108
|
+
registerSObject(rl, sObject);
|
|
109
|
+
}
|
|
110
|
+
export function registerGenericSObjectActions(rl) {
|
|
111
|
+
rl.registerAction("sobject.create", {
|
|
112
|
+
description: "Create any sObject record",
|
|
113
|
+
inputSchema: t.Object({ sObject: t.String(), data: Data }),
|
|
114
|
+
async execute(input, ctx) {
|
|
115
|
+
const p = input;
|
|
116
|
+
return api(ctx, "POST", `/sobjects/${p.sObject}`, p.data);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
rl.registerAction("sobject.get", {
|
|
120
|
+
description: "Get any sObject record",
|
|
121
|
+
inputSchema: t.Object({ sObject: t.String(), id: t.String() }),
|
|
122
|
+
async execute(input, ctx) {
|
|
123
|
+
const p = input;
|
|
124
|
+
return api(ctx, "GET", `/sobjects/${p.sObject}/${p.id}`);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
rl.registerAction("sobject.update", {
|
|
128
|
+
description: "Update any sObject record",
|
|
129
|
+
inputSchema: t.Object({ sObject: t.String(), id: t.String(), data: Data }),
|
|
130
|
+
async execute(input, ctx) {
|
|
131
|
+
const p = input;
|
|
132
|
+
await api(ctx, "PATCH", `/sobjects/${p.sObject}/${p.id}`, p.data);
|
|
133
|
+
return { success: true };
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
rl.registerAction("sobject.delete", {
|
|
137
|
+
description: "Delete any sObject record",
|
|
138
|
+
inputSchema: t.Object({ sObject: t.String(), id: t.String() }),
|
|
139
|
+
async execute(input, ctx) {
|
|
140
|
+
const p = input;
|
|
141
|
+
await api(ctx, "DELETE", `/sobjects/${p.sObject}/${p.id}`);
|
|
142
|
+
return { success: true };
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
rl.registerAction("sobject.describe", {
|
|
146
|
+
description: "Describe an sObject's metadata/fields",
|
|
147
|
+
inputSchema: t.Object({ sObject: t.String() }),
|
|
148
|
+
async execute(input, ctx) {
|
|
149
|
+
return api(ctx, "GET", `/sobjects/${input.sObject}/describe`);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|