@squadbase/vite-server 0.1.17-dev.a107052 → 0.1.17-dev.d4fff69
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 +4284 -820
- package/dist/connectors/airtable-oauth.js +48 -8
- package/dist/connectors/airtable.js +44 -8
- package/dist/connectors/amplitude.js +8 -8
- package/dist/connectors/anthropic.js +2 -2
- package/dist/connectors/asana.js +37 -10
- package/dist/connectors/attio.js +30 -13
- package/dist/connectors/aws-billing.js +8 -8
- package/dist/connectors/azure-sql.js +47 -10
- package/dist/connectors/backlog-api-key.js +40 -15
- package/dist/connectors/clickup.js +50 -10
- package/dist/connectors/cosmosdb.js +12 -12
- package/dist/connectors/customerio.js +8 -8
- package/dist/connectors/dbt.js +686 -25
- package/dist/connectors/freshdesk.js +82 -8
- package/dist/connectors/freshsales.js +8 -8
- package/dist/connectors/freshservice.js +8 -8
- package/dist/connectors/gamma.js +15 -15
- package/dist/connectors/gemini.js +2 -2
- package/dist/connectors/github.js +12 -12
- package/dist/connectors/gmail-oauth.js +10 -10
- package/dist/connectors/gmail.js +4 -4
- package/dist/connectors/google-ads.js +8 -8
- package/dist/connectors/google-analytics-oauth.js +152 -25
- package/dist/connectors/google-analytics.js +475 -95
- package/dist/connectors/google-audit-log.js +4 -4
- package/dist/connectors/google-calendar-oauth.js +61 -15
- package/dist/connectors/google-calendar.js +61 -11
- package/dist/connectors/google-docs.js +10 -10
- package/dist/connectors/google-drive.js +32 -10
- package/dist/connectors/google-search-console-oauth.js +126 -17
- package/dist/connectors/google-sheets.js +6 -6
- package/dist/connectors/google-slides.js +10 -10
- package/dist/connectors/grafana.js +45 -10
- package/dist/connectors/hackernews.d.ts +5 -0
- package/dist/connectors/hackernews.js +890 -0
- package/dist/connectors/hubspot-oauth.js +41 -9
- package/dist/connectors/hubspot.js +25 -9
- package/dist/connectors/influxdb.js +8 -8
- package/dist/connectors/intercom-oauth.js +72 -12
- package/dist/connectors/intercom.js +12 -12
- package/dist/connectors/jdbc.js +37 -10
- package/dist/connectors/jira-api-key.js +68 -11
- package/dist/connectors/kintone-api-token.js +66 -18
- package/dist/connectors/kintone.js +54 -11
- package/dist/connectors/linear.js +54 -12
- package/dist/connectors/linkedin-ads.js +41 -14
- package/dist/connectors/mailchimp-oauth.js +6 -6
- package/dist/connectors/mailchimp.js +6 -6
- package/dist/connectors/meta-ads-oauth.js +33 -14
- package/dist/connectors/meta-ads.js +35 -14
- package/dist/connectors/mixpanel.js +8 -8
- package/dist/connectors/monday.js +9 -9
- package/dist/connectors/mongodb.js +8 -8
- package/dist/connectors/notion-oauth.js +60 -11
- package/dist/connectors/notion.js +60 -11
- package/dist/connectors/openai.js +2 -2
- package/dist/connectors/oracle.js +39 -11
- package/dist/connectors/outlook-oauth.js +21 -21
- package/dist/connectors/powerbi-oauth.js +13 -13
- package/dist/connectors/salesforce.js +42 -9
- package/dist/connectors/semrush.js +6 -6
- package/dist/connectors/sentry.js +36 -10
- package/dist/connectors/shopify-oauth.js +43 -10
- package/dist/connectors/shopify.js +8 -8
- package/dist/connectors/sqlserver.js +47 -10
- package/dist/connectors/stripe-api-key.js +66 -15
- package/dist/connectors/stripe-oauth.js +70 -19
- package/dist/connectors/supabase.js +31 -6
- package/dist/connectors/tableau.js +15 -15
- package/dist/connectors/tiktok-ads.js +37 -16
- package/dist/connectors/wix-store.js +8 -8
- package/dist/connectors/x.d.ts +5 -0
- package/dist/connectors/x.js +927 -0
- package/dist/connectors/zendesk-oauth.js +55 -12
- package/dist/connectors/zendesk.js +12 -12
- package/dist/index.js +4302 -818
- package/dist/main.js +4302 -818
- package/dist/vite-plugin.js +4282 -818
- package/package.json +9 -1
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
// ../connectors/src/connectors/hackernews/sdk/index.ts
|
|
2
|
+
var FIREBASE_BASE_URL = "https://hacker-news.firebaseio.com/v0";
|
|
3
|
+
var ALGOLIA_BASE_URL = "https://hn.algolia.com/api/v1";
|
|
4
|
+
function createClient(_params) {
|
|
5
|
+
function buildUrl(base, path2, query) {
|
|
6
|
+
if (/^https?:\/\//i.test(path2)) {
|
|
7
|
+
throw new Error("hackernews: absolute URLs are not allowed");
|
|
8
|
+
}
|
|
9
|
+
const url = new URL(`${base}${path2.startsWith("/") ? "" : "/"}${path2}`);
|
|
10
|
+
if (query) {
|
|
11
|
+
for (const [k, v] of Object.entries(query)) {
|
|
12
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return url.toString();
|
|
16
|
+
}
|
|
17
|
+
async function parseJson(res) {
|
|
18
|
+
const text = await res.text();
|
|
19
|
+
const data = text ? JSON.parse(text) : null;
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
throw new Error(`hackernews: ${res.status} ${res.statusText}: ${text}`);
|
|
22
|
+
}
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
async function requestFirebase(path2) {
|
|
26
|
+
return fetch(buildUrl(FIREBASE_BASE_URL, path2), {
|
|
27
|
+
method: "GET",
|
|
28
|
+
headers: { Accept: "application/json" }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function requestAlgolia(path2, options) {
|
|
32
|
+
return fetch(buildUrl(ALGOLIA_BASE_URL, path2, options?.query), {
|
|
33
|
+
method: "GET",
|
|
34
|
+
headers: { Accept: "application/json" }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function getStoryIds(kind, options) {
|
|
38
|
+
const pathByKind = {
|
|
39
|
+
top: "/topstories.json",
|
|
40
|
+
new: "/newstories.json",
|
|
41
|
+
best: "/beststories.json",
|
|
42
|
+
ask: "/askstories.json",
|
|
43
|
+
show: "/showstories.json",
|
|
44
|
+
job: "/jobstories.json"
|
|
45
|
+
};
|
|
46
|
+
const ids = await parseJson(
|
|
47
|
+
await requestFirebase(pathByKind[kind])
|
|
48
|
+
);
|
|
49
|
+
return ids.slice(0, options?.limit ?? ids.length);
|
|
50
|
+
}
|
|
51
|
+
async function getItem(id) {
|
|
52
|
+
return parseJson(
|
|
53
|
+
await requestFirebase(`/item/${id}.json`)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
requestFirebase,
|
|
58
|
+
requestAlgolia,
|
|
59
|
+
getItem,
|
|
60
|
+
async getUser(username) {
|
|
61
|
+
return parseJson(
|
|
62
|
+
await requestFirebase(`/user/${encodeURIComponent(username)}.json`)
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
async getMaxItem() {
|
|
66
|
+
return parseJson(await requestFirebase("/maxitem.json"));
|
|
67
|
+
},
|
|
68
|
+
async getUpdates() {
|
|
69
|
+
return parseJson(
|
|
70
|
+
await requestFirebase("/updates.json")
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
getStoryIds,
|
|
74
|
+
async getItems(ids, options) {
|
|
75
|
+
const limit = options?.limit ?? ids.length;
|
|
76
|
+
const concurrency = Math.max(1, Math.min(options?.concurrency ?? 5, 10));
|
|
77
|
+
const targetIds = ids.slice(0, limit);
|
|
78
|
+
const results = [];
|
|
79
|
+
for (let i = 0; i < targetIds.length; i += concurrency) {
|
|
80
|
+
const batch = targetIds.slice(i, i + concurrency);
|
|
81
|
+
const items = await Promise.all(batch.map((id) => getItem(id)));
|
|
82
|
+
for (const item of items) {
|
|
83
|
+
if (item) results.push(item);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
},
|
|
88
|
+
async search(query, options) {
|
|
89
|
+
const path2 = options?.byDate ? "/search_by_date" : "/search";
|
|
90
|
+
return parseJson(
|
|
91
|
+
await requestAlgolia(path2, {
|
|
92
|
+
query: { query, hitsPerPage: 20, ...options?.query ?? {} }
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ../connectors/src/connector-onboarding.ts
|
|
100
|
+
var ConnectorOnboarding = class {
|
|
101
|
+
/** Phase 1: Connection setup instructions (optional — some connectors don't need this) */
|
|
102
|
+
connectionSetupInstructions;
|
|
103
|
+
/** Phase 2: Data overview instructions */
|
|
104
|
+
dataOverviewInstructions;
|
|
105
|
+
constructor(config) {
|
|
106
|
+
this.connectionSetupInstructions = config.connectionSetupInstructions;
|
|
107
|
+
this.dataOverviewInstructions = config.dataOverviewInstructions;
|
|
108
|
+
}
|
|
109
|
+
getConnectionSetupPrompt(language) {
|
|
110
|
+
return this.connectionSetupInstructions?.[language] ?? null;
|
|
111
|
+
}
|
|
112
|
+
getDataOverviewInstructions(language) {
|
|
113
|
+
return this.dataOverviewInstructions[language];
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ../connectors/src/connector-tool.ts
|
|
118
|
+
var ConnectorTool = class {
|
|
119
|
+
name;
|
|
120
|
+
description;
|
|
121
|
+
inputSchema;
|
|
122
|
+
outputSchema;
|
|
123
|
+
_execute;
|
|
124
|
+
constructor(config) {
|
|
125
|
+
this.name = config.name;
|
|
126
|
+
this.description = config.description;
|
|
127
|
+
this.inputSchema = config.inputSchema;
|
|
128
|
+
this.outputSchema = config.outputSchema;
|
|
129
|
+
this._execute = config.execute;
|
|
130
|
+
}
|
|
131
|
+
createTool(connections, config) {
|
|
132
|
+
return {
|
|
133
|
+
description: this.description,
|
|
134
|
+
inputSchema: this.inputSchema,
|
|
135
|
+
outputSchema: this.outputSchema,
|
|
136
|
+
execute: (input) => this._execute(input, connections, config)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// ../connectors/src/connector-plugin.ts
|
|
142
|
+
var ConnectorPlugin = class _ConnectorPlugin {
|
|
143
|
+
slug;
|
|
144
|
+
authType;
|
|
145
|
+
name;
|
|
146
|
+
description;
|
|
147
|
+
iconUrl;
|
|
148
|
+
parameters;
|
|
149
|
+
releaseFlag;
|
|
150
|
+
proxyPolicy;
|
|
151
|
+
experimentalAttributes;
|
|
152
|
+
categories;
|
|
153
|
+
onboarding;
|
|
154
|
+
systemPrompt;
|
|
155
|
+
tools;
|
|
156
|
+
query;
|
|
157
|
+
checkConnection;
|
|
158
|
+
/**
|
|
159
|
+
* SQPD-1212: Logic-based, rule-driven connection setup. Connectors that
|
|
160
|
+
* implement this expose a step-by-step exploration flow (database/schema/
|
|
161
|
+
* table/etc. discovery) that the dashboard backend drives via the
|
|
162
|
+
* `/connections/:connectionId/setup` endpoint. Implement by delegating to
|
|
163
|
+
* `runSetupFlow` from `setup-flow.ts`.
|
|
164
|
+
*/
|
|
165
|
+
setup;
|
|
166
|
+
/**
|
|
167
|
+
* Opt-out of the default "verify before save" behavior on connection
|
|
168
|
+
* creation. The backend invokes `checkConnection` synchronously while
|
|
169
|
+
* creating the connection and aborts (no row inserted) if it fails — this
|
|
170
|
+
* flag disables that for connectors where the check cannot succeed pre-save:
|
|
171
|
+
*
|
|
172
|
+
* - `squadbase-db` populates `connection-url` only after Neon provisioning
|
|
173
|
+
* - OAuth connectors require an OAuth-aware proxyFetch keyed by the
|
|
174
|
+
* connectionId, which doesn't exist until the row is saved
|
|
175
|
+
*
|
|
176
|
+
* Exceptions are the explicit position; new credential-input connectors get
|
|
177
|
+
* the default verify-on-create behavior without opt-in.
|
|
178
|
+
*/
|
|
179
|
+
skipConnectionCheckOnCreate;
|
|
180
|
+
constructor(config) {
|
|
181
|
+
this.slug = config.slug;
|
|
182
|
+
this.authType = config.authType;
|
|
183
|
+
this.name = config.name;
|
|
184
|
+
this.description = config.description;
|
|
185
|
+
this.iconUrl = config.iconUrl;
|
|
186
|
+
this.parameters = config.parameters;
|
|
187
|
+
this.releaseFlag = config.releaseFlag;
|
|
188
|
+
this.proxyPolicy = config.proxyPolicy;
|
|
189
|
+
this.experimentalAttributes = config.experimentalAttributes;
|
|
190
|
+
this.categories = config.categories ?? [];
|
|
191
|
+
this.onboarding = config.onboarding;
|
|
192
|
+
this.systemPrompt = config.systemPrompt;
|
|
193
|
+
this.tools = config.tools;
|
|
194
|
+
this.query = config.query;
|
|
195
|
+
this.checkConnection = config.checkConnection;
|
|
196
|
+
this.setup = config.setup;
|
|
197
|
+
this.skipConnectionCheckOnCreate = config.skipConnectionCheckOnCreate;
|
|
198
|
+
}
|
|
199
|
+
get connectorKey() {
|
|
200
|
+
return _ConnectorPlugin.deriveKey(this.slug, this.authType);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Create tools for connections that belong to this connector.
|
|
204
|
+
* Filters connections by connectorKey internally.
|
|
205
|
+
* Returns tools keyed as `connector_${connectorKey}_${toolName}`.
|
|
206
|
+
*/
|
|
207
|
+
createTools(connections, config, opts) {
|
|
208
|
+
const myConnections = connections.filter(
|
|
209
|
+
(c) => _ConnectorPlugin.deriveKey(c.connector.slug, c.connector.authType) === this.connectorKey
|
|
210
|
+
);
|
|
211
|
+
const result = {};
|
|
212
|
+
for (const t of Object.values(this.tools)) {
|
|
213
|
+
const tool = t.createTool(myConnections, config);
|
|
214
|
+
const originalToModelOutput = tool.toModelOutput;
|
|
215
|
+
result[`connector_${this.connectorKey}_${t.name}`] = {
|
|
216
|
+
...tool,
|
|
217
|
+
toModelOutput: async (options) => {
|
|
218
|
+
if (!originalToModelOutput) {
|
|
219
|
+
return opts.truncateOutput(options.output);
|
|
220
|
+
}
|
|
221
|
+
const modelOutput = await originalToModelOutput(options);
|
|
222
|
+
if (modelOutput.type === "text" || modelOutput.type === "json") {
|
|
223
|
+
return opts.truncateOutput(modelOutput.value);
|
|
224
|
+
}
|
|
225
|
+
return modelOutput;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
static deriveKey(slug, authType) {
|
|
232
|
+
if (authType) return `${slug}-${authType}`;
|
|
233
|
+
const LEGACY_NULL_AUTH_TYPE_MAP = {
|
|
234
|
+
// user-password
|
|
235
|
+
"postgresql": "user-password",
|
|
236
|
+
"mysql": "user-password",
|
|
237
|
+
"clickhouse": "user-password",
|
|
238
|
+
"kintone": "user-password",
|
|
239
|
+
"squadbase-db": "user-password",
|
|
240
|
+
// service-account
|
|
241
|
+
"snowflake": "service-account",
|
|
242
|
+
"bigquery": "service-account",
|
|
243
|
+
"google-analytics": "service-account",
|
|
244
|
+
"google-calendar": "service-account",
|
|
245
|
+
"aws-athena": "service-account",
|
|
246
|
+
"redshift": "service-account",
|
|
247
|
+
// api-key
|
|
248
|
+
"databricks": "api-key",
|
|
249
|
+
"dbt": "api-key",
|
|
250
|
+
"airtable": "api-key",
|
|
251
|
+
"openai": "api-key",
|
|
252
|
+
"gemini": "api-key",
|
|
253
|
+
"anthropic": "api-key",
|
|
254
|
+
"wix-store": "api-key"
|
|
255
|
+
};
|
|
256
|
+
const fallbackAuthType = LEGACY_NULL_AUTH_TYPE_MAP[slug];
|
|
257
|
+
if (fallbackAuthType) return `${slug}-${fallbackAuthType}`;
|
|
258
|
+
return slug;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// ../connectors/src/setup-flow.ts
|
|
263
|
+
async function runSetupFlow(flow, params, ctx, config) {
|
|
264
|
+
const runtime = {
|
|
265
|
+
params,
|
|
266
|
+
language: ctx.language,
|
|
267
|
+
config
|
|
268
|
+
};
|
|
269
|
+
let state = flow.initialState();
|
|
270
|
+
let answerIdx = 0;
|
|
271
|
+
const pendingParameterUpdates = [];
|
|
272
|
+
for (const step of flow.steps) {
|
|
273
|
+
const ans = ctx.answers[answerIdx];
|
|
274
|
+
if (ans && ans.questionSlug === step.slug) {
|
|
275
|
+
state = step.applyAnswer(state, ans.answer);
|
|
276
|
+
if (step.toParameterUpdates) {
|
|
277
|
+
pendingParameterUpdates.push(...step.toParameterUpdates(state));
|
|
278
|
+
}
|
|
279
|
+
answerIdx += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const resolvedAllowFreeText = step.allowFreeText !== void 0 ? step.allowFreeText : true;
|
|
283
|
+
if (step.type === "text") {
|
|
284
|
+
if (step.fetchOptions) {
|
|
285
|
+
const options2 = await step.fetchOptions(state, runtime);
|
|
286
|
+
if (options2.length === 0) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
type: "nextQuestion",
|
|
292
|
+
questionSlug: step.slug,
|
|
293
|
+
question: step.question[ctx.language],
|
|
294
|
+
questionType: "text",
|
|
295
|
+
allowFreeText: resolvedAllowFreeText,
|
|
296
|
+
...pendingParameterUpdates.length > 0 && {
|
|
297
|
+
parameterUpdates: pendingParameterUpdates
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const options = step.fetchOptions ? await step.fetchOptions(state, runtime) : [];
|
|
302
|
+
if (options.length === 0) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
type: "nextQuestion",
|
|
307
|
+
questionSlug: step.slug,
|
|
308
|
+
question: step.question[ctx.language],
|
|
309
|
+
questionType: step.type,
|
|
310
|
+
options,
|
|
311
|
+
allowFreeText: resolvedAllowFreeText,
|
|
312
|
+
...pendingParameterUpdates.length > 0 && {
|
|
313
|
+
parameterUpdates: pendingParameterUpdates
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const dataInvestigationResult = await flow.finalize(state, runtime);
|
|
318
|
+
return {
|
|
319
|
+
type: "fulfilled",
|
|
320
|
+
dataInvestigationResult,
|
|
321
|
+
...pendingParameterUpdates.length > 0 && {
|
|
322
|
+
parameterUpdates: pendingParameterUpdates
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ../connectors/src/auth-types.ts
|
|
328
|
+
var AUTH_TYPES = {
|
|
329
|
+
OAUTH: "oauth",
|
|
330
|
+
API_KEY: "api-key",
|
|
331
|
+
JWT: "jwt",
|
|
332
|
+
SERVICE_ACCOUNT: "service-account",
|
|
333
|
+
PAT: "pat",
|
|
334
|
+
USER_PASSWORD: "user-password"
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// ../connectors/src/connectors/hackernews/setup.ts
|
|
338
|
+
var hackerNewsOnboarding = new ConnectorOnboarding({
|
|
339
|
+
dataOverviewInstructions: {
|
|
340
|
+
en: `1. Use connector_hackernews-api-key_requestFirebase with GET /topstories.json to inspect current top story ids
|
|
341
|
+
2. Fetch a small sample of items with GET /item/{id}.json to understand story fields such as title, url, score, descendants, by, and time
|
|
342
|
+
3. Use connector_hackernews-api-key_searchAlgolia only when keyword or historical search is required
|
|
343
|
+
4. Prefer Firebase list endpoints for dashboards that show top, new, best, Ask HN, Show HN, or job stories`,
|
|
344
|
+
ja: `1. connector_hackernews-api-key_requestFirebase \u3067 GET /topstories.json \u3092\u547C\u3073\u51FA\u3057\u3001\u73FE\u5728\u306E\u30C8\u30C3\u30D7\u30B9\u30C8\u30FC\u30EA\u30FCID\u3092\u78BA\u8A8D\u3057\u307E\u3059
|
|
345
|
+
2. GET /item/{id}.json \u3067\u5C11\u91CF\u306E item \u3092\u53D6\u5F97\u3057\u3001title, url, score, descendants, by, time \u306A\u3069\u306E\u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u628A\u63E1\u3057\u307E\u3059
|
|
346
|
+
3. \u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u3084\u5C65\u6B74\u691C\u7D22\u304C\u5FC5\u8981\u306A\u5834\u5408\u306E\u307F connector_hackernews-api-key_searchAlgolia \u3092\u4F7F\u7528\u3057\u307E\u3059
|
|
347
|
+
4. top, new, best, Ask HN, Show HN, job stories \u306E\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3067\u306F Firebase \u306E\u30EA\u30B9\u30C8\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u3092\u512A\u5148\u3057\u307E\u3059`
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ../connectors/src/connectors/hackernews/setup-flow.ts
|
|
352
|
+
var hackerNewsSetupFlow = {
|
|
353
|
+
initialState: () => ({}),
|
|
354
|
+
steps: [],
|
|
355
|
+
async finalize(_state, rt) {
|
|
356
|
+
const sections = rt.language === "ja" ? [
|
|
357
|
+
"## Hacker News",
|
|
358
|
+
"",
|
|
359
|
+
"- \u8A8D\u8A3C\u60C5\u5831\u306E\u5165\u529B\u306F\u4E0D\u8981\u3067\u3059\u3002",
|
|
360
|
+
"- \u901A\u5E38\u306E\u30E9\u30F3\u30AD\u30F3\u30B0\u3001item\u3001user\u3001updates \u306E\u53D6\u5F97\u306B\u306F\u516C\u5F0F Firebase API \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
361
|
+
"- Algolia API \u306F\u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u3084\u671F\u9593\u691C\u7D22\u304C\u5FC5\u8981\u306A\u6642\u3060\u3051\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
362
|
+
"- \u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u5B9F\u88C5\u3067\u306F\u3001item \u8A73\u7D30\u53D6\u5F97\u306E\u4E26\u5217\u6570\u3068\u4EF6\u6570\u3092\u6291\u3048\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
363
|
+
] : [
|
|
364
|
+
"## Hacker News",
|
|
365
|
+
"",
|
|
366
|
+
"- No credential input is required.",
|
|
367
|
+
"- Use the official Firebase API for rankings, items, users, and updates.",
|
|
368
|
+
"- Use the Algolia API only when keyword or date-range search is required.",
|
|
369
|
+
"- When building dashboards, keep item-detail fetch concurrency and result counts bounded."
|
|
370
|
+
];
|
|
371
|
+
return sections.join("\n");
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// ../connectors/src/connectors/hackernews/tools/request-firebase.ts
|
|
376
|
+
import { z } from "zod";
|
|
377
|
+
var BASE_URL = "https://hacker-news.firebaseio.com/v0";
|
|
378
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
379
|
+
var inputSchema = z.object({
|
|
380
|
+
toolUseIntent: z.string().optional().describe(
|
|
381
|
+
"Brief description of what you intend to accomplish with this tool call"
|
|
382
|
+
),
|
|
383
|
+
connectionId: z.string().describe(
|
|
384
|
+
"ID of the Hacker News connection to use. This connector has no credentials, but the connection ID identifies the selected data source."
|
|
385
|
+
),
|
|
386
|
+
path: z.string().describe(
|
|
387
|
+
"Firebase API path appended to https://hacker-news.firebaseio.com/v0. Examples: '/topstories.json', '/newstories.json', '/beststories.json', '/askstories.json', '/showstories.json', '/jobstories.json', '/item/8863.json', '/user/jl.json', '/maxitem.json', '/updates.json'."
|
|
388
|
+
),
|
|
389
|
+
printPretty: z.boolean().optional().describe("Set true to append print=pretty for readable debugging output.")
|
|
390
|
+
});
|
|
391
|
+
var outputSchema = z.discriminatedUnion("success", [
|
|
392
|
+
z.object({
|
|
393
|
+
success: z.literal(true),
|
|
394
|
+
status: z.number(),
|
|
395
|
+
data: z.unknown()
|
|
396
|
+
}),
|
|
397
|
+
z.object({
|
|
398
|
+
success: z.literal(false),
|
|
399
|
+
status: z.number().optional(),
|
|
400
|
+
error: z.string()
|
|
401
|
+
})
|
|
402
|
+
]);
|
|
403
|
+
var requestFirebaseTool = new ConnectorTool({
|
|
404
|
+
name: "requestFirebase",
|
|
405
|
+
description: `Fetch public Hacker News data from the official Firebase API.
|
|
406
|
+
Use this tool for normal dashboard data: top/new/best/ask/show/job story IDs, individual items, users, max item ID, and updates. The Firebase API is the preferred source whenever you do not need full-text search.
|
|
407
|
+
|
|
408
|
+
This connector does not require API credentials, but a connectionId is still required to select the Hacker News data source. Keep dashboard calls bounded: fetch list IDs first, then fetch only the small slice of item details needed for the current view. Use searchAlgolia only when keyword, tag, or historical search is necessary.`,
|
|
409
|
+
inputSchema,
|
|
410
|
+
outputSchema,
|
|
411
|
+
async execute({ connectionId, path: path2, printPretty }, connections) {
|
|
412
|
+
const connection2 = connections.find((c) => c.id === connectionId);
|
|
413
|
+
if (!connection2) {
|
|
414
|
+
return {
|
|
415
|
+
success: false,
|
|
416
|
+
error: `Connection ${connectionId} not found`
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
console.log(
|
|
420
|
+
`[connector-request] hackernews/${connection2.name}: GET ${path2}`
|
|
421
|
+
);
|
|
422
|
+
try {
|
|
423
|
+
if (/^https?:\/\//i.test(path2)) {
|
|
424
|
+
return {
|
|
425
|
+
success: false,
|
|
426
|
+
error: "Absolute URLs are not allowed. Pass a Firebase path such as /topstories.json."
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const url = new URL(
|
|
430
|
+
`${BASE_URL}${path2.startsWith("/") ? "" : "/"}${path2}`
|
|
431
|
+
);
|
|
432
|
+
if (printPretty) url.searchParams.set("print", "pretty");
|
|
433
|
+
const controller = new AbortController();
|
|
434
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
435
|
+
try {
|
|
436
|
+
const response = await fetch(url.toString(), {
|
|
437
|
+
method: "GET",
|
|
438
|
+
headers: { Accept: "application/json" },
|
|
439
|
+
signal: controller.signal
|
|
440
|
+
});
|
|
441
|
+
const data = await response.json();
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
return {
|
|
444
|
+
success: false,
|
|
445
|
+
status: response.status,
|
|
446
|
+
error: `HTTP ${response.status} ${response.statusText}`
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
success: true,
|
|
451
|
+
status: response.status,
|
|
452
|
+
data
|
|
453
|
+
};
|
|
454
|
+
} finally {
|
|
455
|
+
clearTimeout(timeout);
|
|
456
|
+
}
|
|
457
|
+
} catch (err) {
|
|
458
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
459
|
+
return { success: false, error: msg };
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// ../connectors/src/connectors/hackernews/tools/search-algolia.ts
|
|
465
|
+
import { z as z2 } from "zod";
|
|
466
|
+
var BASE_URL2 = "https://hn.algolia.com/api/v1";
|
|
467
|
+
var REQUEST_TIMEOUT_MS2 = 6e4;
|
|
468
|
+
var inputSchema2 = z2.object({
|
|
469
|
+
toolUseIntent: z2.string().optional().describe(
|
|
470
|
+
"Brief description of what you intend to accomplish with this tool call"
|
|
471
|
+
),
|
|
472
|
+
connectionId: z2.string().describe(
|
|
473
|
+
"ID of the Hacker News connection to use. This connector has no credentials, but the connection ID identifies the selected data source."
|
|
474
|
+
),
|
|
475
|
+
endpoint: z2.enum(["search", "search_by_date", "items", "users"]).describe(
|
|
476
|
+
"Algolia API endpoint. Use search/search_by_date for keyword queries, items for /items/{id}, and users for /users/{username}. Prefer Firebase for non-search dashboard data."
|
|
477
|
+
),
|
|
478
|
+
idOrUsername: z2.string().optional().describe(
|
|
479
|
+
"Required when endpoint is items or users. Pass the item id for items or the username for users."
|
|
480
|
+
),
|
|
481
|
+
queryParams: z2.record(z2.string(), z2.string()).optional().describe(
|
|
482
|
+
"Query parameters for Algolia search. Common params: query, tags (e.g. story,comment,show_hn,ask_hn), numericFilters (e.g. created_at_i>1700000000), page, hitsPerPage. Keep hitsPerPage small for dashboards."
|
|
483
|
+
)
|
|
484
|
+
});
|
|
485
|
+
var outputSchema2 = z2.discriminatedUnion("success", [
|
|
486
|
+
z2.object({
|
|
487
|
+
success: z2.literal(true),
|
|
488
|
+
status: z2.number(),
|
|
489
|
+
data: z2.unknown()
|
|
490
|
+
}),
|
|
491
|
+
z2.object({
|
|
492
|
+
success: z2.literal(false),
|
|
493
|
+
status: z2.number().optional(),
|
|
494
|
+
error: z2.string()
|
|
495
|
+
})
|
|
496
|
+
]);
|
|
497
|
+
var searchAlgoliaTool = new ConnectorTool({
|
|
498
|
+
name: "searchAlgolia",
|
|
499
|
+
description: `Search Hacker News data through the Algolia-powered HN Search API.
|
|
500
|
+
Use this tool only when Firebase cannot answer the question, such as keyword search, historical search, tag search, or sorting search hits by date. For top/new/best/Ask/Show/job dashboards and individual item/user lookups, prefer requestFirebase because it uses the official Hacker News Firebase API.
|
|
501
|
+
|
|
502
|
+
Avoid frequent repeated Algolia searches from dashboards. Keep hitsPerPage small, page deliberately, and cache or reuse results in server logic when possible. The public search API may rate limit by IP when used heavily.`,
|
|
503
|
+
inputSchema: inputSchema2,
|
|
504
|
+
outputSchema: outputSchema2,
|
|
505
|
+
async execute({ connectionId, endpoint, idOrUsername, queryParams }, connections) {
|
|
506
|
+
const connection2 = connections.find((c) => c.id === connectionId);
|
|
507
|
+
if (!connection2) {
|
|
508
|
+
return {
|
|
509
|
+
success: false,
|
|
510
|
+
error: `Connection ${connectionId} not found`
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
console.log(
|
|
514
|
+
`[connector-request] hackernews/${connection2.name}: Algolia ${endpoint}`
|
|
515
|
+
);
|
|
516
|
+
try {
|
|
517
|
+
let path2 = `/${endpoint}`;
|
|
518
|
+
if (endpoint === "items" || endpoint === "users") {
|
|
519
|
+
if (!idOrUsername) {
|
|
520
|
+
return {
|
|
521
|
+
success: false,
|
|
522
|
+
error: `idOrUsername is required for endpoint ${endpoint}`
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
path2 += `/${encodeURIComponent(idOrUsername)}`;
|
|
526
|
+
}
|
|
527
|
+
const url = new URL(`${BASE_URL2}${path2}`);
|
|
528
|
+
if (queryParams) {
|
|
529
|
+
for (const [k, v] of Object.entries(queryParams)) {
|
|
530
|
+
url.searchParams.set(k, v);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const controller = new AbortController();
|
|
534
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
535
|
+
try {
|
|
536
|
+
const response = await fetch(url.toString(), {
|
|
537
|
+
method: "GET",
|
|
538
|
+
headers: { Accept: "application/json" },
|
|
539
|
+
signal: controller.signal
|
|
540
|
+
});
|
|
541
|
+
const data = await response.json();
|
|
542
|
+
if (!response.ok) {
|
|
543
|
+
return {
|
|
544
|
+
success: false,
|
|
545
|
+
status: response.status,
|
|
546
|
+
error: `HTTP ${response.status} ${response.statusText}`
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
success: true,
|
|
551
|
+
status: response.status,
|
|
552
|
+
data
|
|
553
|
+
};
|
|
554
|
+
} finally {
|
|
555
|
+
clearTimeout(timeout);
|
|
556
|
+
}
|
|
557
|
+
} catch (err) {
|
|
558
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
559
|
+
return { success: false, error: msg };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// ../connectors/src/connectors/hackernews/parameters.ts
|
|
565
|
+
var parameters = {};
|
|
566
|
+
|
|
567
|
+
// ../connectors/src/connectors/hackernews/index.ts
|
|
568
|
+
var tools = {
|
|
569
|
+
requestFirebase: requestFirebaseTool,
|
|
570
|
+
searchAlgolia: searchAlgoliaTool
|
|
571
|
+
};
|
|
572
|
+
var hackerNewsConnector = new ConnectorPlugin({
|
|
573
|
+
slug: "hackernews",
|
|
574
|
+
authType: AUTH_TYPES.API_KEY,
|
|
575
|
+
name: "Hacker News",
|
|
576
|
+
description: "Connect to public Hacker News data using the official Firebase API, with optional Algolia-powered search when needed.",
|
|
577
|
+
iconUrl: "https://news.ycombinator.com/y18.svg",
|
|
578
|
+
parameters,
|
|
579
|
+
releaseFlag: { dev1: true, dev2: true, prod: false },
|
|
580
|
+
categories: ["scraping"],
|
|
581
|
+
onboarding: hackerNewsOnboarding,
|
|
582
|
+
systemPrompt: {
|
|
583
|
+
en: `### Tools
|
|
584
|
+
|
|
585
|
+
- \`connector_hackernews-api-key_requestFirebase\`: Fetch public Hacker News data from the official Firebase API. Use this for story rankings, items, users, max item ID, and updates. Prefer this tool whenever keyword search is not required.
|
|
586
|
+
- \`connector_hackernews-api-key_searchAlgolia\`: Search Hacker News data through the Algolia-powered search API. Use this only for keyword search, tag search, date-sorted search, or historical queries that Firebase cannot answer.
|
|
587
|
+
|
|
588
|
+
### Business Logic
|
|
589
|
+
|
|
590
|
+
The business logic type for this connector is "typescript". Use the connector SDK in your handler. This connector has no credential parameters, but still uses the API Key connector model so users can start immediately without entering a key.
|
|
591
|
+
|
|
592
|
+
SDK methods (client created via \`connection(connectionId)\`):
|
|
593
|
+
- \`client.requestFirebase(path)\` \u2014 low-level fetch against the official Firebase API
|
|
594
|
+
- \`client.requestAlgolia(path, options?)\` \u2014 low-level fetch against HN Search powered by Algolia
|
|
595
|
+
- \`client.getItem(id)\` \u2014 get a story, comment, job, poll, or poll option
|
|
596
|
+
- \`client.getUser(username)\` \u2014 get a public HN user profile
|
|
597
|
+
- \`client.getMaxItem()\` \u2014 get the current largest item ID
|
|
598
|
+
- \`client.getUpdates()\` \u2014 get changed item IDs and profile IDs
|
|
599
|
+
- \`client.getStoryIds(kind, options?)\` \u2014 get top/new/best/ask/show/job story IDs
|
|
600
|
+
- \`client.getItems(ids, options?)\` \u2014 fetch item details with bounded concurrency
|
|
601
|
+
- \`client.search(query, options?)\` \u2014 Algolia keyword search; use only when search is required
|
|
602
|
+
|
|
603
|
+
For dashboards showing current top/new/best/Ask/Show/job stories, use Firebase list endpoints and then fetch only the item details needed for the visible view. Avoid unbounded item-tree traversal and avoid repeatedly hitting Algolia from render-time server logic. If the user asks for "search HN for ..." or historical keyword analysis, use Algolia with a small \`hitsPerPage\`.
|
|
604
|
+
|
|
605
|
+
\`\`\`ts
|
|
606
|
+
import type { Context } from "hono";
|
|
607
|
+
import { connection } from "@squadbase/vite-server/connectors/hackernews";
|
|
608
|
+
|
|
609
|
+
const hn = connection("<connectionId>");
|
|
610
|
+
|
|
611
|
+
export default async function handler(c: Context) {
|
|
612
|
+
const { limit = 20 } = await c.req.json<{ limit?: number }>();
|
|
613
|
+
|
|
614
|
+
const ids = await hn.getStoryIds("top", { limit });
|
|
615
|
+
const stories = await hn.getItems(ids, {
|
|
616
|
+
limit,
|
|
617
|
+
concurrency: 5,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
return c.json({ stories });
|
|
621
|
+
}
|
|
622
|
+
\`\`\`
|
|
623
|
+
|
|
624
|
+
### Hacker News API Reference
|
|
625
|
+
|
|
626
|
+
#### Official Firebase API
|
|
627
|
+
- Base URL: \`https://hacker-news.firebaseio.com/v0\`
|
|
628
|
+
- No authentication required
|
|
629
|
+
- Items: \`/item/{id}.json\`
|
|
630
|
+
- Users: \`/user/{username}.json\`
|
|
631
|
+
- Story lists: \`/topstories.json\`, \`/newstories.json\`, \`/beststories.json\`, \`/askstories.json\`, \`/showstories.json\`, \`/jobstories.json\`
|
|
632
|
+
- Live data: \`/maxitem.json\`, \`/updates.json\`
|
|
633
|
+
|
|
634
|
+
#### HN Search powered by Algolia
|
|
635
|
+
- Base URL: \`https://hn.algolia.com/api/v1\`
|
|
636
|
+
- Use \`/search\` and \`/search_by_date\` for keyword queries
|
|
637
|
+
- Common query parameters: \`query\`, \`tags\`, \`numericFilters\`, \`page\`, \`hitsPerPage\`
|
|
638
|
+
- Prefer Firebase unless search or date filtering is genuinely needed`,
|
|
639
|
+
ja: `### \u30C4\u30FC\u30EB
|
|
640
|
+
|
|
641
|
+
- \`connector_hackernews-api-key_requestFirebase\`: \u516C\u5F0F Firebase API \u304B\u3089\u516C\u958B Hacker News \u30C7\u30FC\u30BF\u3092\u53D6\u5F97\u3057\u307E\u3059\u3002\u30E9\u30F3\u30AD\u30F3\u30B0\u3001item\u3001user\u3001max item ID\u3001updates \u306E\u53D6\u5F97\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002\u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u304C\u4E0D\u8981\u306A\u5834\u5408\u306F\u3053\u306E\u30C4\u30FC\u30EB\u3092\u512A\u5148\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
642
|
+
- \`connector_hackernews-api-key_searchAlgolia\`: Algolia \u30D9\u30FC\u30B9\u306E\u691C\u7D22 API \u3067 Hacker News \u30C7\u30FC\u30BF\u3092\u691C\u7D22\u3057\u307E\u3059\u3002Firebase \u3067\u306F\u7B54\u3048\u3089\u308C\u306A\u3044\u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u3001\u30BF\u30B0\u691C\u7D22\u3001\u65E5\u4ED8\u9806\u691C\u7D22\u3001\u5C65\u6B74\u30AF\u30A8\u30EA\u304C\u5FC5\u8981\u306A\u5834\u5408\u306E\u307F\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
643
|
+
|
|
644
|
+
### Business Logic
|
|
645
|
+
|
|
646
|
+
\u3053\u306E\u30B3\u30CD\u30AF\u30BF\u306E\u30D3\u30B8\u30CD\u30B9\u30ED\u30B8\u30C3\u30AF\u30BF\u30A4\u30D7\u306F "typescript" \u3067\u3059\u3002\u30CF\u30F3\u30C9\u30E9\u5185\u3067\u306F\u30B3\u30CD\u30AF\u30BFSDK\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u3053\u306E\u30B3\u30CD\u30AF\u30BF\u306B\u306F\u8A8D\u8A3C\u30D1\u30E9\u30E1\u30FC\u30BF\u306F\u3042\u308A\u307E\u305B\u3093\u304C\u3001\u65E2\u5B58\u30E2\u30C7\u30EB\u306B\u5408\u308F\u305B\u3066 API Key \u30B3\u30CD\u30AF\u30BF\u3068\u3057\u3066\u6271\u3044\u3001\u30E6\u30FC\u30B6\u30FC\u306F\u30AD\u30FC\u5165\u529B\u306A\u3057\u3067\u3059\u3050\u958B\u59CB\u3067\u304D\u307E\u3059\u3002
|
|
647
|
+
|
|
648
|
+
SDK\u30E1\u30BD\u30C3\u30C9 (\`connection(connectionId)\` \u3067\u4F5C\u6210\u3057\u305F\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8):
|
|
649
|
+
- \`client.requestFirebase(path)\` \u2014 \u516C\u5F0F Firebase API \u3078\u306E\u4F4E\u30EC\u30D9\u30EBfetch
|
|
650
|
+
- \`client.requestAlgolia(path, options?)\` \u2014 HN Search powered by Algolia \u3078\u306E\u4F4E\u30EC\u30D9\u30EBfetch
|
|
651
|
+
- \`client.getItem(id)\` \u2014 story, comment, job, poll, poll option \u3092\u53D6\u5F97
|
|
652
|
+
- \`client.getUser(username)\` \u2014 \u516C\u958B HN \u30E6\u30FC\u30B6\u30FC\u30D7\u30ED\u30D5\u30A3\u30FC\u30EB\u3092\u53D6\u5F97
|
|
653
|
+
- \`client.getMaxItem()\` \u2014 \u73FE\u5728\u306E\u6700\u5927 item ID \u3092\u53D6\u5F97
|
|
654
|
+
- \`client.getUpdates()\` \u2014 \u66F4\u65B0\u3055\u308C\u305F item ID \u3068 profile ID \u3092\u53D6\u5F97
|
|
655
|
+
- \`client.getStoryIds(kind, options?)\` \u2014 top/new/best/ask/show/job \u306E story ID \u3092\u53D6\u5F97
|
|
656
|
+
- \`client.getItems(ids, options?)\` \u2014 \u4E26\u5217\u6570\u3092\u5236\u9650\u3057\u3066 item \u8A73\u7D30\u3092\u53D6\u5F97
|
|
657
|
+
- \`client.search(query, options?)\` \u2014 Algolia \u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u3002\u691C\u7D22\u304C\u5FC5\u8981\u306A\u5834\u5408\u306E\u307F\u4F7F\u7528
|
|
658
|
+
|
|
659
|
+
\u73FE\u5728\u306E top/new/best/Ask/Show/job stories \u3092\u8868\u793A\u3059\u308B\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u3067\u306F\u3001Firebase \u306E\u30EA\u30B9\u30C8\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u3092\u4F7F\u3044\u3001\u8868\u793A\u306B\u5FC5\u8981\u306A item \u8A73\u7D30\u3060\u3051\u3092\u53D6\u5F97\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u7121\u5236\u9650\u306E\u30B3\u30E1\u30F3\u30C8\u30C4\u30EA\u30FC\u8D70\u67FB\u3084\u3001\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u6642\u30B5\u30FC\u30D0\u30FC\u30ED\u30B8\u30C3\u30AF\u304B\u3089\u306E Algolia \u9023\u6253\u306F\u907F\u3051\u3066\u304F\u3060\u3055\u3044\u3002\u30E6\u30FC\u30B6\u30FC\u304C\u300CHN\u3067\u691C\u7D22\u3057\u3066\u300D\u3084\u5C65\u6B74\u30AD\u30FC\u30EF\u30FC\u30C9\u5206\u6790\u3092\u6C42\u3081\u305F\u5834\u5408\u306F\u3001\u5C0F\u3055\u306A \`hitsPerPage\` \u3067 Algolia \u3092\u4F7F\u3044\u307E\u3059\u3002
|
|
660
|
+
|
|
661
|
+
\`\`\`ts
|
|
662
|
+
import type { Context } from "hono";
|
|
663
|
+
import { connection } from "@squadbase/vite-server/connectors/hackernews";
|
|
664
|
+
|
|
665
|
+
const hn = connection("<connectionId>");
|
|
666
|
+
|
|
667
|
+
export default async function handler(c: Context) {
|
|
668
|
+
const { limit = 20 } = await c.req.json<{ limit?: number }>();
|
|
669
|
+
|
|
670
|
+
const ids = await hn.getStoryIds("top", { limit });
|
|
671
|
+
const stories = await hn.getItems(ids, {
|
|
672
|
+
limit,
|
|
673
|
+
concurrency: 5,
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
return c.json({ stories });
|
|
677
|
+
}
|
|
678
|
+
\`\`\`
|
|
679
|
+
|
|
680
|
+
### Hacker News API \u30EA\u30D5\u30A1\u30EC\u30F3\u30B9
|
|
681
|
+
|
|
682
|
+
#### \u516C\u5F0F Firebase API
|
|
683
|
+
- \u30D9\u30FC\u30B9URL: \`https://hacker-news.firebaseio.com/v0\`
|
|
684
|
+
- \u8A8D\u8A3C\u4E0D\u8981
|
|
685
|
+
- Items: \`/item/{id}.json\`
|
|
686
|
+
- Users: \`/user/{username}.json\`
|
|
687
|
+
- Story lists: \`/topstories.json\`, \`/newstories.json\`, \`/beststories.json\`, \`/askstories.json\`, \`/showstories.json\`, \`/jobstories.json\`
|
|
688
|
+
- Live data: \`/maxitem.json\`, \`/updates.json\`
|
|
689
|
+
|
|
690
|
+
#### HN Search powered by Algolia
|
|
691
|
+
- \u30D9\u30FC\u30B9URL: \`https://hn.algolia.com/api/v1\`
|
|
692
|
+
- \u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u306B\u306F \`/search\` \u3068 \`/search_by_date\` \u3092\u4F7F\u7528
|
|
693
|
+
- \u4E3B\u8981\u30AF\u30A8\u30EA\u30D1\u30E9\u30E1\u30FC\u30BF: \`query\`, \`tags\`, \`numericFilters\`, \`page\`, \`hitsPerPage\`
|
|
694
|
+
- \u691C\u7D22\u3084\u65E5\u4ED8\u30D5\u30A3\u30EB\u30BF\u304C\u672C\u5F53\u306B\u5FC5\u8981\u306A\u5834\u5408\u3092\u9664\u304D Firebase \u3092\u512A\u5148\u3057\u3066\u304F\u3060\u3055\u3044`
|
|
695
|
+
},
|
|
696
|
+
tools,
|
|
697
|
+
setup: (params, ctx, config) => runSetupFlow(hackerNewsSetupFlow, params, ctx, config),
|
|
698
|
+
async checkConnection() {
|
|
699
|
+
try {
|
|
700
|
+
const res = await fetch(
|
|
701
|
+
"https://hacker-news.firebaseio.com/v0/maxitem.json",
|
|
702
|
+
{
|
|
703
|
+
method: "GET",
|
|
704
|
+
headers: { Accept: "application/json" }
|
|
705
|
+
}
|
|
706
|
+
);
|
|
707
|
+
if (!res.ok) {
|
|
708
|
+
const errorText = await res.text().catch(() => res.statusText);
|
|
709
|
+
return {
|
|
710
|
+
success: false,
|
|
711
|
+
error: `Hacker News API failed: HTTP ${res.status} ${errorText}`
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
return { success: true };
|
|
715
|
+
} catch (error) {
|
|
716
|
+
return {
|
|
717
|
+
success: false,
|
|
718
|
+
error: error instanceof Error ? error.message : String(error)
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// src/connectors/create-connector-sdk.ts
|
|
725
|
+
import { readFileSync } from "fs";
|
|
726
|
+
import path from "path";
|
|
727
|
+
|
|
728
|
+
// src/connector-client/env.ts
|
|
729
|
+
function resolveEnvVar(entry, key, connectionId) {
|
|
730
|
+
const envVarName = entry.envVars[key];
|
|
731
|
+
if (!envVarName) {
|
|
732
|
+
throw new Error(`Connection "${connectionId}" is missing envVars mapping for key "${key}"`);
|
|
733
|
+
}
|
|
734
|
+
const value = process.env[envVarName];
|
|
735
|
+
if (!value) {
|
|
736
|
+
throw new Error(`Environment variable "${envVarName}" (for connection "${connectionId}", key "${key}") is not set`);
|
|
737
|
+
}
|
|
738
|
+
return value;
|
|
739
|
+
}
|
|
740
|
+
function resolveEnvVarOptional(entry, key) {
|
|
741
|
+
const envVarName = entry.envVars[key];
|
|
742
|
+
if (!envVarName) return void 0;
|
|
743
|
+
return process.env[envVarName] || void 0;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/connector-client/proxy-fetch.ts
|
|
747
|
+
import { getContext } from "hono/context-storage";
|
|
748
|
+
import { getCookie } from "hono/cookie";
|
|
749
|
+
var APP_SESSION_COOKIE_NAME = "__Host-squadbase-session";
|
|
750
|
+
var TABLEAU_SESSION_SENTINEL_URL = "squadbase://tableau-session/";
|
|
751
|
+
function normalizeHeaders(input) {
|
|
752
|
+
const out = {};
|
|
753
|
+
if (!input) return out;
|
|
754
|
+
new Headers(input).forEach((value, key) => {
|
|
755
|
+
out[key] = value;
|
|
756
|
+
});
|
|
757
|
+
return out;
|
|
758
|
+
}
|
|
759
|
+
function extractInputUrl(input) {
|
|
760
|
+
if (typeof input === "string") return input;
|
|
761
|
+
if (input instanceof URL) return input.href;
|
|
762
|
+
return input.url;
|
|
763
|
+
}
|
|
764
|
+
function createSandboxProxyFetch(connectionId) {
|
|
765
|
+
return async (input, init) => {
|
|
766
|
+
const token = process.env.INTERNAL_SQUADBASE_OAUTH_MACHINE_CREDENTIAL;
|
|
767
|
+
const sandboxId = process.env.INTERNAL_SQUADBASE_SANDBOX_ID;
|
|
768
|
+
if (!token || !sandboxId) {
|
|
769
|
+
throw new Error(
|
|
770
|
+
"Connection proxy is not configured. Please check your deployment settings."
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
const originalUrl = extractInputUrl(input);
|
|
774
|
+
const baseDomain = process.env["SQUADBASE_PREVIEW_BASE_DOMAIN"] ?? "preview.app.squadbase.dev";
|
|
775
|
+
if (originalUrl === TABLEAU_SESSION_SENTINEL_URL) {
|
|
776
|
+
const sessionUrl = `https://${sandboxId}.${baseDomain}/_sqcore/connections/${connectionId}/tableau-session`;
|
|
777
|
+
return fetch(sessionUrl, {
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
const originalMethod = init?.method ?? "GET";
|
|
783
|
+
const originalBody = init?.body ? JSON.parse(init.body) : void 0;
|
|
784
|
+
const proxyUrl = `https://${sandboxId}.${baseDomain}/_sqcore/connections/${connectionId}/request`;
|
|
785
|
+
return fetch(proxyUrl, {
|
|
786
|
+
method: "POST",
|
|
787
|
+
headers: {
|
|
788
|
+
"Content-Type": "application/json",
|
|
789
|
+
Authorization: `Bearer ${token}`
|
|
790
|
+
},
|
|
791
|
+
body: JSON.stringify({
|
|
792
|
+
url: originalUrl,
|
|
793
|
+
method: originalMethod,
|
|
794
|
+
headers: normalizeHeaders(init?.headers),
|
|
795
|
+
body: originalBody
|
|
796
|
+
})
|
|
797
|
+
});
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function createDeployedAppProxyFetch(connectionId) {
|
|
801
|
+
const projectId = process.env["SQUADBASE_PROJECT_ID"];
|
|
802
|
+
if (!projectId) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
"Connection proxy is not configured. Please check your deployment settings."
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const baseDomain = process.env["SQUADBASE_APP_BASE_DOMAIN"] ?? "squadbase.app";
|
|
808
|
+
const proxyUrl = `https://${projectId}.${baseDomain}/_sqcore/connections/${connectionId}/request`;
|
|
809
|
+
const sessionUrl = `https://${projectId}.${baseDomain}/_sqcore/connections/${connectionId}/tableau-session`;
|
|
810
|
+
return async (input, init) => {
|
|
811
|
+
const originalUrl = extractInputUrl(input);
|
|
812
|
+
const c = getContext();
|
|
813
|
+
const appSession = getCookie(c, APP_SESSION_COOKIE_NAME);
|
|
814
|
+
if (!appSession) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
"No authentication method available for connection proxy."
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
if (originalUrl === TABLEAU_SESSION_SENTINEL_URL) {
|
|
820
|
+
return fetch(sessionUrl, {
|
|
821
|
+
method: "POST",
|
|
822
|
+
headers: { Authorization: `Bearer ${appSession}` }
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
const originalMethod = init?.method ?? "GET";
|
|
826
|
+
const originalBody = init?.body ? JSON.parse(init.body) : void 0;
|
|
827
|
+
return fetch(proxyUrl, {
|
|
828
|
+
method: "POST",
|
|
829
|
+
headers: {
|
|
830
|
+
"Content-Type": "application/json",
|
|
831
|
+
Authorization: `Bearer ${appSession}`
|
|
832
|
+
},
|
|
833
|
+
body: JSON.stringify({
|
|
834
|
+
url: originalUrl,
|
|
835
|
+
method: originalMethod,
|
|
836
|
+
headers: normalizeHeaders(init?.headers),
|
|
837
|
+
body: originalBody
|
|
838
|
+
})
|
|
839
|
+
});
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function createProxyFetch(connectionId) {
|
|
843
|
+
if (process.env.INTERNAL_SQUADBASE_SANDBOX_ID) {
|
|
844
|
+
return createSandboxProxyFetch(connectionId);
|
|
845
|
+
}
|
|
846
|
+
return createDeployedAppProxyFetch(connectionId);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/connectors/create-connector-sdk.ts
|
|
850
|
+
function loadConnectionsSync() {
|
|
851
|
+
const filePath = process.env.CONNECTIONS_PATH ?? path.join(process.cwd(), ".squadbase/connections.json");
|
|
852
|
+
try {
|
|
853
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
854
|
+
return JSON.parse(raw);
|
|
855
|
+
} catch {
|
|
856
|
+
return {};
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function createConnectorSdk(plugin, createClient2) {
|
|
860
|
+
return (connectionId) => {
|
|
861
|
+
const connections = loadConnectionsSync();
|
|
862
|
+
const entry = connections[connectionId];
|
|
863
|
+
if (!entry) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`Connection "${connectionId}" not found in .squadbase/connections.json`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
if (entry.connector.slug !== plugin.slug) {
|
|
869
|
+
throw new Error(
|
|
870
|
+
`Connection "${connectionId}" is not a ${plugin.slug} connection (got "${entry.connector.slug}")`
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
const params = {};
|
|
874
|
+
for (const param of Object.values(plugin.parameters)) {
|
|
875
|
+
if (param.required) {
|
|
876
|
+
params[param.slug] = resolveEnvVar(entry, param.slug, connectionId);
|
|
877
|
+
} else {
|
|
878
|
+
const val = resolveEnvVarOptional(entry, param.slug);
|
|
879
|
+
if (val !== void 0) params[param.slug] = val;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return createClient2(params, createProxyFetch(connectionId));
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/connectors/entries/hackernews.ts
|
|
887
|
+
var connection = createConnectorSdk(hackerNewsConnector, createClient);
|
|
888
|
+
export {
|
|
889
|
+
connection
|
|
890
|
+
};
|