@squadbase/vite-server 0.0.1-build-1

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/index.js ADDED
@@ -0,0 +1,743 @@
1
+ // src/index.ts
2
+ import { Hono as Hono5 } from "hono";
3
+ import { cors } from "hono/cors";
4
+ import path4 from "path";
5
+
6
+ // src/registry.ts
7
+ import { readdir, readFile, mkdir } from "fs/promises";
8
+ import { watch as fsWatch2 } from "fs";
9
+ import path2 from "path";
10
+
11
+ // src/connector-client/registry.ts
12
+ import { readFileSync, watch as fsWatch } from "fs";
13
+ import path from "path";
14
+
15
+ // src/connector-client/postgresql.ts
16
+ import pg from "pg";
17
+ var { Pool } = pg;
18
+ function createPostgreSQLClient(connectionString) {
19
+ const pool = new Pool({ connectionString, ssl: { rejectUnauthorized: false } });
20
+ return {
21
+ async query(sql, params) {
22
+ const result = await pool.query(sql, params);
23
+ return { rows: result.rows };
24
+ }
25
+ };
26
+ }
27
+
28
+ // src/connector-client/env.ts
29
+ function resolveEnvVar(entry, key, slug) {
30
+ const envVarName = entry.envVars[key];
31
+ if (!envVarName) {
32
+ throw new Error(`Connector "${slug}" is missing envVars mapping for key "${key}"`);
33
+ }
34
+ const value = process.env[envVarName];
35
+ if (!value) {
36
+ throw new Error(`Environment variable "${envVarName}" (for "${slug}.${key}") is not set`);
37
+ }
38
+ return value;
39
+ }
40
+
41
+ // src/connector-client/bigquery.ts
42
+ function createBigQueryClient(entry, slug) {
43
+ const projectId = resolveEnvVar(entry, "project-id", slug);
44
+ const serviceAccountJsonBase64 = resolveEnvVar(entry, "service-account-json-base64", slug);
45
+ const serviceAccountJson = Buffer.from(serviceAccountJsonBase64, "base64").toString("utf-8");
46
+ let gcpCredentials;
47
+ try {
48
+ gcpCredentials = JSON.parse(serviceAccountJson);
49
+ } catch {
50
+ throw new Error(
51
+ `BigQuery service account JSON (decoded from base64) is not valid JSON for slug "${slug}"`
52
+ );
53
+ }
54
+ return {
55
+ async query(sql) {
56
+ const { BigQuery } = await import("@google-cloud/bigquery");
57
+ const bq = new BigQuery({ projectId, credentials: gcpCredentials });
58
+ const [job] = await bq.createQueryJob({ query: sql });
59
+ const [allRows] = await job.getQueryResults({ timeoutMs: 3e4 });
60
+ return { rows: allRows };
61
+ }
62
+ };
63
+ }
64
+
65
+ // src/connector-client/snowflake.ts
66
+ function createSnowflakeClient(entry, slug) {
67
+ const accountIdentifier = resolveEnvVar(entry, "account", slug);
68
+ const user = resolveEnvVar(entry, "user", slug);
69
+ const role = resolveEnvVar(entry, "role", slug);
70
+ const warehouse = resolveEnvVar(entry, "warehouse", slug);
71
+ const privateKeyBase64 = resolveEnvVar(entry, "private-key-base64", slug);
72
+ const privateKey = Buffer.from(privateKeyBase64, "base64").toString("utf-8");
73
+ return {
74
+ async query(sql) {
75
+ const snowflake = (await import("snowflake-sdk")).default;
76
+ snowflake.configure({ logLevel: "ERROR" });
77
+ const connection = snowflake.createConnection({
78
+ account: accountIdentifier,
79
+ username: user,
80
+ role,
81
+ warehouse,
82
+ authenticator: "SNOWFLAKE_JWT",
83
+ privateKey
84
+ });
85
+ await new Promise((resolve, reject) => {
86
+ connection.connect((err) => {
87
+ if (err) reject(new Error(`Snowflake connect failed: ${err.message}`));
88
+ else resolve();
89
+ });
90
+ });
91
+ const rows = await new Promise((resolve, reject) => {
92
+ connection.execute({
93
+ sqlText: sql,
94
+ complete: (err, _stmt, rows2) => {
95
+ if (err) reject(new Error(`Snowflake query failed: ${err.message}`));
96
+ else resolve(rows2 ?? []);
97
+ }
98
+ });
99
+ });
100
+ connection.destroy((err) => {
101
+ if (err) console.warn(`[connector-client] Snowflake destroy error: ${err.message}`);
102
+ });
103
+ return { rows };
104
+ }
105
+ };
106
+ }
107
+
108
+ // src/connector-client/registry.ts
109
+ function createConnectorRegistry() {
110
+ let connectionsCache = null;
111
+ const clientCache = /* @__PURE__ */ new Map();
112
+ function getConnectionsFilePath() {
113
+ return process.env.CONNECTIONS_PATH ?? path.join(process.cwd(), "../../.squadbase/connections.json");
114
+ }
115
+ function loadConnections() {
116
+ if (connectionsCache !== null) return connectionsCache;
117
+ const filePath = getConnectionsFilePath();
118
+ try {
119
+ const raw = readFileSync(filePath, "utf-8");
120
+ connectionsCache = JSON.parse(raw);
121
+ } catch {
122
+ connectionsCache = {};
123
+ }
124
+ return connectionsCache;
125
+ }
126
+ async function getClient2(connectorSlug, connectorType) {
127
+ if (!connectorSlug) {
128
+ const cacheKey = "__squadbase-db__";
129
+ const cached2 = clientCache.get(cacheKey);
130
+ if (cached2) return cached2;
131
+ const url = process.env.SQUADBASE_POSTGRESQL_URL;
132
+ if (!url) throw new Error("SQUADBASE_POSTGRESQL_URL environment variable is not set");
133
+ const client = createPostgreSQLClient(url);
134
+ clientCache.set(cacheKey, client);
135
+ return client;
136
+ }
137
+ const cached = clientCache.get(connectorSlug);
138
+ if (cached) return cached;
139
+ const connections = loadConnections();
140
+ const entry = connections[connectorSlug];
141
+ if (!entry) {
142
+ throw new Error(`connector slug '${connectorSlug}' not found in .squadbase/connections.json`);
143
+ }
144
+ const resolvedType = connectorType ?? entry.connectorType;
145
+ if (!resolvedType) {
146
+ throw new Error(
147
+ `connector type could not be determined for slug '${connectorSlug}'. Specify connectorType in the data-source JSON or in .squadbase/connections.json.`
148
+ );
149
+ }
150
+ if (resolvedType === "snowflake") {
151
+ return createSnowflakeClient(entry, connectorSlug);
152
+ }
153
+ if (resolvedType === "bigquery") {
154
+ return createBigQueryClient(entry, connectorSlug);
155
+ }
156
+ if (resolvedType === "postgresql" || resolvedType === "squadbase-db") {
157
+ const urlEnvName = entry.envVars["connection-url"];
158
+ if (!urlEnvName) {
159
+ throw new Error(`'connection-url' is not defined in envVars for connector '${connectorSlug}'`);
160
+ }
161
+ const connectionUrl = process.env[urlEnvName];
162
+ if (!connectionUrl) {
163
+ throw new Error(
164
+ `environment variable '${urlEnvName}' (mapped from connector '${connectorSlug}') is not set`
165
+ );
166
+ }
167
+ const client = createPostgreSQLClient(connectionUrl);
168
+ clientCache.set(connectorSlug, client);
169
+ return client;
170
+ }
171
+ throw new Error(
172
+ `connector type '${resolvedType}' is not supported. Supported: "snowflake", "bigquery", "postgresql", "squadbase-db"`
173
+ );
174
+ }
175
+ function reloadEnvFile2(envPath) {
176
+ try {
177
+ const raw = readFileSync(envPath, "utf-8");
178
+ for (const line of raw.split("\n")) {
179
+ const trimmed = line.trim();
180
+ if (!trimmed || trimmed.startsWith("#")) continue;
181
+ const eqIdx = trimmed.indexOf("=");
182
+ if (eqIdx === -1) continue;
183
+ const key = trimmed.slice(0, eqIdx).trim();
184
+ const value = trimmed.slice(eqIdx + 1).trim();
185
+ if (key) process.env[key] = value;
186
+ }
187
+ console.log("[connector-client] .env reloaded");
188
+ } catch {
189
+ }
190
+ }
191
+ function watchConnectionsFile2() {
192
+ const filePath = getConnectionsFilePath();
193
+ const envPath = path.join(process.cwd(), "..", "..", ".env");
194
+ try {
195
+ fsWatch(filePath, { persistent: false }, () => {
196
+ console.log("[connector-client] connections.json changed, clearing cache");
197
+ connectionsCache = null;
198
+ clientCache.clear();
199
+ setImmediate(() => reloadEnvFile2(envPath));
200
+ });
201
+ } catch {
202
+ }
203
+ }
204
+ return { getClient: getClient2, reloadEnvFile: reloadEnvFile2, watchConnectionsFile: watchConnectionsFile2 };
205
+ }
206
+
207
+ // src/connector-client/index.ts
208
+ var { getClient, reloadEnvFile, watchConnectionsFile } = createConnectorRegistry();
209
+
210
+ // src/registry.ts
211
+ var dataSources = /* @__PURE__ */ new Map();
212
+ var viteServer = null;
213
+ function validateHandlerPath(dirPath, handlerPath) {
214
+ const absolute = path2.resolve(dirPath, handlerPath);
215
+ const normalizedDir = path2.resolve(dirPath);
216
+ if (!absolute.startsWith(normalizedDir + path2.sep)) {
217
+ throw new Error(`Handler path escapes data-source directory: ${handlerPath}`);
218
+ }
219
+ if (!absolute.endsWith(".ts")) {
220
+ throw new Error(`Handler must be a .ts file: ${handlerPath}`);
221
+ }
222
+ return absolute;
223
+ }
224
+ async function loadTypeScriptHandler(absolutePath) {
225
+ let mod;
226
+ if (viteServer) {
227
+ const module = viteServer.moduleGraph.getModuleById(absolutePath);
228
+ if (module) viteServer.moduleGraph.invalidateModule(module);
229
+ mod = await viteServer.ssrLoadModule(absolutePath);
230
+ } else {
231
+ const { pathToFileURL } = await import("url");
232
+ mod = await import(pathToFileURL(absolutePath).href);
233
+ }
234
+ const handler = mod.default;
235
+ if (typeof handler !== "function") {
236
+ throw new Error(`Handler must export a default function: ${absolutePath}`);
237
+ }
238
+ return handler;
239
+ }
240
+ function buildQuery(queryTemplate, parameterMeta, runtimeParams) {
241
+ const defaults = new Map(
242
+ parameterMeta.map((p) => [p.name, p.default ?? null])
243
+ );
244
+ const placeholderToIndex = /* @__PURE__ */ new Map();
245
+ const values = [];
246
+ const text = queryTemplate.replace(
247
+ /\{\{(\w+)\}\}/g,
248
+ (_match, name) => {
249
+ if (!placeholderToIndex.has(name)) {
250
+ const value = Object.prototype.hasOwnProperty.call(runtimeParams, name) ? runtimeParams[name] : defaults.get(name) ?? null;
251
+ values.push(value);
252
+ placeholderToIndex.set(name, values.length);
253
+ }
254
+ return `$${placeholderToIndex.get(name)}`;
255
+ }
256
+ );
257
+ return { text, values };
258
+ }
259
+ var defaultDataSourceDir = path2.join(process.cwd(), "data-source");
260
+ async function initialize() {
261
+ console.log(
262
+ `[registry] loading data sources from ${defaultDataSourceDir}...`
263
+ );
264
+ dataSources.clear();
265
+ const dirPath = process.env.DATA_SOURCE_DIR || defaultDataSourceDir;
266
+ await mkdir(dirPath, { recursive: true });
267
+ const files = await readdir(dirPath);
268
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
269
+ const results = await Promise.allSettled(
270
+ jsonFiles.map(async (file) => {
271
+ const slug = file.replace(/\.json$/, "");
272
+ const raw = await readFile(`${dirPath}/${file}`, "utf-8");
273
+ const def = JSON.parse(raw);
274
+ if (!def.description) {
275
+ console.warn(`[registry] Skipping ${file}: missing description`);
276
+ return;
277
+ }
278
+ if (def.type === "typescript") {
279
+ if (!def.handlerPath) {
280
+ console.warn(`[registry] Skipping ${file}: missing handlerPath`);
281
+ return;
282
+ }
283
+ const absoluteHandlerPath = validateHandlerPath(dirPath, def.handlerPath);
284
+ const dataSourceDef = {
285
+ description: def.description,
286
+ parameters: def.parameters ?? [],
287
+ response: def.response,
288
+ cacheConfig: def.cache,
289
+ handler: async () => {
290
+ throw new Error("TypeScript handler must be called via _tsHandlerPath");
291
+ },
292
+ _isTypescript: true,
293
+ _tsHandlerPath: absoluteHandlerPath
294
+ };
295
+ dataSources.set(slug, dataSourceDef);
296
+ console.log(`[registry] registered (typescript): ${slug}`);
297
+ } else {
298
+ const sqlDef = def;
299
+ if (!sqlDef.query) {
300
+ console.warn(`[registry] Skipping ${file}: missing query`);
301
+ return;
302
+ }
303
+ const dataSourceDef = {
304
+ description: sqlDef.description,
305
+ parameters: sqlDef.parameters ?? [],
306
+ response: sqlDef.response,
307
+ connectorSlug: sqlDef.connectorSlug,
308
+ cacheConfig: sqlDef.cache,
309
+ handler: async (runtimeParams) => {
310
+ const client = await getClient(sqlDef.connectorSlug, sqlDef.connectorType);
311
+ const isExternalConnector = sqlDef.connectorType === "snowflake" || sqlDef.connectorType === "bigquery";
312
+ let queryText;
313
+ let queryValues;
314
+ if (isExternalConnector) {
315
+ const defaults = new Map(
316
+ (sqlDef.parameters ?? []).map((p) => [p.name, p.default ?? null])
317
+ );
318
+ queryText = sqlDef.query.replace(
319
+ /\{\{(\w+)\}\}/g,
320
+ (_match, name) => {
321
+ const value = Object.prototype.hasOwnProperty.call(
322
+ runtimeParams,
323
+ name
324
+ ) ? runtimeParams[name] : defaults.get(name) ?? "";
325
+ if (typeof value === "string")
326
+ return `'${value.replace(/'/g, "''")}'`;
327
+ if (value === null || value === void 0) return "NULL";
328
+ return String(value);
329
+ }
330
+ );
331
+ queryValues = [];
332
+ } else {
333
+ const built = buildQuery(
334
+ sqlDef.query,
335
+ sqlDef.parameters ?? [],
336
+ runtimeParams
337
+ );
338
+ queryText = built.text;
339
+ queryValues = built.values;
340
+ }
341
+ const result = await client.query(queryText, queryValues);
342
+ return result.rows;
343
+ }
344
+ };
345
+ dataSources.set(slug, dataSourceDef);
346
+ console.log(`[registry] registered: ${slug}`);
347
+ }
348
+ })
349
+ );
350
+ results.forEach((result, i) => {
351
+ if (result.status === "rejected") {
352
+ console.error(
353
+ `[registry] Failed to load ${jsonFiles[i]}:`,
354
+ result.reason
355
+ );
356
+ }
357
+ });
358
+ console.log(`[registry] ${dataSources.size} data source(s) ready`);
359
+ }
360
+ var reloadTimer = null;
361
+ function startWatching() {
362
+ const dirPath = process.env.DATA_SOURCE_DIR || defaultDataSourceDir;
363
+ try {
364
+ fsWatch2(dirPath, { persistent: false }, (_event, filename) => {
365
+ if (!filename?.endsWith(".json") && !filename?.endsWith(".ts")) return;
366
+ if (reloadTimer) clearTimeout(reloadTimer);
367
+ reloadTimer = setTimeout(async () => {
368
+ console.log("[registry] data-source changed, reloading...");
369
+ await initialize();
370
+ }, 300);
371
+ });
372
+ console.log("[registry] watching data-source directory");
373
+ } catch {
374
+ console.warn(
375
+ "[registry] could not watch data-source directory (static load only)"
376
+ );
377
+ }
378
+ }
379
+ function getDataSource(slug) {
380
+ return dataSources.get(slug);
381
+ }
382
+ function getAllMeta() {
383
+ return Array.from(dataSources.entries()).map(([slug, def]) => ({
384
+ slug,
385
+ description: def.description,
386
+ parameters: def.parameters,
387
+ response: def.response,
388
+ connectorSlug: def.connectorSlug
389
+ }));
390
+ }
391
+ function getMeta(slug) {
392
+ const def = dataSources.get(slug);
393
+ if (!def) return void 0;
394
+ return {
395
+ slug,
396
+ description: def.description,
397
+ parameters: def.parameters,
398
+ response: def.response,
399
+ connectorSlug: def.connectorSlug
400
+ };
401
+ }
402
+
403
+ // src/routes/data-source.ts
404
+ import { Hono } from "hono";
405
+
406
+ // src/cache.ts
407
+ var MAX_SIZE = 100;
408
+ var cache = /* @__PURE__ */ new Map();
409
+ var totalHits = 0;
410
+ var totalMisses = 0;
411
+ function cacheGet(key) {
412
+ const entry = cache.get(key);
413
+ if (!entry) {
414
+ totalMisses++;
415
+ return void 0;
416
+ }
417
+ cache.delete(key);
418
+ cache.set(key, entry);
419
+ return entry;
420
+ }
421
+ function isFresh(entry) {
422
+ if (entry.ttl <= 0) return false;
423
+ const ageMs = Date.now() - entry.cachedAt;
424
+ return ageMs < entry.ttl * 1e3;
425
+ }
426
+ function cacheSet(key, data, ttl) {
427
+ if (ttl <= 0) return;
428
+ if (cache.has(key)) {
429
+ cache.delete(key);
430
+ } else if (cache.size >= MAX_SIZE) {
431
+ const oldestKey = cache.keys().next().value;
432
+ if (oldestKey !== void 0) {
433
+ cache.delete(oldestKey);
434
+ console.log(`[cache] evicted: ${oldestKey}`);
435
+ }
436
+ }
437
+ cache.set(key, {
438
+ data,
439
+ cachedAt: Date.now(),
440
+ ttl,
441
+ hits: 0
442
+ });
443
+ }
444
+ function recordHit(key) {
445
+ totalHits++;
446
+ const entry = cache.get(key);
447
+ if (entry) {
448
+ entry.hits++;
449
+ }
450
+ }
451
+ function invalidateSlug(slug) {
452
+ const prefix = `${slug}:`;
453
+ let count = 0;
454
+ for (const key of cache.keys()) {
455
+ if (key.startsWith(prefix)) {
456
+ cache.delete(key);
457
+ count++;
458
+ }
459
+ }
460
+ console.log(`[cache] invalidated ${count} entries for slug: ${slug}`);
461
+ return count;
462
+ }
463
+ function invalidateAll() {
464
+ const count = cache.size;
465
+ cache.clear();
466
+ totalHits = 0;
467
+ totalMisses = 0;
468
+ console.log(`[cache] invalidated all ${count} entries`);
469
+ return count;
470
+ }
471
+ function getStats() {
472
+ const now = Date.now();
473
+ const entries = Array.from(cache.entries()).map(([key, entry]) => ({
474
+ key,
475
+ cachedAt: entry.cachedAt,
476
+ ttl: entry.ttl,
477
+ ageSeconds: Math.floor((now - entry.cachedAt) / 1e3),
478
+ hits: entry.hits,
479
+ expired: !isFresh(entry)
480
+ }));
481
+ return {
482
+ size: cache.size,
483
+ maxSize: MAX_SIZE,
484
+ totalHits,
485
+ totalMisses,
486
+ entries
487
+ };
488
+ }
489
+ function buildCacheKey(slug, params) {
490
+ const sortedParams = Object.fromEntries(
491
+ Object.entries(params).sort(([a], [b]) => a.localeCompare(b))
492
+ );
493
+ return `${slug}:${JSON.stringify(sortedParams)}`;
494
+ }
495
+
496
+ // src/lib/csv.ts
497
+ function convertRowsToCsv(rows) {
498
+ if (rows.length === 0) return "";
499
+ const headers = Object.keys(rows[0]);
500
+ const escape = (v) => {
501
+ const s = v === null || v === void 0 ? "" : String(v);
502
+ return s.includes(",") || s.includes('"') || s.includes("\n") ? `"${s.replace(/"/g, '""')}"` : s;
503
+ };
504
+ return [
505
+ headers.join(","),
506
+ ...rows.map((r) => headers.map((h) => escape(r[h])).join(","))
507
+ ].join("\n");
508
+ }
509
+
510
+ // src/routes/data-source.ts
511
+ var app = new Hono();
512
+ function buildResponse(c, result, response) {
513
+ const contentType = response?.defaultContentType ?? "application/json";
514
+ if (contentType === "text/csv") {
515
+ const csv = convertRowsToCsv(result);
516
+ return c.text(csv, 200, { "Content-Type": "text/csv; charset=utf-8" });
517
+ }
518
+ const schema = response?.content?.["application/json"]?.schema;
519
+ if (schema?.type === "object" && schema.properties) {
520
+ return c.json(result);
521
+ }
522
+ return c.json({ data: result });
523
+ }
524
+ app.get("/:slug", async (c) => {
525
+ const slug = c.req.param("slug");
526
+ const ds = getDataSource(slug);
527
+ if (!ds) {
528
+ return c.json({ error: `Data source '${slug}' not found` }, 404);
529
+ }
530
+ try {
531
+ let result;
532
+ if (ds._isTypescript && ds._tsHandlerPath) {
533
+ const handler = await loadTypeScriptHandler(ds._tsHandlerPath);
534
+ result = await handler(c);
535
+ } else {
536
+ result = await ds.handler({});
537
+ }
538
+ return buildResponse(c, result, ds.response);
539
+ } catch (e) {
540
+ console.error(`[data-source] ${slug} error:`, e);
541
+ return c.json(
542
+ { error: e instanceof Error ? e.message : "Internal error" },
543
+ 500
544
+ );
545
+ }
546
+ });
547
+ app.post("/:slug", async (c) => {
548
+ const slug = c.req.param("slug");
549
+ const ds = getDataSource(slug);
550
+ if (!ds) {
551
+ return c.json({ error: `Data source '${slug}' not found` }, 404);
552
+ }
553
+ try {
554
+ const body = await c.req.json().catch(() => ({}));
555
+ const params = body.params ?? {};
556
+ const cacheConfig = ds.cacheConfig;
557
+ const ttl = cacheConfig?.ttl ?? 0;
558
+ if (ttl <= 0) {
559
+ let result2;
560
+ if (ds._isTypescript && ds._tsHandlerPath) {
561
+ const handler = await loadTypeScriptHandler(ds._tsHandlerPath);
562
+ result2 = await handler(c);
563
+ } else {
564
+ result2 = await ds.handler(params);
565
+ }
566
+ return buildResponse(c, result2, ds.response);
567
+ }
568
+ const cacheKey = buildCacheKey(slug, params);
569
+ const cached = cacheGet(cacheKey);
570
+ if (cached) {
571
+ if (isFresh(cached)) {
572
+ recordHit(cacheKey);
573
+ const ageSeconds = Math.floor((Date.now() - cached.cachedAt) / 1e3);
574
+ c.header("X-Cache", "HIT");
575
+ c.header("X-Cache-Age", String(ageSeconds));
576
+ c.header("Cache-Control", `max-age=${ttl - ageSeconds}`);
577
+ return buildResponse(c, cached.data, ds.response);
578
+ }
579
+ if (cacheConfig?.staleWhileRevalidate) {
580
+ recordHit(cacheKey);
581
+ const ageSeconds = Math.floor((Date.now() - cached.cachedAt) / 1e3);
582
+ c.header("X-Cache", "STALE");
583
+ c.header("X-Cache-Age", String(ageSeconds));
584
+ c.header("Cache-Control", `max-age=0, stale-while-revalidate=${ttl}`);
585
+ void (async () => {
586
+ try {
587
+ let freshData;
588
+ if (ds._isTypescript && ds._tsHandlerPath) {
589
+ const tsHandler = await loadTypeScriptHandler(ds._tsHandlerPath);
590
+ freshData = await tsHandler(c);
591
+ } else {
592
+ freshData = await ds.handler(params);
593
+ }
594
+ cacheSet(cacheKey, freshData, ttl);
595
+ console.log(`[cache] background revalidated: ${cacheKey}`);
596
+ } catch (e) {
597
+ console.error(`[cache] background revalidation failed for ${slug}:`, e);
598
+ }
599
+ })();
600
+ return buildResponse(c, cached.data, ds.response);
601
+ }
602
+ }
603
+ let result;
604
+ if (ds._isTypescript && ds._tsHandlerPath) {
605
+ const handler = await loadTypeScriptHandler(ds._tsHandlerPath);
606
+ result = await handler(c);
607
+ } else {
608
+ result = await ds.handler(params);
609
+ }
610
+ cacheSet(cacheKey, result, ttl);
611
+ c.header("X-Cache", "MISS");
612
+ c.header("X-Cache-Age", "0");
613
+ c.header("Cache-Control", `max-age=${ttl}`);
614
+ return buildResponse(c, result, ds.response);
615
+ } catch (e) {
616
+ console.error(`[data-source] ${slug} error:`, e);
617
+ return c.json(
618
+ { error: e instanceof Error ? e.message : "Internal error" },
619
+ 500
620
+ );
621
+ }
622
+ });
623
+ var data_source_default = app;
624
+
625
+ // src/routes/data-source-meta.ts
626
+ import { Hono as Hono2 } from "hono";
627
+ var app2 = new Hono2();
628
+ app2.get("/", (c) => {
629
+ return c.json(getAllMeta());
630
+ });
631
+ app2.get("/:slug", (c) => {
632
+ const slug = c.req.param("slug");
633
+ const meta = getMeta(slug);
634
+ if (!meta) {
635
+ return c.json({ error: `Data source '${slug}' not found` }, 404);
636
+ }
637
+ return c.json(meta);
638
+ });
639
+ var data_source_meta_default = app2;
640
+
641
+ // src/routes/cache.ts
642
+ import { Hono as Hono3 } from "hono";
643
+ var app3 = new Hono3();
644
+ app3.get("/stats", (c) => {
645
+ const stats = getStats();
646
+ const total = stats.totalHits + stats.totalMisses;
647
+ const hitRate = total > 0 ? `${(stats.totalHits / total * 100).toFixed(2)}%` : "N/A";
648
+ return c.json({ ...stats, hitRate });
649
+ });
650
+ app3.post("/invalidate", (c) => {
651
+ const count = invalidateAll();
652
+ return c.json({ invalidated: count, message: "All cache entries cleared" });
653
+ });
654
+ app3.post("/invalidate/:slug", (c) => {
655
+ const slug = c.req.param("slug");
656
+ const count = invalidateSlug(slug);
657
+ return c.json({ slug, invalidated: count });
658
+ });
659
+ var cache_default = app3;
660
+
661
+ // src/routes/pages.ts
662
+ import { Hono as Hono4 } from "hono";
663
+ import path3 from "path";
664
+ import fs from "fs";
665
+ var DATA_DIR = process.env.DATA_DIR ?? path3.join(process.cwd(), "data");
666
+ var app4 = new Hono4();
667
+ function getPageList() {
668
+ if (!fs.existsSync(DATA_DIR)) return [];
669
+ const files = fs.readdirSync(DATA_DIR).filter((f) => f.endsWith(".json"));
670
+ const pages = files.map((file) => {
671
+ const name = path3.basename(file, ".json");
672
+ const urlPath = name === "home" ? "/" : "/" + name.replace(/-/g, "/");
673
+ let title = name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, " ");
674
+ try {
675
+ const raw = JSON.parse(
676
+ fs.readFileSync(path3.join(DATA_DIR, file), "utf-8")
677
+ );
678
+ const puckData = raw.pageData || raw;
679
+ if (puckData?.root?.props?.title) {
680
+ title = puckData.root.props.title;
681
+ }
682
+ } catch {
683
+ }
684
+ return { name, path: urlPath, title };
685
+ });
686
+ pages.sort((a, b) => {
687
+ if (a.name === "home") return -1;
688
+ if (b.name === "home") return 1;
689
+ return a.name.localeCompare(b.name);
690
+ });
691
+ return pages;
692
+ }
693
+ app4.get("/pages", (c) => c.json(getPageList()));
694
+ app4.get("/page-data", (c) => {
695
+ const page = c.req.query("page") || "home";
696
+ const safePage = page.replace(/[^a-zA-Z0-9-_]/g, "");
697
+ const filePath = path3.join(DATA_DIR, `${safePage}.json`);
698
+ if (!fs.existsSync(filePath)) return c.json({ error: "not found" }, 404);
699
+ try {
700
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
701
+ if (raw.pageData) {
702
+ return c.json({
703
+ runtimeData: raw.runtimeData || { queries: [] },
704
+ pageData: raw.pageData
705
+ });
706
+ }
707
+ return c.json({ runtimeData: { queries: [] }, pageData: raw });
708
+ } catch {
709
+ return c.json({ error: "Failed to parse file" }, 500);
710
+ }
711
+ });
712
+ app4.get("/runtime-data", (c) => {
713
+ const page = c.req.query("page") || "home";
714
+ const safePage = page.replace(/[^a-zA-Z0-9-_]/g, "");
715
+ const filePath = path3.join(DATA_DIR, `${safePage}.json`);
716
+ if (!fs.existsSync(filePath)) return c.json({ queries: [] });
717
+ try {
718
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
719
+ return c.json(raw.runtimeData || { queries: [] });
720
+ } catch {
721
+ return c.json({ queries: [] });
722
+ }
723
+ });
724
+ var pages_default = app4;
725
+
726
+ // src/index.ts
727
+ var apiApp = new Hono5();
728
+ apiApp.use("/*", cors());
729
+ apiApp.route("/data-source", data_source_default);
730
+ apiApp.route("/data-source-meta", data_source_meta_default);
731
+ apiApp.route("/cache", cache_default);
732
+ apiApp.route("/", pages_default);
733
+ reloadEnvFile(path4.join(process.cwd(), "..", "..", ".env"));
734
+ await initialize();
735
+ startWatching();
736
+ watchConnectionsFile();
737
+ var app5 = new Hono5();
738
+ app5.get("/healthz", (c) => c.json({ status: "ok" }));
739
+ app5.route("/api", apiApp);
740
+ var src_default = app5;
741
+ export {
742
+ src_default as default
743
+ };