@squadbase/vite-server 0.1.9-dev.f236b23 → 0.1.10-dev.5b0c0a8
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/cli/index.js +1526 -89508
- package/dist/connectors/aws-billing.js +18 -4
- package/dist/connectors/azure-sql.js +162 -16
- package/dist/connectors/cosmosdb.d.ts +5 -0
- package/dist/connectors/cosmosdb.js +743 -0
- package/dist/connectors/google-ads.js +196 -120
- package/dist/connectors/jdbc.js +311 -20402
- package/dist/connectors/mongodb.d.ts +5 -0
- package/dist/connectors/mongodb.js +879 -0
- package/dist/connectors/oracle.js +205 -28
- package/dist/connectors/semrush.js +109 -21
- package/dist/connectors/sqlserver.js +158 -15
- package/dist/index.js +1595 -89587
- package/dist/main.js +1580 -89572
- package/dist/vite-plugin.js +1475 -89467
- package/package.json +13 -2
- package/dist/cli/cpufeatures-ORCDQN2Y.node +0 -0
- package/dist/cli/sshcrypto-P3UBA7BP.node +0 -0
- package/dist/cpufeatures-ORCDQN2Y.node +0 -0
- package/dist/sshcrypto-P3UBA7BP.node +0 -0
|
@@ -42,6 +42,131 @@ var ParameterDefinition = class {
|
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
// ../connectors/src/lib/ssh-tunnel.ts
|
|
46
|
+
var sshTunnelParameters = {
|
|
47
|
+
sshHost: new ParameterDefinition({
|
|
48
|
+
slug: "ssh-host",
|
|
49
|
+
name: "SSH Tunnel Host",
|
|
50
|
+
description: "Optional. Hostname of the SSH bastion to tunnel through. Leave empty to connect directly.",
|
|
51
|
+
envVarBaseKey: "SSH_TUNNEL_HOST",
|
|
52
|
+
type: "text",
|
|
53
|
+
secret: false,
|
|
54
|
+
required: false
|
|
55
|
+
}),
|
|
56
|
+
sshPort: new ParameterDefinition({
|
|
57
|
+
slug: "ssh-port",
|
|
58
|
+
name: "SSH Tunnel Port",
|
|
59
|
+
description: "Optional. SSH port of the bastion host (default: 22).",
|
|
60
|
+
envVarBaseKey: "SSH_TUNNEL_PORT",
|
|
61
|
+
type: "text",
|
|
62
|
+
secret: false,
|
|
63
|
+
required: false
|
|
64
|
+
}),
|
|
65
|
+
sshUsername: new ParameterDefinition({
|
|
66
|
+
slug: "ssh-username",
|
|
67
|
+
name: "SSH Tunnel Username",
|
|
68
|
+
description: "Optional. Username for SSH authentication. Required when SSH Tunnel Host is set.",
|
|
69
|
+
envVarBaseKey: "SSH_TUNNEL_USERNAME",
|
|
70
|
+
type: "text",
|
|
71
|
+
secret: false,
|
|
72
|
+
required: false
|
|
73
|
+
}),
|
|
74
|
+
sshPrivateKeyBase64: new ParameterDefinition({
|
|
75
|
+
slug: "ssh-private-key-base64",
|
|
76
|
+
name: "SSH Private Key",
|
|
77
|
+
description: "Optional. Private key (PEM, base64-encoded) used for SSH authentication. Required when SSH Tunnel Host is set.",
|
|
78
|
+
envVarBaseKey: "SSH_TUNNEL_PRIVATE_KEY_BASE64",
|
|
79
|
+
type: "base64EncodedText",
|
|
80
|
+
secret: true,
|
|
81
|
+
required: false
|
|
82
|
+
}),
|
|
83
|
+
sshPassphrase: new ParameterDefinition({
|
|
84
|
+
slug: "ssh-passphrase",
|
|
85
|
+
name: "SSH Private Key Passphrase",
|
|
86
|
+
description: "Optional. Passphrase for the SSH private key, if it is encrypted.",
|
|
87
|
+
envVarBaseKey: "SSH_TUNNEL_PASSPHRASE",
|
|
88
|
+
type: "text",
|
|
89
|
+
secret: true,
|
|
90
|
+
required: false
|
|
91
|
+
})
|
|
92
|
+
};
|
|
93
|
+
var NOOP_TUNNEL_HOSTPORT = (host, port) => ({
|
|
94
|
+
host,
|
|
95
|
+
port,
|
|
96
|
+
close: async () => {
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
function connectionParamsToRecord(connection2) {
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const p of connection2.parameters) {
|
|
102
|
+
if (p.value != null) out[p.parameterSlug] = p.value;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
async function maybeOpenSshTunnelHostPort(params, dbHost, dbPort) {
|
|
107
|
+
const sshHost = params[sshTunnelParameters.sshHost.slug];
|
|
108
|
+
if (!sshHost) return NOOP_TUNNEL_HOSTPORT(dbHost, dbPort);
|
|
109
|
+
const sshUsername = params[sshTunnelParameters.sshUsername.slug];
|
|
110
|
+
const sshPrivateKeyBase64 = params[sshTunnelParameters.sshPrivateKeyBase64.slug];
|
|
111
|
+
if (!sshUsername || !sshPrivateKeyBase64) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"SSH tunnel requires `ssh-username` and `ssh-private-key-base64` when `ssh-host` is set."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const sshPort = Number(params[sshTunnelParameters.sshPort.slug] || "22") || 22;
|
|
117
|
+
const sshPassphrase = params[sshTunnelParameters.sshPassphrase.slug];
|
|
118
|
+
const [{ Client }, net] = await Promise.all([
|
|
119
|
+
import("ssh2"),
|
|
120
|
+
import("net")
|
|
121
|
+
]);
|
|
122
|
+
const sshClient = new Client();
|
|
123
|
+
await new Promise((resolve, reject) => {
|
|
124
|
+
sshClient.once("ready", () => resolve());
|
|
125
|
+
sshClient.once("error", reject);
|
|
126
|
+
sshClient.connect({
|
|
127
|
+
host: sshHost,
|
|
128
|
+
port: sshPort,
|
|
129
|
+
username: sshUsername,
|
|
130
|
+
privateKey: Buffer.from(sshPrivateKeyBase64, "base64"),
|
|
131
|
+
passphrase: sshPassphrase || void 0,
|
|
132
|
+
readyTimeout: 1e4
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
const server = net.createServer((socket) => {
|
|
136
|
+
sshClient.forwardOut(
|
|
137
|
+
socket.remoteAddress ?? "127.0.0.1",
|
|
138
|
+
socket.remotePort ?? 0,
|
|
139
|
+
dbHost,
|
|
140
|
+
dbPort,
|
|
141
|
+
(err, stream) => {
|
|
142
|
+
if (err) {
|
|
143
|
+
socket.destroy(err);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
socket.pipe(stream).pipe(socket);
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
await new Promise((resolve, reject) => {
|
|
151
|
+
server.once("error", reject);
|
|
152
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
153
|
+
});
|
|
154
|
+
const address = server.address();
|
|
155
|
+
if (!address || typeof address === "string") {
|
|
156
|
+
server.close();
|
|
157
|
+
sshClient.end();
|
|
158
|
+
throw new Error("Failed to allocate local port for SSH tunnel.");
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
host: "127.0.0.1",
|
|
162
|
+
port: address.port,
|
|
163
|
+
close: async () => {
|
|
164
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
165
|
+
sshClient.end();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
45
170
|
// ../connectors/src/connectors/oracle/utils.ts
|
|
46
171
|
var JDBC_THIN_PREFIX_RE = /^jdbc:oracle:thin:/i;
|
|
47
172
|
var JDBC_OCI_PREFIX_RE = /^jdbc:oracle:oci/i;
|
|
@@ -107,35 +232,76 @@ function parseOracleJdbcUrl(jdbcUrl, options = {}) {
|
|
|
107
232
|
function redactOracleUrl(jdbcUrl) {
|
|
108
233
|
return jdbcUrl.replace(/(:\/\/)([^@/]+)@/, "$1***@").replace(/(thin:)([^@]+)@/i, "$1***@");
|
|
109
234
|
}
|
|
235
|
+
function parseOracleConnectStringHostPort(connectString) {
|
|
236
|
+
const m = /^([^:/]+):(\d+)(.*)$/.exec(connectString);
|
|
237
|
+
if (!m) return null;
|
|
238
|
+
return { host: m[1], port: Number(m[2]), trailing: m[3] };
|
|
239
|
+
}
|
|
240
|
+
function rewriteOracleConnectStringHostPort(connectString, host, port) {
|
|
241
|
+
const parts = parseOracleConnectStringHostPort(connectString);
|
|
242
|
+
if (!parts) return connectString;
|
|
243
|
+
return `${host}:${port}${parts.trailing}`;
|
|
244
|
+
}
|
|
110
245
|
|
|
111
246
|
// ../connectors/src/lib/oracle-runner.ts
|
|
247
|
+
var GLOBAL_ORACLEDB_PATH = "/usr/lib/node_modules/oracledb/index.js";
|
|
112
248
|
async function importOracleDb() {
|
|
113
|
-
|
|
114
|
-
|
|
249
|
+
let mod;
|
|
250
|
+
try {
|
|
251
|
+
mod = await import("oracledb");
|
|
252
|
+
} catch {
|
|
253
|
+
mod = await import(GLOBAL_ORACLEDB_PATH);
|
|
254
|
+
}
|
|
255
|
+
const resolved = mod;
|
|
256
|
+
return resolved.default ?? resolved;
|
|
115
257
|
}
|
|
116
|
-
async function runOracleQuery(parsed, sql) {
|
|
258
|
+
async function runOracleQuery(parsed, sql, options = {}) {
|
|
117
259
|
const oracledb = await importOracleDb();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
260
|
+
let tunnel = null;
|
|
261
|
+
if (options.tunnelParams) {
|
|
262
|
+
const hostPort = parseOracleConnectStringHostPort(parsed.connectString);
|
|
263
|
+
if (hostPort) {
|
|
264
|
+
tunnel = await maybeOpenSshTunnelHostPort(
|
|
265
|
+
options.tunnelParams,
|
|
266
|
+
hostPort.host,
|
|
267
|
+
hostPort.port
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
123
271
|
try {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
272
|
+
const connectString = tunnel ? rewriteOracleConnectStringHostPort(
|
|
273
|
+
parsed.connectString,
|
|
274
|
+
tunnel.host,
|
|
275
|
+
tunnel.port
|
|
276
|
+
) : parsed.connectString;
|
|
277
|
+
const connection2 = await oracledb.getConnection({
|
|
278
|
+
user: parsed.user,
|
|
279
|
+
password: parsed.password,
|
|
280
|
+
connectString
|
|
129
281
|
});
|
|
130
|
-
return { rows: result.rows ?? [] };
|
|
131
|
-
} finally {
|
|
132
282
|
try {
|
|
133
|
-
await connection2.
|
|
134
|
-
|
|
283
|
+
const result = await connection2.execute(
|
|
284
|
+
sql,
|
|
285
|
+
[],
|
|
286
|
+
{
|
|
287
|
+
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
|
288
|
+
// Bound by the connector's own row cap, but keep the driver from
|
|
289
|
+
// streaming arbitrarily large result sets.
|
|
290
|
+
maxRows: 5e3
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
return { rows: result.rows ?? [] };
|
|
294
|
+
} finally {
|
|
295
|
+
try {
|
|
296
|
+
await connection2.close();
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
135
299
|
}
|
|
300
|
+
} finally {
|
|
301
|
+
await tunnel?.close();
|
|
136
302
|
}
|
|
137
303
|
}
|
|
138
|
-
async function checkOracleConnection(url, credentials) {
|
|
304
|
+
async function checkOracleConnection(url, credentials, options = {}) {
|
|
139
305
|
let parsed;
|
|
140
306
|
try {
|
|
141
307
|
parsed = parseOracleJdbcUrl(url, credentials);
|
|
@@ -146,7 +312,7 @@ async function checkOracleConnection(url, credentials) {
|
|
|
146
312
|
};
|
|
147
313
|
}
|
|
148
314
|
try {
|
|
149
|
-
await runOracleQuery(parsed, "SELECT 1 FROM DUAL");
|
|
315
|
+
await runOracleQuery(parsed, "SELECT 1 FROM DUAL", options);
|
|
150
316
|
return { success: true };
|
|
151
317
|
} catch (err) {
|
|
152
318
|
let msg = err instanceof Error ? err.message : String(err);
|
|
@@ -183,7 +349,8 @@ var parameters = {
|
|
|
183
349
|
type: "text",
|
|
184
350
|
secret: true,
|
|
185
351
|
required: false
|
|
186
|
-
})
|
|
352
|
+
}),
|
|
353
|
+
...sshTunnelParameters
|
|
187
354
|
};
|
|
188
355
|
|
|
189
356
|
// ../connectors/src/connectors/oracle/sdk/index.ts
|
|
@@ -200,7 +367,9 @@ function createClient(params) {
|
|
|
200
367
|
async function runQuery(sql) {
|
|
201
368
|
try {
|
|
202
369
|
const cleanSql = sql.replace(/;\s*$/, "");
|
|
203
|
-
const { rows } = await runOracleQuery(parsed, cleanSql
|
|
370
|
+
const { rows } = await runOracleQuery(parsed, cleanSql, {
|
|
371
|
+
tunnelParams: params
|
|
372
|
+
});
|
|
204
373
|
return rows;
|
|
205
374
|
} catch (err) {
|
|
206
375
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -449,7 +618,9 @@ Do NOT terminate statements with a semicolon; the driver rejects trailing termin
|
|
|
449
618
|
}
|
|
450
619
|
try {
|
|
451
620
|
const cleanSql = sql.replace(/;\s*$/, "");
|
|
452
|
-
const { rows } = await runOracleQuery(parsed, cleanSql
|
|
621
|
+
const { rows } = await runOracleQuery(parsed, cleanSql, {
|
|
622
|
+
tunnelParams: connectionParamsToRecord(connection2)
|
|
623
|
+
});
|
|
453
624
|
const truncated = rows.length > MAX_ROWS;
|
|
454
625
|
return {
|
|
455
626
|
success: true,
|
|
@@ -519,10 +690,14 @@ The business logic type for this connector is "sql".
|
|
|
519
690
|
},
|
|
520
691
|
tools,
|
|
521
692
|
async checkConnection(params, _config) {
|
|
522
|
-
return checkOracleConnection(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
693
|
+
return checkOracleConnection(
|
|
694
|
+
params[parameters.jdbcUrl.slug],
|
|
695
|
+
{
|
|
696
|
+
username: params[parameters.username.slug],
|
|
697
|
+
password: params[parameters.password.slug]
|
|
698
|
+
},
|
|
699
|
+
{ tunnelParams: params }
|
|
700
|
+
);
|
|
526
701
|
},
|
|
527
702
|
async query(params, sql, _namedParams) {
|
|
528
703
|
const parsed = parseOracleJdbcUrl(params[parameters.jdbcUrl.slug], {
|
|
@@ -532,11 +707,13 @@ The business logic type for this connector is "sql".
|
|
|
532
707
|
const sample = unwrapSampleLimit(sql);
|
|
533
708
|
if (sample) {
|
|
534
709
|
const inner = sample.inner.replace(/;\s*$/, "");
|
|
535
|
-
const result = await runOracleQuery(parsed, inner
|
|
710
|
+
const result = await runOracleQuery(parsed, inner, {
|
|
711
|
+
tunnelParams: params
|
|
712
|
+
});
|
|
536
713
|
return { rows: result.rows.slice(0, sample.limit) };
|
|
537
714
|
}
|
|
538
715
|
const cleanSql = sql.replace(/;\s*$/, "");
|
|
539
|
-
return runOracleQuery(parsed, cleanSql);
|
|
716
|
+
return runOracleQuery(parsed, cleanSql, { tunnelParams: params });
|
|
540
717
|
}
|
|
541
718
|
});
|
|
542
719
|
|
|
@@ -97,13 +97,41 @@ function createClient(params) {
|
|
|
97
97
|
const { query, ...rest } = init ?? {};
|
|
98
98
|
return fetch(buildUrl(path2, query), rest);
|
|
99
99
|
}
|
|
100
|
+
async function checkUnits() {
|
|
101
|
+
const url = new URL("https://www.semrush.com/users/countapiunits.html");
|
|
102
|
+
url.searchParams.set("key", apiKey);
|
|
103
|
+
const res = await fetch(url.toString(), { method: "GET" });
|
|
104
|
+
const text = (await res.text()).trim();
|
|
105
|
+
if (text.startsWith("ERROR ")) {
|
|
106
|
+
throw new Error(`semrush checkUnits: ${text}`);
|
|
107
|
+
}
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`semrush checkUnits: ${res.status} ${res.statusText}: ${text}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const n = Number(text);
|
|
114
|
+
if (!Number.isFinite(n)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`semrush checkUnits: response is not numeric: ${text.slice(0, 200)}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return n;
|
|
120
|
+
}
|
|
100
121
|
return {
|
|
101
122
|
request,
|
|
123
|
+
checkUnits,
|
|
102
124
|
async report(type, query) {
|
|
103
125
|
const res = await request("/", { query: { type, ...query ?? {} } });
|
|
104
126
|
const text = await res.text();
|
|
105
127
|
if (text.startsWith("ERROR ")) {
|
|
106
|
-
|
|
128
|
+
const trimmed = text.trim();
|
|
129
|
+
if (/API UNITS BALANCE IS ZERO/i.test(trimmed)) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`semrush: ${trimmed}. The Semrush account has no remaining API units. Top up units in the Semrush console or call checkUnits() before report() to fail fast.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`semrush: ${trimmed}`);
|
|
107
135
|
}
|
|
108
136
|
if (!res.ok) {
|
|
109
137
|
throw new Error(
|
|
@@ -468,16 +496,18 @@ var semrushOnboarding = new ConnectorOnboarding({
|
|
|
468
496
|
- Write only 1 sentence between tool calls, then immediately call the next tool. Skip unnecessary explanations and proceed efficiently`
|
|
469
497
|
},
|
|
470
498
|
dataOverviewInstructions: {
|
|
471
|
-
en: `1. Call ${requestToolName} with path "/" and queryParams \`{ "type": "
|
|
499
|
+
en: `1. Call ${requestToolName} with path "/" and queryParams \`{ "type": "domain_ranks", "domain": "<example.com>", "database": "us" }\` to inspect the domain summary report (single-row CSV: rank, organic/paid keywords, traffic, cost)
|
|
472
500
|
2. Call ${requestToolName} with path "/" and queryParams \`{ "type": "domain_organic", "domain": "<example.com>", "database": "us", "display_limit": "5" }\` to sample organic keywords
|
|
473
501
|
3. Call ${requestToolName} with path "/" and queryParams \`{ "type": "phrase_this", "phrase": "<keyword>", "database": "us" }\` to inspect a keyword overview
|
|
474
|
-
4.
|
|
475
|
-
5.
|
|
476
|
-
|
|
502
|
+
4. Optionally call ${requestToolName} with path "/analytics/v1/" and queryParams \`{ "type": "backlinks_overview", "target": "<example.com>", "target_type": "root_domain" }\` to inspect a backlinks summary (returns semicolon-separated CSV \u2014 keep \`responseFormat="text"\`)
|
|
503
|
+
5. Always pass an explicit small \`display_limit\` (e.g. \`"5"\`) \u2014 never rely on the default 10000, which can trip \`ERROR 132\` even when units remain
|
|
504
|
+
6. Remember: the Standard Analytics API and the \`/analytics/v1/?type=backlinks_*\` Backlinks endpoints both return semicolon-separated CSV with the first row as the header`,
|
|
505
|
+
ja: `1. ${requestToolName} \u3067 path "/" \u3068 queryParams \`{ "type": "domain_ranks", "domain": "<example.com>", "database": "us" }\` \u3092\u547C\u3073\u51FA\u3057\u3001\u30C9\u30E1\u30A4\u30F3\u30B5\u30DE\u30EA\u30FC\u30EC\u30DD\u30FC\u30C8\uFF08rank\u30FB\u30AA\u30FC\u30AC\u30CB\u30C3\u30AF/\u6709\u6599\u30AD\u30FC\u30EF\u30FC\u30C9\u30FB\u30C8\u30E9\u30D5\u30A3\u30C3\u30AF\u30FB\u30B3\u30B9\u30C8\u7B49\u306E1\u884CCSV\uFF09\u3092\u78BA\u8A8D
|
|
477
506
|
2. ${requestToolName} \u3067 path "/" \u3068 queryParams \`{ "type": "domain_organic", "domain": "<example.com>", "database": "us", "display_limit": "5" }\` \u3092\u547C\u3073\u51FA\u3057\u3001\u30AA\u30FC\u30AC\u30CB\u30C3\u30AF\u30AD\u30FC\u30EF\u30FC\u30C9\u3092\u30B5\u30F3\u30D7\u30EA\u30F3\u30B0
|
|
478
507
|
3. ${requestToolName} \u3067 path "/" \u3068 queryParams \`{ "type": "phrase_this", "phrase": "<keyword>", "database": "us" }\` \u3092\u547C\u3073\u51FA\u3057\u3001\u30AD\u30FC\u30EF\u30FC\u30C9\u6982\u8981\u3092\u78BA\u8A8D
|
|
479
|
-
4. \u5FC5\u8981\u306B\u5FDC\u3058\u3066\
|
|
480
|
-
5. \
|
|
508
|
+
4. \u5FC5\u8981\u306B\u5FDC\u3058\u3066 ${requestToolName} \u3067 path "/analytics/v1/" \u3068 queryParams \`{ "type": "backlinks_overview", "target": "<example.com>", "target_type": "root_domain" }\` \u3092\u547C\u3073\u51FA\u3057\u3066\u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\u30B5\u30DE\u30EA\u30FC\u3092\u78BA\u8A8D\uFF08\u30BB\u30DF\u30B3\u30ED\u30F3\u533A\u5207\u308ACSV\u3092\u8FD4\u3059\u305F\u3081 \`responseFormat="text"\` \u306E\u307E\u307E\uFF09
|
|
509
|
+
5. \`display_limit\` \u306F\u5FC5\u305A\u660E\u793A\u7684\u306B\u5C0F\u3055\u3044\u5024\uFF08\u4F8B: \`"5"\`\uFF09\u3092\u6307\u5B9A\u3059\u308B\u3053\u3068\u3002\u30C7\u30D5\u30A9\u30EB\u30C810000\u306E\u307E\u307E\u3060\u3068\u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u304C\u3042\u3063\u3066\u3082 \`ERROR 132\` \u3067\u62D2\u5426\u3055\u308C\u308B\u3053\u3068\u304C\u3042\u308B
|
|
510
|
+
6. \u6CE8\u610F: Standard Analytics API \u304A\u3088\u3073 \`/analytics/v1/?type=backlinks_*\` \u306E\u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\u7CFB\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u306F\u3069\u3061\u3089\u3082\u30BB\u30DF\u30B3\u30ED\u30F3\u533A\u5207\u308ACSV\uFF081\u884C\u76EE\u304C\u30D8\u30C3\u30C0\u30FC\uFF09\u3092\u8FD4\u3059`
|
|
481
511
|
}
|
|
482
512
|
});
|
|
483
513
|
|
|
@@ -504,10 +534,19 @@ The business logic type for this connector is "typescript". Write handler code u
|
|
|
504
534
|
|
|
505
535
|
SDK methods (client created via \`connection(connectionId)\`):
|
|
506
536
|
- \`client.request(path, init?)\` \u2014 low-level authenticated fetch. Pass \`init.query\` to set query parameters; \`key\` is injected automatically
|
|
507
|
-
- \`client.
|
|
537
|
+
- \`client.checkUnits()\` \u2014 return the current API unit balance as a number. **FREE \u2014 does not consume units.** Use as a pre-flight before \`report()\`
|
|
538
|
+
- \`client.report(type, query?)\` \u2014 call a Standard Analytics report and parse the CSV into \`{ columns: string[], rows: Record<string, string>[], raw: string }\`. Throws \`API UNITS BALANCE IS ZERO\` when the account is out of units
|
|
508
539
|
- \`client.trends(path, query?)\` \u2014 call a Trends API endpoint (\`/analytics/v1/...\`) and return parsed JSON
|
|
509
540
|
- \`client.projects(path, init?)\` \u2014 call a Projects API endpoint (\`/management/v1/...\`) and return parsed JSON
|
|
510
541
|
|
|
542
|
+
\u{1F6A8} **MANDATORY pre-flight before creating any server logic that calls \`report()\`.** Before authoring a TypeScript handler that invokes \`client.report(...)\` (Standard Analytics \u2014 \`domain_ranks\`, \`domain_organic\`, \`phrase_this\`, etc.), you MUST first call the \`semrush-api-key_request\` tool with \`path: "https://www.semrush.com/users/countapiunits.html"\`, no \`queryParams\`, \`responseFormat: "text"\` to confirm the unit balance is greater than 0. If the balance is 0, **stop, do not create the server logic**, and report the issue to the user (the account owner needs to top up API units in the Semrush console). \`testFetchServerLogic\` will execute the handler and consume units; creating handlers when the balance is 0 produces \`ERROR 132 :: API UNITS BALANCE IS ZERO\` failures across every server logic.
|
|
543
|
+
|
|
544
|
+
\u{1F6A8} **Always set a small \`display_limit\` explicitly \u2014 never rely on the default.** Semrush computes "required units = display_limit \xD7 per-row cost" up front, and rejects the request with \`ERROR 132 :: API UNITS BALANCE IS ZERO\` when the *estimated* cost exceeds the remaining balance, **even when the actual balance is non-zero** (e.g. balance \u2248 40,950 still fails for \`display_limit=10000\` on costly reports). Default to \`display_limit: "1000"\` (or smaller \u2014 \`"100"\` is plenty for most dashboards), and only increase it when the user explicitly asks for a larger sample AND \`checkUnits()\` confirms enough headroom. If you truly need all rows, paginate with \`display_offset\` in chunks of 1000 instead of bumping \`display_limit\`. Treat \`ERROR 132\` as "request too large for the current balance" first, and only as "balance is literally zero" after re-checking units.
|
|
545
|
+
|
|
546
|
+
\u{1F6AB} **Do NOT use the Trends API (\`client.trends\`) or Projects API (\`client.projects\`) unless the user explicitly requests them.** These APIs require separate paid subscriptions (Trends and Projects/Management) that not every Semrush account has. Calls without the corresponding subscription fail with an authorization error. Default to the Standard Analytics API (\`client.report\`) for all SEO/keyword/backlink/competitor analysis. Only reach for \`trends()\` / \`projects()\` when the user explicitly mentions Trends data (traffic analytics, market explorer, etc.) or Projects (site audit, position tracking, etc.) \u2014 and confirm the user has the subscription before building the server logic.
|
|
547
|
+
|
|
548
|
+
\u26A0\uFE0F **Important: \`report()\` row shape.** \`rows\` is \`Record<string, string>[]\` \u2014 each row is an object keyed by the CSV column NAME (matching \`columns\`), NOT a positional array. Access fields with \`row["Url"]\`, \`row["Keyword"]\`, \`row["Search Volume"]\` (column names may contain spaces). Do NOT use \`columns.indexOf("Url")\` and then \`row[index]\` \u2014 that returns \`undefined\` and silently produces empty results when combined with \`?? ""\` or \`Number(...) || 0\`. All values are strings; convert numeric columns with \`Number(row["Position"])\`.
|
|
549
|
+
|
|
511
550
|
\`\`\`ts
|
|
512
551
|
import type { Context } from "hono";
|
|
513
552
|
import { connection } from "@squadbase/vite-server/connectors/semrush";
|
|
@@ -520,9 +559,25 @@ export default async function handler(c: Context) {
|
|
|
520
559
|
database?: string;
|
|
521
560
|
}>();
|
|
522
561
|
|
|
523
|
-
const
|
|
562
|
+
const result = await semrush.report("domain_organic", {
|
|
563
|
+
domain,
|
|
564
|
+
database,
|
|
565
|
+
display_limit: "100",
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// \u2705 Correct: access by column name
|
|
569
|
+
const rows = result.rows.map((row) => ({
|
|
570
|
+
keyword: row["Keyword"],
|
|
571
|
+
position: Number(row["Position"]) || 0,
|
|
572
|
+
searchVolume: Number(row["Search Volume"]) || 0,
|
|
573
|
+
url: row["Url"] ?? "",
|
|
574
|
+
}));
|
|
575
|
+
|
|
576
|
+
// \u274C Wrong: do NOT do this \u2014 row[index] is undefined because rows are objects, not arrays
|
|
577
|
+
// const urlIdx = result.columns.indexOf("Url");
|
|
578
|
+
// const url = (row as unknown as string[])[urlIdx];
|
|
524
579
|
|
|
525
|
-
return c.json({ columns:
|
|
580
|
+
return c.json({ columns: result.columns, rows });
|
|
526
581
|
}
|
|
527
582
|
\`\`\`
|
|
528
583
|
|
|
@@ -534,8 +589,8 @@ export default async function handler(c: Context) {
|
|
|
534
589
|
|
|
535
590
|
Authentication: API key passed as the \`key\` query parameter on every request (handled automatically).
|
|
536
591
|
|
|
537
|
-
#### Common Standard Analytics report types
|
|
538
|
-
- \`
|
|
592
|
+
#### Common Standard Analytics report types (path \`/\`, returns CSV)
|
|
593
|
+
- \`domain_ranks\` \u2014 single-row domain summary (rank, organic/paid keywords, traffic, cost). **There is no \`domain_overview\` type \u2014 use \`domain_ranks\`.**
|
|
539
594
|
- \`domain_organic\` \u2014 organic keywords for a domain
|
|
540
595
|
- \`domain_adwords\` \u2014 paid keywords for a domain
|
|
541
596
|
- \`domain_organic_organic\` / \`domain_adwords_adwords\` \u2014 organic / paid competitors
|
|
@@ -544,10 +599,13 @@ Authentication: API key passed as the \`key\` query parameter on every request (
|
|
|
544
599
|
- \`phrase_fullsearch\` \u2014 full-text keyword research
|
|
545
600
|
- \`phrase_questions\` \u2014 question keywords
|
|
546
601
|
- \`phrase_kdi\` \u2014 keyword difficulty index
|
|
602
|
+
- \`url_organic\` / \`url_adwords\` \u2014 keywords ranking for a specific URL
|
|
603
|
+
|
|
604
|
+
#### Backlinks API report types (path \`/analytics/v1/\`, also returns **CSV** \u2014 not JSON)
|
|
605
|
+
Backlinks endpoints live under \`/analytics/v1/?type=backlinks_*\` and require \`target\` + \`target_type\` (\`root_domain\` | \`domain\` | \`url\`) instead of \`domain\`/\`database\`. They return semicolon-separated CSV, so use \`responseFormat="text"\` from the request tool, and from the SDK use \`client.request("/analytics/v1/", { query: { type: "backlinks_overview", target, target_type: "root_domain" } })\` and parse the CSV yourself \u2014 \`client.trends()\` will throw because it JSON-parses the body.
|
|
547
606
|
- \`backlinks_overview\` \u2014 backlinks summary
|
|
548
607
|
- \`backlinks\` \u2014 list of backlinks
|
|
549
608
|
- \`backlinks_refdomains\` \u2014 referring domains
|
|
550
|
-
- \`url_organic\` / \`url_adwords\` \u2014 keywords ranking for a specific URL
|
|
551
609
|
|
|
552
610
|
To check remaining API units (free, does NOT consume units), call the request tool with an absolute URL: \`path: "https://www.semrush.com/users/countapiunits.html"\`, no query params, \`responseFormat: "text"\`. The response body is just a number.
|
|
553
611
|
|
|
@@ -555,7 +613,7 @@ To check remaining API units (free, does NOT consume units), call the request to
|
|
|
555
613
|
- \`type\` \u2014 report type (required for the Standard API)
|
|
556
614
|
- \`domain\` / \`phrase\` / \`url\` \u2014 entity to query
|
|
557
615
|
- \`database\` \u2014 regional database (e.g. \`us\`, \`uk\`, \`de\`, \`fr\`, \`jp\`, \`br\`); required for most reports
|
|
558
|
-
- \`display_limit\` \u2014 page size (default 10000, max 100000 depending on report)
|
|
616
|
+
- \`display_limit\` \u2014 page size (default 10000, max 100000 depending on report). **Do NOT use the default \u2014 always pass an explicit small value like \`"1000"\`.** Large limits make Semrush pre-charge required units and reject with \`ERROR 132\` even when the balance is non-zero.
|
|
559
617
|
- \`display_offset\` \u2014 pagination offset
|
|
560
618
|
- \`display_date\` \u2014 historical date in \`YYYYMM15\` format (always day 15)
|
|
561
619
|
- \`export_columns\` \u2014 comma-separated columns to return (e.g. \`Ph,Po,Nq,Cp\`)
|
|
@@ -566,6 +624,7 @@ To check remaining API units (free, does NOT consume units), call the request to
|
|
|
566
624
|
- Each Standard Analytics report consumes API units; check the unit balance via \`https://www.semrush.com/users/countapiunits.html?key=...\` (free) first if you suspect a quota issue
|
|
567
625
|
- The CSV separator is \`;\` (semicolon), NOT \`,\`. Some cells may contain commas inside them.
|
|
568
626
|
- An HTTP 200 response with a body starting with \`ERROR\` indicates an API error (auth, parameters, or quota)
|
|
627
|
+
- \`ERROR 132 :: API UNITS BALANCE IS ZERO\` does NOT only mean the balance is literally 0. Semrush also returns it when \`display_limit \xD7 per-row cost\` exceeds the remaining balance. Re-check the balance with \`countapiunits.html\` and lower \`display_limit\` (e.g. to \`1000\` or \`100\`) before assuming the account is empty.
|
|
569
628
|
- The Trends API requires a separate Trends subscription; calls without it will fail with an authorization error
|
|
570
629
|
- Date strings in historical endpoints must be the 15th of the month (\`YYYYMM15\`)`,
|
|
571
630
|
ja: `### \u30C4\u30FC\u30EB
|
|
@@ -578,10 +637,19 @@ To check remaining API units (free, does NOT consume units), call the request to
|
|
|
578
637
|
|
|
579
638
|
SDK\u30E1\u30BD\u30C3\u30C9 (\`connection(connectionId)\` \u3067\u4F5C\u6210\u3057\u305F\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8):
|
|
580
639
|
- \`client.request(path, init?)\` \u2014 \u8A8D\u8A3C\u4ED8\u304D\u306E\u4F4E\u30EC\u30D9\u30EBfetch\u3002\`init.query\` \u3067\u30AF\u30A8\u30EA\u30D1\u30E9\u30E1\u30FC\u30BF\u3092\u6307\u5B9A\u3002\`key\` \u306F\u81EA\u52D5\u4ED8\u4E0E
|
|
581
|
-
- \`client.
|
|
640
|
+
- \`client.checkUnits()\` \u2014 \u73FE\u5728\u306E API \u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u3092\u6570\u5024\u3067\u8FD4\u3059\u3002**\u7121\u6599\u3067\u3001\u30E6\u30CB\u30C3\u30C8\u3092\u6D88\u8CBB\u3057\u306A\u3044\u3002** \`report()\` \u5B9F\u884C\u524D\u306E\u30D7\u30EA\u30D5\u30E9\u30A4\u30C8\u306B\u4F7F\u3046
|
|
641
|
+
- \`client.report(type, query?)\` \u2014 Standard Analytics \u306E\u30EC\u30DD\u30FC\u30C8\u3092\u547C\u3073\u51FA\u3057\u3001CSV\u3092 \`{ columns: string[], rows: Record<string, string>[], raw: string }\` \u306B\u30D1\u30FC\u30B9\u3002\u6B8B\u91CF\u30BC\u30ED\u306E\u5834\u5408\u306F \`API UNITS BALANCE IS ZERO\` \u3092\u542B\u3080\u30A8\u30E9\u30FC\u3092 throw
|
|
582
642
|
- \`client.trends(path, query?)\` \u2014 Trends API \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\uFF08\`/analytics/v1/...\`\uFF09\u3092\u547C\u3073\u51FA\u3057 JSON \u3092\u8FD4\u3059
|
|
583
643
|
- \`client.projects(path, init?)\` \u2014 Projects API \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\uFF08\`/management/v1/...\`\uFF09\u3092\u547C\u3073\u51FA\u3057 JSON \u3092\u8FD4\u3059
|
|
584
644
|
|
|
645
|
+
\u{1F6A8} **\`report()\` \u3092\u547C\u3076\u30B5\u30FC\u30D0\u30FC\u30ED\u30B8\u30C3\u30AF\u3092\u4F5C\u6210\u3059\u308B\u524D\u306E\u5FC5\u9808\u30D7\u30EA\u30D5\u30E9\u30A4\u30C8\u3002** Standard Analytics\uFF08\`domain_ranks\`\u3001\`domain_organic\`\u3001\`phrase_this\` \u306A\u3069\uFF09\u306E \`client.report(...)\` \u3092\u542B\u3080 TypeScript \u30CF\u30F3\u30C9\u30E9\u3092\u4F5C\u6210\u3059\u308B\u524D\u306B\u3001\u5FC5\u305A \`semrush-api-key_request\` \u30C4\u30FC\u30EB\u3092 \`path: "https://www.semrush.com/users/countapiunits.html"\`\u3001\`queryParams\` \u7121\u3057\u3001\`responseFormat: "text"\` \u3067\u547C\u3073\u51FA\u3057\u3066\u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u304C 0 \u3088\u308A\u5927\u304D\u3044\u3053\u3068\u3092\u78BA\u8A8D\u3059\u308B\u3053\u3068\u3002\u6B8B\u91CF\u304C 0 \u306E\u5834\u5408\u306F **\u30B5\u30FC\u30D0\u30FC\u30ED\u30B8\u30C3\u30AF\u3092\u4F5C\u6210\u305B\u305A\u306B\u505C\u6B62\u3057\u3001\u30E6\u30FC\u30B6\u30FC\u306B\u5831\u544A**\u3059\u308B\uFF08Semrush \u306E\u7BA1\u7406\u30B3\u30F3\u30BD\u30FC\u30EB\u304B\u3089 API \u30E6\u30CB\u30C3\u30C8\u3092\u88DC\u5145\u3059\u308B\u5FC5\u8981\u304C\u3042\u308B\uFF09\u3002\`testFetchServerLogic\` \u306F\u30CF\u30F3\u30C9\u30E9\u3092\u5B9F\u884C\u3057\u3066\u30E6\u30CB\u30C3\u30C8\u3092\u6D88\u8CBB\u3059\u308B\u305F\u3081\u3001\u6B8B\u91CF 0 \u306E\u307E\u307E\u4F5C\u6210\u3059\u308B\u3068\u5168\u30B5\u30FC\u30D0\u30FC\u30ED\u30B8\u30C3\u30AF\u304C \`ERROR 132 :: API UNITS BALANCE IS ZERO\` \u3067\u5931\u6557\u3059\u308B\u3002
|
|
646
|
+
|
|
647
|
+
\u{1F6A8} **\`display_limit\` \u306F\u5FC5\u305A\u660E\u793A\u7684\u306B\u5C0F\u3055\u3044\u5024\u3092\u6307\u5B9A\u3057\u3001\u30C7\u30D5\u30A9\u30EB\u30C8\u306B\u983C\u3089\u306A\u3044\u3053\u3068\u3002** Semrush \u306F\u4E8B\u524D\u306B\u300C\u5FC5\u8981\u30E6\u30CB\u30C3\u30C8 = display_limit \xD7 \u884C\u3042\u305F\u308A\u5358\u4FA1\u300D\u3092\u8A08\u7B97\u3057\u3001\u305D\u306E**\u898B\u7A4D\u984D**\u304C\u6B8B\u91CF\u3092\u8D85\u3048\u308B\u3068\u30EA\u30AF\u30A8\u30B9\u30C8\u3092 \`ERROR 132 :: API UNITS BALANCE IS ZERO\` \u3067\u62D2\u5426\u3059\u308B\u3002**\u5B9F\u6B8B\u91CF\u304C\u30BC\u30ED\u3067\u306A\u304F\u3066\u3082**\u8D77\u3053\u308B\uFF08\u4F8B\uFF1A\u6B8B\u91CF\u7D04 40,950 \u3067\u3082\u3001\u30B3\u30B9\u30C8\u306E\u9AD8\u3044\u30EC\u30DD\u30FC\u30C8\u3067 \`display_limit=10000\` \u3060\u3068\u5931\u6557\u3059\u308B\uFF09\u3002\u539F\u5247\u3068\u3057\u3066 \`display_limit: "1000"\`\uFF08\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u7528\u9014\u306A\u3089 \`"100"\` \u3067\u3082\u5341\u5206\uFF09\u3092\u6307\u5B9A\u3057\u3001\u30E6\u30FC\u30B6\u30FC\u304C\u660E\u793A\u7684\u306B\u5927\u304D\u306A\u30B5\u30F3\u30D7\u30EB\u3092\u8981\u6C42\u3057\u3001\u304B\u3064 \`checkUnits()\` \u3067\u6B8B\u91CF\u306B\u5341\u5206\u306A\u4F59\u88D5\u304C\u3042\u308B\u3053\u3068\u3092\u78BA\u8A8D\u3067\u304D\u305F\u5834\u5408\u306E\u307F\u5897\u3084\u3059\u3002\u5168\u884C\u304C\u5FC5\u8981\u306A\u5834\u5408\u306F \`display_limit\` \u3092\u4E0A\u3052\u308B\u306E\u3067\u306F\u306A\u304F\u3001\`display_offset\` \u3067 1000 \u4EF6\u523B\u307F\u306E\u30DA\u30FC\u30B8\u30CD\u30FC\u30B7\u30E7\u30F3\u3092\u5B9F\u88C5\u3059\u308B\u3053\u3068\u3002\`ERROR 132\` \u306F\u300C\u6B8B\u91CF\u304C\u6587\u5B57\u901A\u308A\u30BC\u30ED\u300D\u3088\u308A\u5148\u306B\u300C\u73FE\u5728\u306E\u6B8B\u91CF\u306B\u5BFE\u3057\u3066\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u5927\u304D\u3059\u304E\u308B\u300D\u3092\u7591\u3044\u3001\u30E6\u30CB\u30C3\u30C8\u3092\u518D\u78BA\u8A8D\u3057\u3066\u304B\u3089\u5224\u65AD\u3059\u308B\u3053\u3068\u3002
|
|
648
|
+
|
|
649
|
+
\u{1F6AB} **Trends API\uFF08\`client.trends\`\uFF09\u3068 Projects API\uFF08\`client.projects\`\uFF09\u306F\u3001\u30E6\u30FC\u30B6\u30FC\u304B\u3089\u660E\u793A\u7684\u306B\u6307\u793A\u3055\u308C\u306A\u3044\u9650\u308A\u4F7F\u7528\u3057\u306A\u3044\u3053\u3068\u3002** \u3053\u308C\u3089\u306E API \u306F\u5225\u9014\u6709\u6599\u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\uFF08Trends / Projects\uFF08Management\uFF09\uFF09\u304C\u5FC5\u8981\u3067\u3001\u3059\u3079\u3066\u306E Semrush \u30A2\u30AB\u30A6\u30F3\u30C8\u304C\u5951\u7D04\u3057\u3066\u3044\u308B\u308F\u3051\u3067\u306F\u306A\u3044\u3002\u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\u304C\u306A\u3044\u72B6\u614B\u3067\u547C\u3073\u51FA\u3059\u3068\u8A8D\u53EF\u30A8\u30E9\u30FC\u3067\u5931\u6557\u3059\u308B\u3002SEO\u30FB\u30AD\u30FC\u30EF\u30FC\u30C9\u30FB\u88AB\u30EA\u30F3\u30AF\u30FB\u7AF6\u5408\u5206\u6790\u306F\u539F\u5247 Standard Analytics API\uFF08\`client.report\`\uFF09\u3067\u884C\u3046\u3002\`trends()\` / \`projects()\` \u3092\u4F7F\u3046\u306E\u306F\u3001\u30E6\u30FC\u30B6\u30FC\u304C Trends \u30C7\u30FC\u30BF\uFF08\u30C8\u30E9\u30D5\u30A3\u30C3\u30AF\u30A2\u30CA\u30EA\u30C6\u30A3\u30AF\u30B9\u3001\u30DE\u30FC\u30B1\u30C3\u30C8\u30A8\u30AF\u30B9\u30D7\u30ED\u30FC\u30E9\u30FC\u7B49\uFF09\u307E\u305F\u306F Projects \u6A5F\u80FD\uFF08\u30B5\u30A4\u30C8\u76E3\u67FB\u3001\u30DD\u30B8\u30B7\u30E7\u30F3\u30C8\u30E9\u30C3\u30AD\u30F3\u30B0\u7B49\uFF09\u3092**\u660E\u793A\u7684\u306B\u6C42\u3081\u305F\u5834\u5408\u306E\u307F**\u3067\u3001\u305D\u306E\u969B\u3082\u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\u306E\u6709\u7121\u3092\u30E6\u30FC\u30B6\u30FC\u306B\u78BA\u8A8D\u3057\u3066\u304B\u3089\u30B5\u30FC\u30D0\u30FC\u30ED\u30B8\u30C3\u30AF\u3092\u4F5C\u6210\u3059\u308B\u3053\u3068\u3002
|
|
650
|
+
|
|
651
|
+
\u26A0\uFE0F **\u91CD\u8981: \`report()\` \u306E\u884C\uFF08row\uFF09\u306E\u5F62\u72B6\u306B\u3064\u3044\u3066\u3002** \`rows\` \u306F \`Record<string, string>[]\` \u3067\u3059 \u2014 \u5404\u884C\u306F CSV \u306E\u30AB\u30E9\u30E0\u300C\u540D\u300D\uFF08\`columns\` \u3068\u4E00\u81F4\uFF09\u3092\u30AD\u30FC\u3068\u3057\u305F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308A\u3001\u4F4D\u7F6E\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u306E\u914D\u5217\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u30D5\u30A3\u30FC\u30EB\u30C9\u3078\u306E\u30A2\u30AF\u30BB\u30B9\u306F\u5FC5\u305A \`row["Url"]\`\u3001\`row["Keyword"]\`\u3001\`row["Search Volume"]\`\uFF08\u30AB\u30E9\u30E0\u540D\u306B\u30B9\u30DA\u30FC\u30B9\u3092\u542B\u3080\u3053\u3068\u3042\u308A\uFF09\u306E\u3088\u3046\u306B\u30AB\u30E9\u30E0\u540D\u3067\u884C\u3063\u3066\u304F\u3060\u3055\u3044\u3002\`columns.indexOf("Url")\` \u3067\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u3092\u53D6\u5F97\u3057\u3066\u304B\u3089 \`row[index]\` \u3067\u30A2\u30AF\u30BB\u30B9\u3057\u3066\u306F\u3044\u3051\u307E\u305B\u3093 \u2014 \u305D\u308C\u306F \`undefined\` \u3092\u8FD4\u3057\u3001\`?? ""\` \u3084 \`Number(...) || 0\` \u3068\u7D44\u307F\u5408\u308F\u3055\u308B\u3053\u3068\u3067\u7D50\u679C\u304C\u7A7A\u306B\u306A\u308B\u7121\u97F3\u306E\u5931\u6557\u3092\u751F\u307F\u307E\u3059\u3002\u5168\u3066\u306E\u5024\u306F\u6587\u5B57\u5217\u306A\u306E\u3067\u3001\u6570\u5024\u30AB\u30E9\u30E0\u306F \`Number(row["Position"])\` \u3067\u660E\u793A\u5909\u63DB\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
652
|
+
|
|
585
653
|
\`\`\`ts
|
|
586
654
|
import type { Context } from "hono";
|
|
587
655
|
import { connection } from "@squadbase/vite-server/connectors/semrush";
|
|
@@ -594,9 +662,25 @@ export default async function handler(c: Context) {
|
|
|
594
662
|
database?: string;
|
|
595
663
|
}>();
|
|
596
664
|
|
|
597
|
-
const
|
|
665
|
+
const result = await semrush.report("domain_organic", {
|
|
666
|
+
domain,
|
|
667
|
+
database,
|
|
668
|
+
display_limit: "100",
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// \u2705 \u6B63: \u30AB\u30E9\u30E0\u540D\u3067\u30A2\u30AF\u30BB\u30B9\u3059\u308B
|
|
672
|
+
const rows = result.rows.map((row) => ({
|
|
673
|
+
keyword: row["Keyword"],
|
|
674
|
+
position: Number(row["Position"]) || 0,
|
|
675
|
+
searchVolume: Number(row["Search Volume"]) || 0,
|
|
676
|
+
url: row["Url"] ?? "",
|
|
677
|
+
}));
|
|
678
|
+
|
|
679
|
+
// \u274C \u8AA4: \u884C\u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308A\u914D\u5217\u3067\u306F\u306A\u3044\u305F\u3081 row[index] \u306F undefined \u306B\u306A\u308B
|
|
680
|
+
// const urlIdx = result.columns.indexOf("Url");
|
|
681
|
+
// const url = (row as unknown as string[])[urlIdx];
|
|
598
682
|
|
|
599
|
-
return c.json({ columns:
|
|
683
|
+
return c.json({ columns: result.columns, rows });
|
|
600
684
|
}
|
|
601
685
|
\`\`\`
|
|
602
686
|
|
|
@@ -608,8 +692,8 @@ export default async function handler(c: Context) {
|
|
|
608
692
|
|
|
609
693
|
\u8A8D\u8A3C: API\u30AD\u30FC\u3092\u3059\u3079\u3066\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u306B \`key\` \u30AF\u30A8\u30EA\u30D1\u30E9\u30E1\u30FC\u30BF\u3068\u3057\u3066\u4ED8\u4E0E\uFF08\u81EA\u52D5\uFF09\u3002
|
|
610
694
|
|
|
611
|
-
#### \u4E3B\u8981\u306A Standard Analytics \u30EC\u30DD\u30FC\u30C8\u30BF\u30A4\u30D7
|
|
612
|
-
- \`
|
|
695
|
+
#### \u4E3B\u8981\u306A Standard Analytics \u30EC\u30DD\u30FC\u30C8\u30BF\u30A4\u30D7\uFF08path \`/\`\u3001CSV\u3092\u8FD4\u3059\uFF09
|
|
696
|
+
- \`domain_ranks\` \u2014 \u30C9\u30E1\u30A4\u30F3\u306E\u30B5\u30DE\u30EA\u30FC\uFF08rank\u3001\u30AA\u30FC\u30AC\u30CB\u30C3\u30AF/\u6709\u6599\u30AD\u30FC\u30EF\u30FC\u30C9\u6570\u3001\u30C8\u30E9\u30D5\u30A3\u30C3\u30AF\u3001\u30B3\u30B9\u30C8\u306E1\u884CCSV\uFF09\u3002**\`domain_overview\` \u3068\u3044\u3046\u30BF\u30A4\u30D7\u306F\u5B58\u5728\u3057\u306A\u3044\u3002\`domain_ranks\` \u3092\u4F7F\u3046\u3053\u3068\u3002**
|
|
613
697
|
- \`domain_organic\` \u2014 \u30C9\u30E1\u30A4\u30F3\u306E\u30AA\u30FC\u30AC\u30CB\u30C3\u30AF\u30AD\u30FC\u30EF\u30FC\u30C9
|
|
614
698
|
- \`domain_adwords\` \u2014 \u30C9\u30E1\u30A4\u30F3\u306E\u6709\u6599\u30AD\u30FC\u30EF\u30FC\u30C9
|
|
615
699
|
- \`domain_organic_organic\` / \`domain_adwords_adwords\` \u2014 \u30AA\u30FC\u30AC\u30CB\u30C3\u30AF\uFF0F\u6709\u6599\u306E\u7AF6\u5408
|
|
@@ -618,10 +702,13 @@ export default async function handler(c: Context) {
|
|
|
618
702
|
- \`phrase_fullsearch\` \u2014 \u30D5\u30EB\u30C6\u30AD\u30B9\u30C8\u30AD\u30FC\u30EF\u30FC\u30C9\u30EA\u30B5\u30FC\u30C1
|
|
619
703
|
- \`phrase_questions\` \u2014 \u8CEA\u554F\u5F62\u5F0F\u30AD\u30FC\u30EF\u30FC\u30C9
|
|
620
704
|
- \`phrase_kdi\` \u2014 \u30AD\u30FC\u30EF\u30FC\u30C9\u96E3\u6613\u5EA6\uFF08KDI\uFF09
|
|
705
|
+
- \`url_organic\` / \`url_adwords\` \u2014 \u7279\u5B9AURL\u3067\u30E9\u30F3\u30AF\u30A4\u30F3\u3057\u3066\u3044\u308B\u30AD\u30FC\u30EF\u30FC\u30C9
|
|
706
|
+
|
|
707
|
+
#### Backlinks API \u30EC\u30DD\u30FC\u30C8\u30BF\u30A4\u30D7\uFF08path \`/analytics/v1/\`\u3001\u3053\u3061\u3089\u3082 **CSV** \u3092\u8FD4\u3059\u3002JSON \u3067\u306F\u306A\u3044\uFF09
|
|
708
|
+
Backlinks \u7CFB\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u306F \`/analytics/v1/?type=backlinks_*\` \u914D\u4E0B\u306B\u3042\u308A\u3001\`domain\`/\`database\` \u3067\u306F\u306A\u304F \`target\` + \`target_type\`\uFF08\`root_domain\` | \`domain\` | \`url\`\uFF09\u3092\u8981\u6C42\u3059\u308B\u3002\u30EC\u30B9\u30DD\u30F3\u30B9\u306F\u30BB\u30DF\u30B3\u30ED\u30F3\u533A\u5207\u308ACSV\u306A\u306E\u3067\u3001request \u30C4\u30FC\u30EB\u3067\u306F \`responseFormat="text"\` \u3092\u4F7F\u3044\u3001SDK \u3067\u306F \`client.request("/analytics/v1/", { query: { type: "backlinks_overview", target, target_type: "root_domain" } })\` \u3092\u547C\u3093\u3067 CSV \u3092\u81EA\u524D\u3067\u30D1\u30FC\u30B9\u3059\u308B\u3053\u3068\u3002\`client.trends()\` \u306F JSON.parse \u3059\u308B\u305F\u3081 backlinks \u7CFB\u3067\u4F7F\u3046\u3068 throw \u3059\u308B\u3002
|
|
621
709
|
- \`backlinks_overview\` \u2014 \u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\u6982\u8981
|
|
622
710
|
- \`backlinks\` \u2014 \u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\u4E00\u89A7
|
|
623
711
|
- \`backlinks_refdomains\` \u2014 \u53C2\u7167\u30C9\u30E1\u30A4\u30F3
|
|
624
|
-
- \`url_organic\` / \`url_adwords\` \u2014 \u7279\u5B9AURL\u3067\u30E9\u30F3\u30AF\u30A4\u30F3\u3057\u3066\u3044\u308B\u30AD\u30FC\u30EF\u30FC\u30C9
|
|
625
712
|
|
|
626
713
|
API \u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u306E\u78BA\u8A8D\uFF08\u7121\u6599\u3001\u30E6\u30CB\u30C3\u30C8\u3092\u6D88\u8CBB\u3057\u306A\u3044\uFF09\u306F request \u30C4\u30FC\u30EB\u306B\u7D76\u5BFEURL\u3092\u6E21\u3059: \`path: "https://www.semrush.com/users/countapiunits.html"\`\u3001queryParams \u306A\u3057\u3001\`responseFormat: "text"\`\u3002\u30EC\u30B9\u30DD\u30F3\u30B9\u672C\u6587\u306F\u6570\u5024\u306E\u307F\u3002
|
|
627
714
|
|
|
@@ -629,7 +716,7 @@ API \u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u306E\u78BA\u8A8D\uFF08\u7121\u6599\u30
|
|
|
629
716
|
- \`type\` \u2014 \u30EC\u30DD\u30FC\u30C8\u7A2E\u5225\uFF08Standard API \u3067\u306F\u5FC5\u9808\uFF09
|
|
630
717
|
- \`domain\` / \`phrase\` / \`url\` \u2014 \u30AF\u30A8\u30EA\u5BFE\u8C61\u306E\u30A8\u30F3\u30C6\u30A3\u30C6\u30A3
|
|
631
718
|
- \`database\` \u2014 \u5730\u57DF\u5225\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\uFF08\`us\`, \`uk\`, \`de\`, \`fr\`, \`jp\`, \`br\` \u306A\u3069\uFF09\u3002\u591A\u304F\u306E\u30EC\u30DD\u30FC\u30C8\u3067\u5FC5\u9808
|
|
632
|
-
- \`display_limit\` \u2014 \u30DA\u30FC\u30B8\u30B5\u30A4\u30BA\uFF08\u30C7\u30D5\u30A9\u30EB\u30C810000\u3001\u30EC\u30DD\u30FC\u30C8\u306B\u3088\u3063\u3066\u306F\u6700\u5927100000\uFF09
|
|
719
|
+
- \`display_limit\` \u2014 \u30DA\u30FC\u30B8\u30B5\u30A4\u30BA\uFF08\u30C7\u30D5\u30A9\u30EB\u30C810000\u3001\u30EC\u30DD\u30FC\u30C8\u306B\u3088\u3063\u3066\u306F\u6700\u5927100000\uFF09\u3002**\u30C7\u30D5\u30A9\u30EB\u30C8\u3092\u4F7F\u308F\u305A\u3001\u5FC5\u305A \`"1000"\` \u7A0B\u5EA6\u306E\u5C0F\u3055\u3044\u5024\u3092\u660E\u793A\u7684\u306B\u6307\u5B9A\u3059\u308B\u3053\u3068\u3002** \u5927\u304D\u3044\u5024\u3060\u3068 Semrush \u304C\u5FC5\u8981\u30E6\u30CB\u30C3\u30C8\u3092\u4E8B\u524D\u8A08\u7B97\u3057\u3001\u6B8B\u91CF\u304C\u3042\u3063\u3066\u3082 \`ERROR 132\` \u3067\u62D2\u5426\u3059\u308B\u3002
|
|
633
720
|
- \`display_offset\` \u2014 \u30DA\u30FC\u30B8\u30CD\u30FC\u30B7\u30E7\u30F3\u30AA\u30D5\u30BB\u30C3\u30C8
|
|
634
721
|
- \`display_date\` \u2014 \u5C65\u6B74\u306E\u65E5\u4ED8\u3002\`YYYYMM15\` \u5F62\u5F0F\uFF08\u5FC5\u305A\u6708\u306E15\u65E5\uFF09
|
|
635
722
|
- \`export_columns\` \u2014 \u8FD4\u5374\u30AB\u30E9\u30E0\u3092\u30AB\u30F3\u30DE\u533A\u5207\u308A\u3067\u6307\u5B9A\uFF08\u4F8B: \`Ph,Po,Nq,Cp\`\uFF09
|
|
@@ -640,6 +727,7 @@ API \u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u306E\u78BA\u8A8D\uFF08\u7121\u6599\u30
|
|
|
640
727
|
- Standard Analytics \u306E\u5404\u30EC\u30DD\u30FC\u30C8\u306F API \u30E6\u30CB\u30C3\u30C8\u3092\u6D88\u8CBB\u3059\u308B\u3002\u30AF\u30A9\u30FC\u30BF\u304C\u7591\u308F\u3057\u3044\u5834\u5408\u306F \`https://www.semrush.com/users/countapiunits.html?key=...\` \u3067\u6B8B\u91CF\u3092\u5148\u306B\u78BA\u8A8D\u3059\u308B\uFF08\u7121\u6599\uFF09
|
|
641
728
|
- CSV \u306E\u30BB\u30D1\u30EC\u30FC\u30BF\u306F \`;\`\uFF08\u30BB\u30DF\u30B3\u30ED\u30F3\uFF09\u3067\u3042\u308A \`,\` \u3067\u306F\u306A\u3044\u3002\u30BB\u30EB\u5185\u306B\u30AB\u30F3\u30DE\u304C\u542B\u307E\u308C\u308B\u3053\u3068\u304C\u3042\u308B
|
|
642
729
|
- HTTP 200 \u3067\u3082\u672C\u6587\u304C \`ERROR\` \u3067\u59CB\u307E\u308B\u5834\u5408\u306F API\u30A8\u30E9\u30FC\uFF08\u8A8D\u8A3C\u3001\u30D1\u30E9\u30E1\u30FC\u30BF\u3001\u30AF\u30A9\u30FC\u30BF\uFF09
|
|
730
|
+
- \`ERROR 132 :: API UNITS BALANCE IS ZERO\` \u306F\u300C\u6B8B\u91CF\u304C\u6587\u5B57\u901A\u308A 0\u300D\u3060\u3051\u3092\u610F\u5473\u3059\u308B\u308F\u3051\u3067\u306F\u306A\u3044\u3002Semrush \u306F \`display_limit \xD7 \u884C\u3042\u305F\u308A\u5358\u4FA1\` \u304C\u6B8B\u91CF\u3092\u8D85\u3048\u308B\u5834\u5408\u3082\u540C\u3058\u30A8\u30E9\u30FC\u3092\u8FD4\u3059\u3002\`countapiunits.html\` \u3067\u6B8B\u91CF\u3092\u518D\u78BA\u8A8D\u3057\u3001\`display_limit\` \u3092 \`1000\` \u3084 \`100\` \u307E\u3067\u4E0B\u3052\u3066\u304B\u3089\u300C\u6B8B\u91CF\u5207\u308C\u300D\u3068\u5224\u65AD\u3059\u308B\u3053\u3068\u3002
|
|
643
731
|
- Trends API \u306F\u5225\u9014 Trends \u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\u304C\u5FC5\u8981\u3002\u672A\u5951\u7D04\u3060\u3068\u8A8D\u53EF\u30A8\u30E9\u30FC\u306B\u306A\u308B
|
|
644
732
|
- \u5C65\u6B74\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u306E\u65E5\u4ED8\u306F\u5FC5\u305A\u6708\u306E15\u65E5\uFF08\`YYYYMM15\`\uFF09`
|
|
645
733
|
},
|