@adityanair98/api-oracle 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +74 -0
- package/dist/dashboard/public/app.js +1004 -0
- package/dist/dashboard/public/index.html +142 -0
- package/dist/dashboard/public/public/app.js +1004 -0
- package/dist/dashboard/public/public/index.html +142 -0
- package/dist/dashboard/public/public/styles.css +1464 -0
- package/dist/dashboard/public/styles.css +1464 -0
- package/dist/dashboard/routes/api.d.ts +7 -0
- package/dist/dashboard/routes/api.js +245 -0
- package/dist/dashboard/server.d.ts +9 -0
- package/dist/dashboard/server.js +45 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +23 -0
- package/dist/knowledge/db.d.ts +22 -0
- package/dist/knowledge/db.js +182 -0
- package/dist/knowledge/schema.d.ts +275 -0
- package/dist/knowledge/schema.js +135 -0
- package/dist/knowledge/scorer.d.ts +63 -0
- package/dist/knowledge/scorer.js +314 -0
- package/dist/knowledge/search.d.ts +37 -0
- package/dist/knowledge/search.js +111 -0
- package/dist/knowledge/synonyms.d.ts +36 -0
- package/dist/knowledge/synonyms.js +523 -0
- package/dist/knowledge/tfidf.d.ts +42 -0
- package/dist/knowledge/tfidf.js +138 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +40 -0
- package/dist/tools/check-freshness.d.ts +9 -0
- package/dist/tools/check-freshness.js +95 -0
- package/dist/tools/compare-apis.d.ts +8 -0
- package/dist/tools/compare-apis.js +149 -0
- package/dist/tools/find-api.d.ts +9 -0
- package/dist/tools/find-api.js +120 -0
- package/dist/tools/get-setup-guide.d.ts +8 -0
- package/dist/tools/get-setup-guide.js +127 -0
- package/dist/updater/linter.d.ts +31 -0
- package/dist/updater/linter.js +219 -0
- package/dist/updater/report.d.ts +29 -0
- package/dist/updater/report.js +96 -0
- package/dist/updater/staleness.d.ts +39 -0
- package/dist/updater/staleness.js +66 -0
- package/dist/updater/version-tracker.d.ts +28 -0
- package/dist/updater/version-tracker.js +50 -0
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.js +13 -0
- package/dist/utils/logger.d.ts +20 -0
- package/dist/utils/logger.js +32 -0
- package/package.json +56 -0
- package/src/entries/ai/anthropic.json +95 -0
- package/src/entries/ai/eleven-labs.json +90 -0
- package/src/entries/ai/openai.json +95 -0
- package/src/entries/ai/replicate.json +87 -0
- package/src/entries/ai/resemble-ai.json +88 -0
- package/src/entries/ai/stability-ai.json +89 -0
- package/src/entries/analytics/posthog.json +88 -0
- package/src/entries/analytics/sentry.json +84 -0
- package/src/entries/auth/auth0.json +90 -0
- package/src/entries/auth/clerk.json +95 -0
- package/src/entries/cms/contentful.json +92 -0
- package/src/entries/cms/sanity.json +92 -0
- package/src/entries/cms/strapi.json +93 -0
- package/src/entries/commerce/medusa.json +91 -0
- package/src/entries/commerce/shopify-api.json +91 -0
- package/src/entries/communication/sendbird.json +85 -0
- package/src/entries/communication/stream-chat.json +94 -0
- package/src/entries/database/firebase.json +88 -0
- package/src/entries/database/neon.json +94 -0
- package/src/entries/database/planetscale.json +95 -0
- package/src/entries/database/supabase.json +94 -0
- package/src/entries/database/upstash.json +94 -0
- package/src/entries/devops/fly-io.json +90 -0
- package/src/entries/devops/netlify.json +90 -0
- package/src/entries/devops/railway.json +90 -0
- package/src/entries/devops/vercel.json +90 -0
- package/src/entries/email/mailgun.json +91 -0
- package/src/entries/email/postmark.json +91 -0
- package/src/entries/email/resend.json +89 -0
- package/src/entries/email/sendgrid.json +90 -0
- package/src/entries/forms/formspark.json +85 -0
- package/src/entries/forms/typeform.json +98 -0
- package/src/entries/infrastructure/aws-s3.json +104 -0
- package/src/entries/infrastructure/cloudflare-r2.json +92 -0
- package/src/entries/infrastructure/cloudflare-workers.json +92 -0
- package/src/entries/infrastructure/digital-ocean-spaces.json +87 -0
- package/src/entries/integration/nango.json +90 -0
- package/src/entries/integration/zapier.json +92 -0
- package/src/entries/maps/google-maps.json +89 -0
- package/src/entries/maps/mapbox.json +87 -0
- package/src/entries/media/deepgram.json +84 -0
- package/src/entries/media/imgix.json +84 -0
- package/src/entries/media/mux.json +94 -0
- package/src/entries/messaging/ably.json +94 -0
- package/src/entries/messaging/pusher.json +94 -0
- package/src/entries/messaging/twilio.json +94 -0
- package/src/entries/messaging/vonage.json +89 -0
- package/src/entries/notifications/knock.json +84 -0
- package/src/entries/notifications/novu.json +84 -0
- package/src/entries/notifications/onesignal.json +84 -0
- package/src/entries/payments/lemonsqueezy.json +91 -0
- package/src/entries/payments/paddle.json +90 -0
- package/src/entries/payments/paypal.json +91 -0
- package/src/entries/payments/razorpay.json +85 -0
- package/src/entries/payments/square.json +91 -0
- package/src/entries/payments/stripe.json +96 -0
- package/src/entries/scheduling/cal-com.json +90 -0
- package/src/entries/scheduling/calendly.json +90 -0
- package/src/entries/search/algolia.json +96 -0
- package/src/entries/security/arcjet.json +89 -0
- package/src/entries/security/snyk.json +90 -0
- package/src/entries/storage/cloudinary.json +93 -0
- package/src/entries/storage/uploadthing.json +90 -0
- package/src/entries/testing/browserstack.json +86 -0
- package/src/entries/testing/checkly.json +89 -0
- package/src/entries/workflow/inngest.json +88 -0
- package/src/entries/workflow/temporal.json +90 -0
- package/src/entries/workflow/trigger-dev.json +89 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard REST API routes.
|
|
3
|
+
* All routes return { success: true, data: ... } or { success: false, error: "..." }
|
|
4
|
+
*
|
|
5
|
+
* Exports: apiRouter
|
|
6
|
+
*/
|
|
7
|
+
import { Router } from "express";
|
|
8
|
+
import { getAllEntries, getBySlug as getEntryBySlug } from "../../knowledge/db.js";
|
|
9
|
+
import { findApis } from "../../knowledge/search.js";
|
|
10
|
+
import { checkAllEntries, checkStaleness } from "../../updater/staleness.js";
|
|
11
|
+
import { buildFreshnessReport, formatReport } from "../../updater/report.js";
|
|
12
|
+
import { lintEntry } from "../../updater/linter.js";
|
|
13
|
+
import { createLogger } from "../../utils/logger.js";
|
|
14
|
+
const logger = createLogger("dashboard:api");
|
|
15
|
+
export const apiRouter = Router();
|
|
16
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
17
|
+
function ok(res, data) {
|
|
18
|
+
res.json({ success: true, data });
|
|
19
|
+
}
|
|
20
|
+
function err(res, status, message) {
|
|
21
|
+
res.status(status).json({ success: false, error: message });
|
|
22
|
+
}
|
|
23
|
+
const LEVEL_ORDER = {
|
|
24
|
+
fresh: 0, aging: 1, stale: 2, critical: 3,
|
|
25
|
+
};
|
|
26
|
+
function sortEntries(entries, sort, order) {
|
|
27
|
+
const stalenessMap = new Map(checkAllEntries(entries).map((i) => [i.slug, i.level]));
|
|
28
|
+
return [...entries].sort((a, b) => {
|
|
29
|
+
let cmp = 0;
|
|
30
|
+
switch (sort) {
|
|
31
|
+
case "name":
|
|
32
|
+
cmp = a.name.localeCompare(b.name);
|
|
33
|
+
break;
|
|
34
|
+
case "qualityScore":
|
|
35
|
+
cmp = a.qualityScore - b.qualityScore;
|
|
36
|
+
break;
|
|
37
|
+
case "lastVerified":
|
|
38
|
+
cmp = a.lastVerified.localeCompare(b.lastVerified);
|
|
39
|
+
break;
|
|
40
|
+
case "staleness": {
|
|
41
|
+
const la = LEVEL_ORDER[stalenessMap.get(a.slug) ?? "fresh"];
|
|
42
|
+
const lb = LEVEL_ORDER[stalenessMap.get(b.slug) ?? "fresh"];
|
|
43
|
+
cmp = la - lb;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return order === "desc" ? -cmp : cmp;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// ─── GET /api/entries ─────────────────────────────────────────────────────────
|
|
51
|
+
apiRouter.get("/entries", (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
let entries = getAllEntries();
|
|
54
|
+
const { category, search, sort = "name", order = "asc" } = req.query;
|
|
55
|
+
if (category && category !== "all") {
|
|
56
|
+
entries = entries.filter((e) => e.category === category);
|
|
57
|
+
}
|
|
58
|
+
if (search && search.trim()) {
|
|
59
|
+
const results = findApis(search.trim(), undefined, 68);
|
|
60
|
+
const slugSet = new Set(results.map((r) => r.entry.slug));
|
|
61
|
+
entries = entries.filter((e) => slugSet.has(e.slug));
|
|
62
|
+
// Preserve search order
|
|
63
|
+
const slugOrder = new Map(results.map((r, i) => [r.entry.slug, i]));
|
|
64
|
+
entries.sort((a, b) => (slugOrder.get(a.slug) ?? 99) - (slugOrder.get(b.slug) ?? 99));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
entries = sortEntries(entries, sort, order);
|
|
68
|
+
}
|
|
69
|
+
const stalenessInfos = checkAllEntries(entries);
|
|
70
|
+
const stalenessMap = new Map(stalenessInfos.map((i) => [i.slug, i]));
|
|
71
|
+
const data = entries.map((e) => {
|
|
72
|
+
const staleness = stalenessMap.get(e.slug);
|
|
73
|
+
return {
|
|
74
|
+
slug: e.slug,
|
|
75
|
+
name: e.name,
|
|
76
|
+
category: e.category,
|
|
77
|
+
subcategory: e.subcategory,
|
|
78
|
+
description: e.description.slice(0, 120) + (e.description.length > 120 ? "…" : ""),
|
|
79
|
+
qualityScore: e.qualityScore,
|
|
80
|
+
pricing: { model: e.pricing.model, freeTier: e.pricing.freeTier },
|
|
81
|
+
bestFor: e.bestFor,
|
|
82
|
+
lastVerified: e.lastVerified,
|
|
83
|
+
staleness: staleness
|
|
84
|
+
? { level: staleness.level, daysSinceVerified: staleness.daysSinceVerified }
|
|
85
|
+
: null,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
ok(res, data);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
logger.error("GET /api/entries error", { error: String(e) });
|
|
92
|
+
err(res, 500, "Internal server error");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// ─── GET /api/entries/:slug ───────────────────────────────────────────────────
|
|
96
|
+
apiRouter.get("/entries/:slug", (req, res) => {
|
|
97
|
+
try {
|
|
98
|
+
const { slug } = req.params;
|
|
99
|
+
const entry = getEntryBySlug(slug);
|
|
100
|
+
if (!entry) {
|
|
101
|
+
err(res, 404, `No entry found with slug: ${slug}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const staleness = checkStaleness(entry);
|
|
105
|
+
const lintResult = lintEntry(entry);
|
|
106
|
+
ok(res, { entry, staleness, lintResult });
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
logger.error("GET /api/entries/:slug error", { error: String(e) });
|
|
110
|
+
err(res, 500, "Internal server error");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// ─── GET /api/categories ──────────────────────────────────────────────────────
|
|
114
|
+
apiRouter.get("/categories", (_req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
const entries = getAllEntries();
|
|
117
|
+
const counts = new Map();
|
|
118
|
+
for (const e of entries) {
|
|
119
|
+
counts.set(e.category, (counts.get(e.category) ?? 0) + 1);
|
|
120
|
+
}
|
|
121
|
+
const data = [...counts.entries()]
|
|
122
|
+
.map(([category, count]) => ({ category, count }))
|
|
123
|
+
.sort((a, b) => a.category.localeCompare(b.category));
|
|
124
|
+
ok(res, data);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
logger.error("GET /api/categories error", { error: String(e) });
|
|
128
|
+
err(res, 500, "Internal server error");
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// ─── GET /api/stats ───────────────────────────────────────────────────────────
|
|
132
|
+
apiRouter.get("/stats", (_req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
const entries = getAllEntries();
|
|
135
|
+
const stalenessInfos = checkAllEntries(entries);
|
|
136
|
+
const staleness = { fresh: 0, aging: 0, stale: 0, critical: 0 };
|
|
137
|
+
for (const info of stalenessInfos)
|
|
138
|
+
staleness[info.level]++;
|
|
139
|
+
const lintResults = entries.map(lintEntry);
|
|
140
|
+
const lintPassing = lintResults.filter((r) => r.passed).length;
|
|
141
|
+
const avgQuality = entries.reduce((sum, e) => sum + e.qualityScore, 0) / entries.length;
|
|
142
|
+
const categories = new Set(entries.map((e) => e.category)).size;
|
|
143
|
+
ok(res, {
|
|
144
|
+
totalEntries: entries.length,
|
|
145
|
+
categories,
|
|
146
|
+
staleness,
|
|
147
|
+
linterResults: { passing: lintPassing, failing: entries.length - lintPassing },
|
|
148
|
+
averageQualityScore: Math.round(avgQuality * 10) / 10,
|
|
149
|
+
lastUpdated: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
logger.error("GET /api/stats error", { error: String(e) });
|
|
154
|
+
err(res, 500, "Internal server error");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// ─── GET /api/search ─────────────────────────────────────────────────────────
|
|
158
|
+
apiRouter.get("/search", (req, res) => {
|
|
159
|
+
try {
|
|
160
|
+
const { q } = req.query;
|
|
161
|
+
if (!q || !q.trim()) {
|
|
162
|
+
err(res, 400, "Query param 'q' is required");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const results = findApis(q.trim(), undefined, 5);
|
|
166
|
+
const data = {
|
|
167
|
+
query: q.trim(),
|
|
168
|
+
count: results.length,
|
|
169
|
+
results: results.map((r) => ({
|
|
170
|
+
slug: r.entry.slug,
|
|
171
|
+
name: r.entry.name,
|
|
172
|
+
category: r.entry.category,
|
|
173
|
+
score: Math.round(r.score * 1000) / 1000,
|
|
174
|
+
confidence: Math.round((r.confidence ?? 0) * 100),
|
|
175
|
+
scoreBreakdown: r.scoreBreakdown,
|
|
176
|
+
bestFor: r.entry.bestFor,
|
|
177
|
+
description: r.entry.description.slice(0, 120) + "…",
|
|
178
|
+
})),
|
|
179
|
+
};
|
|
180
|
+
ok(res, data);
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
logger.error("GET /api/search error", { error: String(e) });
|
|
184
|
+
err(res, 500, "Internal server error");
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// ─── GET /api/refresh-report ──────────────────────────────────────────────────
|
|
188
|
+
apiRouter.get("/refresh-report", (_req, res) => {
|
|
189
|
+
try {
|
|
190
|
+
const entries = getAllEntries();
|
|
191
|
+
const allInfos = checkAllEntries(entries);
|
|
192
|
+
const report = buildFreshnessReport(allInfos);
|
|
193
|
+
ok(res, { report, markdown: formatReport(report) });
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
logger.error("GET /api/refresh-report error", { error: String(e) });
|
|
197
|
+
err(res, 500, "Internal server error");
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
// ─── POST /api/entries/:slug/verify ──────────────────────────────────────────
|
|
201
|
+
apiRouter.post("/entries/:slug/verify", (req, res) => {
|
|
202
|
+
try {
|
|
203
|
+
const { slug } = req.params;
|
|
204
|
+
const entry = getEntryBySlug(slug);
|
|
205
|
+
if (!entry) {
|
|
206
|
+
err(res, 404, `No entry found with slug: ${slug}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// In a real setup this would write to the JSON file and re-seed.
|
|
210
|
+
// For the dashboard, we return the updated staleness as if verified now.
|
|
211
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
212
|
+
const updatedEntry = { ...entry, lastVerified: today };
|
|
213
|
+
const staleness = checkStaleness(updatedEntry);
|
|
214
|
+
logger.info("Mark as verified (read-only — update JSON file to persist)", { slug, today });
|
|
215
|
+
ok(res, {
|
|
216
|
+
slug,
|
|
217
|
+
lastVerified: today,
|
|
218
|
+
staleness,
|
|
219
|
+
note: `To persist: update lastVerified in src/entries/**/${slug}.json and run npm run seed`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
logger.error("POST /api/entries/:slug/verify error", { error: String(e) });
|
|
224
|
+
err(res, 500, "Internal server error");
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// ─── GET /api/lint ────────────────────────────────────────────────────────────
|
|
228
|
+
apiRouter.get("/lint", (_req, res) => {
|
|
229
|
+
try {
|
|
230
|
+
const entries = getAllEntries();
|
|
231
|
+
const results = entries.map(lintEntry);
|
|
232
|
+
const failing = results.filter((r) => !r.passed);
|
|
233
|
+
const passing = results.filter((r) => r.passed);
|
|
234
|
+
ok(res, {
|
|
235
|
+
total: results.length,
|
|
236
|
+
passing: passing.length,
|
|
237
|
+
failing: failing.length,
|
|
238
|
+
results: results.sort((a, b) => b.issues.length - a.issues.length),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
logger.error("GET /api/lint error", { error: String(e) });
|
|
243
|
+
err(res, 500, "Internal server error");
|
|
244
|
+
}
|
|
245
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Oracle Web Dashboard — Express server.
|
|
3
|
+
*
|
|
4
|
+
* Runs as a separate process from the MCP server.
|
|
5
|
+
* Start with: npm run dashboard
|
|
6
|
+
*
|
|
7
|
+
* Exports: startDashboard
|
|
8
|
+
*/
|
|
9
|
+
import express from "express";
|
|
10
|
+
import { join, dirname } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { initDb } from "../knowledge/db.js";
|
|
13
|
+
import { config } from "../utils/config.js";
|
|
14
|
+
import { createLogger } from "../utils/logger.js";
|
|
15
|
+
import { apiRouter } from "./routes/api.js";
|
|
16
|
+
const logger = createLogger("dashboard");
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3737;
|
|
19
|
+
export function startDashboard() {
|
|
20
|
+
const app = express();
|
|
21
|
+
// Parse JSON bodies
|
|
22
|
+
app.use(express.json());
|
|
23
|
+
// Serve static frontend files
|
|
24
|
+
const publicDir = join(__dirname, "public");
|
|
25
|
+
app.use(express.static(publicDir));
|
|
26
|
+
// Mount REST API
|
|
27
|
+
app.use("/api", apiRouter);
|
|
28
|
+
// Catch-all: serve index.html for client-side routing
|
|
29
|
+
app.get("*", (_req, res) => {
|
|
30
|
+
res.sendFile(join(publicDir, "index.html"));
|
|
31
|
+
});
|
|
32
|
+
app.listen(PORT, () => {
|
|
33
|
+
process.stdout.write(`\n API Oracle Dashboard\n`);
|
|
34
|
+
process.stdout.write(` ───────────────────────────────\n`);
|
|
35
|
+
process.stdout.write(` Local: http://localhost:${PORT}\n`);
|
|
36
|
+
process.stdout.write(` Entries: 68 APIs across 23 categories\n\n`);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// When run directly (npm run dashboard)
|
|
40
|
+
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
|
|
41
|
+
if (isMain) {
|
|
42
|
+
logger.info("Initializing database for dashboard", { dbPath: config.dbPath });
|
|
43
|
+
initDb(config.dbPath);
|
|
44
|
+
startDashboard();
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point — initializes the SQLite database and starts the MCP server.
|
|
3
|
+
* Run with: npm run dev (via tsx) or node dist/index.js (compiled).
|
|
4
|
+
*/
|
|
5
|
+
import { initDb } from "./knowledge/db.js";
|
|
6
|
+
import { startServer } from "./server.js";
|
|
7
|
+
import { config } from "./utils/config.js";
|
|
8
|
+
import { logger } from "./utils/logger.js";
|
|
9
|
+
async function main() {
|
|
10
|
+
logger.info("Starting API Oracle MCP server", {
|
|
11
|
+
dbPath: config.dbPath,
|
|
12
|
+
nodeEnv: config.nodeEnv,
|
|
13
|
+
});
|
|
14
|
+
// Initialize SQLite database (creates tables if they don't exist)
|
|
15
|
+
initDb(config.dbPath);
|
|
16
|
+
logger.info("Database initialized");
|
|
17
|
+
// Start the MCP server on stdio
|
|
18
|
+
await startServer();
|
|
19
|
+
}
|
|
20
|
+
main().catch((err) => {
|
|
21
|
+
process.stderr.write(`Fatal error: ${String(err)}\n`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite database initialization and query layer.
|
|
3
|
+
* Uses better-sqlite3 for synchronous access (safe in MCP server context).
|
|
4
|
+
* Complex nested objects are stored as JSON columns.
|
|
5
|
+
*
|
|
6
|
+
* Exports: initDb, insertEntry, getBySlug, getByCategory,
|
|
7
|
+
* searchByText, getAllEntries, deleteBySlug, closeDb
|
|
8
|
+
*/
|
|
9
|
+
import Database from "better-sqlite3";
|
|
10
|
+
import { type ApiEntry } from "./schema.js";
|
|
11
|
+
export declare function initDb(dbPath?: string): void;
|
|
12
|
+
export declare function getDb(): Database.Database;
|
|
13
|
+
export declare function closeDb(): void;
|
|
14
|
+
/** Allows tests to inject their own in-memory database instance */
|
|
15
|
+
export declare function _setDbForTesting(testDb: Database.Database): void;
|
|
16
|
+
export declare function insertEntry(entry: ApiEntry): void;
|
|
17
|
+
export declare function getBySlug(slug: string): ApiEntry | null;
|
|
18
|
+
export declare function getByCategory(category: string): ApiEntry[];
|
|
19
|
+
export declare function searchByText(query: string): ApiEntry[];
|
|
20
|
+
export declare function getAllEntries(): ApiEntry[];
|
|
21
|
+
export declare function deleteBySlug(slug: string): void;
|
|
22
|
+
export declare function slugExists(slug: string): boolean;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite database initialization and query layer.
|
|
3
|
+
* Uses better-sqlite3 for synchronous access (safe in MCP server context).
|
|
4
|
+
* Complex nested objects are stored as JSON columns.
|
|
5
|
+
*
|
|
6
|
+
* Exports: initDb, insertEntry, getBySlug, getByCategory,
|
|
7
|
+
* searchByText, getAllEntries, deleteBySlug, closeDb
|
|
8
|
+
*/
|
|
9
|
+
import Database from "better-sqlite3";
|
|
10
|
+
import { mkdirSync } from "fs";
|
|
11
|
+
import { dirname } from "path";
|
|
12
|
+
import { ApiEntrySchema, validateEntry } from "./schema.js";
|
|
13
|
+
import { createLogger } from "../utils/logger.js";
|
|
14
|
+
const logger = createLogger("db");
|
|
15
|
+
// Module-level db instance (singleton)
|
|
16
|
+
let db = null;
|
|
17
|
+
// ─── JSON columns — stored as serialized JSON strings ────────────────────────
|
|
18
|
+
const JSON_COLUMNS = [
|
|
19
|
+
"useCases",
|
|
20
|
+
"auth",
|
|
21
|
+
"pricing",
|
|
22
|
+
"rateLimits",
|
|
23
|
+
"sdk",
|
|
24
|
+
"codeExamples",
|
|
25
|
+
"gotchas",
|
|
26
|
+
"reliability",
|
|
27
|
+
"alternatives",
|
|
28
|
+
"complementary",
|
|
29
|
+
];
|
|
30
|
+
// ─── Schema DDL ───────────────────────────────────────────────────────────────
|
|
31
|
+
const CREATE_TABLE_SQL = `
|
|
32
|
+
CREATE TABLE IF NOT EXISTS apis (
|
|
33
|
+
slug TEXT PRIMARY KEY,
|
|
34
|
+
name TEXT NOT NULL,
|
|
35
|
+
category TEXT NOT NULL,
|
|
36
|
+
subcategory TEXT NOT NULL,
|
|
37
|
+
website TEXT NOT NULL,
|
|
38
|
+
description TEXT NOT NULL,
|
|
39
|
+
useCases TEXT NOT NULL,
|
|
40
|
+
auth TEXT NOT NULL,
|
|
41
|
+
pricing TEXT NOT NULL,
|
|
42
|
+
rateLimits TEXT NOT NULL,
|
|
43
|
+
sdk TEXT NOT NULL,
|
|
44
|
+
codeExamples TEXT NOT NULL,
|
|
45
|
+
gotchas TEXT NOT NULL,
|
|
46
|
+
reliability TEXT NOT NULL,
|
|
47
|
+
qualityScore REAL NOT NULL,
|
|
48
|
+
qualityJustification TEXT NOT NULL,
|
|
49
|
+
alternatives TEXT NOT NULL,
|
|
50
|
+
complementary TEXT NOT NULL,
|
|
51
|
+
bestFor TEXT NOT NULL,
|
|
52
|
+
lastVerified TEXT NOT NULL,
|
|
53
|
+
entryVersion INTEGER NOT NULL,
|
|
54
|
+
addedBy TEXT NOT NULL
|
|
55
|
+
);
|
|
56
|
+
`;
|
|
57
|
+
const CREATE_CATEGORY_INDEX_SQL = `
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_apis_category ON apis(category);
|
|
59
|
+
`;
|
|
60
|
+
const CREATE_QUALITY_INDEX_SQL = `
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_apis_quality ON apis(qualityScore DESC);
|
|
62
|
+
`;
|
|
63
|
+
// ─── Serialization helpers ────────────────────────────────────────────────────
|
|
64
|
+
function serializeEntry(entry) {
|
|
65
|
+
const row = { ...entry };
|
|
66
|
+
for (const col of JSON_COLUMNS) {
|
|
67
|
+
row[col] = JSON.stringify(entry[col]);
|
|
68
|
+
}
|
|
69
|
+
return row;
|
|
70
|
+
}
|
|
71
|
+
function deserializeRow(row) {
|
|
72
|
+
const deserialized = { ...row };
|
|
73
|
+
for (const col of JSON_COLUMNS) {
|
|
74
|
+
if (typeof deserialized[col] === "string") {
|
|
75
|
+
deserialized[col] = JSON.parse(deserialized[col]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const result = ApiEntrySchema.safeParse(deserialized);
|
|
79
|
+
if (!result.success) {
|
|
80
|
+
const issues = result.error.issues.map((i) => i.message).join(", ");
|
|
81
|
+
throw new Error(`Stored entry is invalid: ${issues}`);
|
|
82
|
+
}
|
|
83
|
+
return result.data;
|
|
84
|
+
}
|
|
85
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
86
|
+
export function initDb(dbPath = "./data/api-oracle.db") {
|
|
87
|
+
if (db)
|
|
88
|
+
return; // already initialized
|
|
89
|
+
if (dbPath !== ":memory:") {
|
|
90
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
db = new Database(dbPath);
|
|
93
|
+
db.pragma("journal_mode = WAL");
|
|
94
|
+
db.pragma("foreign_keys = ON");
|
|
95
|
+
db.exec(CREATE_TABLE_SQL);
|
|
96
|
+
db.exec(CREATE_CATEGORY_INDEX_SQL);
|
|
97
|
+
db.exec(CREATE_QUALITY_INDEX_SQL);
|
|
98
|
+
logger.info("Database initialized", { dbPath });
|
|
99
|
+
}
|
|
100
|
+
export function getDb() {
|
|
101
|
+
if (!db)
|
|
102
|
+
throw new Error("Database not initialized — call initDb() first");
|
|
103
|
+
return db;
|
|
104
|
+
}
|
|
105
|
+
export function closeDb() {
|
|
106
|
+
if (db) {
|
|
107
|
+
db.close();
|
|
108
|
+
db = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Allows tests to inject their own in-memory database instance */
|
|
112
|
+
export function _setDbForTesting(testDb) {
|
|
113
|
+
testDb.exec(CREATE_TABLE_SQL);
|
|
114
|
+
testDb.exec(CREATE_CATEGORY_INDEX_SQL);
|
|
115
|
+
testDb.exec(CREATE_QUALITY_INDEX_SQL);
|
|
116
|
+
db = testDb;
|
|
117
|
+
}
|
|
118
|
+
export function insertEntry(entry) {
|
|
119
|
+
const validation = validateEntry(entry);
|
|
120
|
+
if (!validation.success) {
|
|
121
|
+
throw new Error(`Invalid entry: ${validation.errors.join("; ")}`);
|
|
122
|
+
}
|
|
123
|
+
const row = serializeEntry(validation.data);
|
|
124
|
+
const database = getDb();
|
|
125
|
+
const columns = Object.keys(row);
|
|
126
|
+
const placeholders = columns.map((c) => `@${c}`).join(", ");
|
|
127
|
+
const sql = `INSERT INTO apis (${columns.join(", ")}) VALUES (${placeholders})`;
|
|
128
|
+
const stmt = database.prepare(sql);
|
|
129
|
+
stmt.run(row);
|
|
130
|
+
logger.info("Inserted entry", { slug: entry.slug });
|
|
131
|
+
}
|
|
132
|
+
export function getBySlug(slug) {
|
|
133
|
+
const database = getDb();
|
|
134
|
+
const row = database
|
|
135
|
+
.prepare("SELECT * FROM apis WHERE slug = ?")
|
|
136
|
+
.get(slug);
|
|
137
|
+
if (!row)
|
|
138
|
+
return null;
|
|
139
|
+
return deserializeRow(row);
|
|
140
|
+
}
|
|
141
|
+
export function getByCategory(category) {
|
|
142
|
+
const database = getDb();
|
|
143
|
+
const rows = database
|
|
144
|
+
.prepare("SELECT * FROM apis WHERE category = ? ORDER BY qualityScore DESC")
|
|
145
|
+
.all(category);
|
|
146
|
+
return rows.map(deserializeRow);
|
|
147
|
+
}
|
|
148
|
+
export function searchByText(query) {
|
|
149
|
+
const database = getDb();
|
|
150
|
+
const pattern = `%${query}%`;
|
|
151
|
+
// Search across name, description, subcategory, bestFor, and JSON-encoded useCases
|
|
152
|
+
const rows = database
|
|
153
|
+
.prepare(`SELECT * FROM apis
|
|
154
|
+
WHERE name LIKE ?
|
|
155
|
+
OR description LIKE ?
|
|
156
|
+
OR subcategory LIKE ?
|
|
157
|
+
OR bestFor LIKE ?
|
|
158
|
+
OR useCases LIKE ?
|
|
159
|
+
OR category LIKE ?
|
|
160
|
+
ORDER BY qualityScore DESC`)
|
|
161
|
+
.all(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
162
|
+
return rows.map(deserializeRow);
|
|
163
|
+
}
|
|
164
|
+
export function getAllEntries() {
|
|
165
|
+
const database = getDb();
|
|
166
|
+
const rows = database
|
|
167
|
+
.prepare("SELECT * FROM apis ORDER BY qualityScore DESC")
|
|
168
|
+
.all();
|
|
169
|
+
return rows.map(deserializeRow);
|
|
170
|
+
}
|
|
171
|
+
export function deleteBySlug(slug) {
|
|
172
|
+
const database = getDb();
|
|
173
|
+
database.prepare("DELETE FROM apis WHERE slug = ?").run(slug);
|
|
174
|
+
logger.info("Deleted entry", { slug });
|
|
175
|
+
}
|
|
176
|
+
export function slugExists(slug) {
|
|
177
|
+
const database = getDb();
|
|
178
|
+
const row = database
|
|
179
|
+
.prepare("SELECT 1 FROM apis WHERE slug = ?")
|
|
180
|
+
.get(slug);
|
|
181
|
+
return row !== undefined;
|
|
182
|
+
}
|