nothing-browser 0.0.16 → 0.0.18
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/client/index.js +224 -40
- package/dist/piggy/client/index.d.ts +77 -2
- package/dist/piggy/client/index.d.ts.map +1 -1
- package/dist/piggy/pool/index.d.ts +23 -0
- package/dist/piggy/pool/index.d.ts.map +1 -0
- package/dist/piggy/register/index.d.ts +3 -1
- package/dist/piggy/register/index.d.ts.map +1 -1
- package/dist/piggy/server/index.d.ts +22 -1
- package/dist/piggy/server/index.d.ts.map +1 -1
- package/dist/piggy/store/index.d.ts +22 -0
- package/dist/piggy/store/index.d.ts.map +1 -0
- package/dist/piggy.d.ts +6 -174
- package/dist/piggy.d.ts.map +1 -1
- package/dist/piggy.js +7736 -277
- package/dist/register/index.js +6291 -205
- package/dist/server/index.js +6252 -79
- package/package.json +3 -1
- package/piggy/client/index.ts +325 -54
- package/piggy/pool/index.d.ts +12 -0
- package/piggy/pool/index.ts +75 -0
- package/piggy/register/index.ts +231 -214
- package/piggy/server/index.d.ts +51 -14
- package/piggy/server/index.ts +68 -15
- package/piggy/store/index.d.ts +26 -0
- package/piggy/store/index.ts +230 -0
- package/piggy.ts +118 -320
package/piggy/server/index.d.ts
CHANGED
|
@@ -1,21 +1,58 @@
|
|
|
1
|
+
// piggy/server/index.d.ts
|
|
1
2
|
import { Elysia } from "elysia";
|
|
3
|
+
|
|
2
4
|
export type BeforeMiddleware = (ctx: {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
params: Record<string, string>;
|
|
6
|
+
query: Record<string, string>;
|
|
7
|
+
body: any;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
set: any;
|
|
8
10
|
}) => void | Promise<void>;
|
|
9
|
-
|
|
11
|
+
|
|
12
|
+
export type RouteHandler = (
|
|
13
|
+
params: Record<string, string>,
|
|
14
|
+
query: Record<string, string>,
|
|
15
|
+
body: any
|
|
16
|
+
) => Promise<any>;
|
|
17
|
+
|
|
18
|
+
export interface RouteParameter {
|
|
19
|
+
name: string;
|
|
20
|
+
in: "query" | "path" | "header" | "cookie";
|
|
21
|
+
description?: string;
|
|
22
|
+
required?: boolean;
|
|
23
|
+
schema?: Record<string, any>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RouteDetail {
|
|
27
|
+
tags?: string[];
|
|
28
|
+
summary?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
deprecated?: boolean;
|
|
31
|
+
hide?: boolean;
|
|
32
|
+
parameters?: RouteParameter[];
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
export interface RouteConfig {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
path: string;
|
|
37
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
38
|
+
handler: RouteHandler;
|
|
39
|
+
ttl: number;
|
|
40
|
+
before: BeforeMiddleware[];
|
|
41
|
+
detail?: RouteDetail;
|
|
16
42
|
}
|
|
43
|
+
|
|
17
44
|
export declare const routeRegistry: Map<string, RouteConfig>;
|
|
18
45
|
export declare const keepAliveSites: Set<string>;
|
|
19
|
-
|
|
20
|
-
export declare function
|
|
21
|
-
|
|
46
|
+
|
|
47
|
+
export declare function startServer(
|
|
48
|
+
port: number,
|
|
49
|
+
hostname?: string,
|
|
50
|
+
openapiOpts?: {
|
|
51
|
+
title?: string;
|
|
52
|
+
version?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
path?: string;
|
|
55
|
+
}
|
|
56
|
+
): Promise<Elysia>;
|
|
57
|
+
|
|
58
|
+
export declare function stopServer(): void;
|
package/piggy/server/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// piggy/server/index.ts
|
|
2
2
|
import { Elysia } from "elysia";
|
|
3
|
+
import { openapi } from "@elysiajs/openapi";
|
|
3
4
|
import * as cache from "../cache/memory";
|
|
4
5
|
import logger from "../logger";
|
|
5
6
|
|
|
@@ -17,12 +18,30 @@ export type RouteHandler = (
|
|
|
17
18
|
body: any
|
|
18
19
|
) => Promise<any>;
|
|
19
20
|
|
|
21
|
+
export interface RouteParameter {
|
|
22
|
+
name: string;
|
|
23
|
+
in: "query" | "path" | "header" | "cookie";
|
|
24
|
+
description?: string;
|
|
25
|
+
required?: boolean;
|
|
26
|
+
schema?: Record<string, any>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RouteDetail {
|
|
30
|
+
tags?: string[];
|
|
31
|
+
summary?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
deprecated?: boolean;
|
|
34
|
+
hide?: boolean;
|
|
35
|
+
parameters?: RouteParameter[];
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
export interface RouteConfig {
|
|
21
39
|
path: string;
|
|
22
40
|
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
23
41
|
handler: RouteHandler;
|
|
24
42
|
ttl: number;
|
|
25
43
|
before: BeforeMiddleware[];
|
|
44
|
+
detail?: RouteDetail;
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
export const routeRegistry = new Map<string, RouteConfig>();
|
|
@@ -45,28 +64,56 @@ function mapError(err: Error, site: string) {
|
|
|
45
64
|
|
|
46
65
|
let _app: Elysia | null = null;
|
|
47
66
|
|
|
48
|
-
export async function startServer(
|
|
67
|
+
export async function startServer(
|
|
68
|
+
port: number,
|
|
69
|
+
hostname = "0.0.0.0",
|
|
70
|
+
openapiOpts?: {
|
|
71
|
+
title?: string;
|
|
72
|
+
version?: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
path?: string;
|
|
75
|
+
}
|
|
76
|
+
): Promise<Elysia> {
|
|
49
77
|
_app = new Elysia();
|
|
50
78
|
|
|
51
|
-
// ──
|
|
79
|
+
// ── OpenAPI ───────────────────────────────────────────────────────────────
|
|
80
|
+
_app.use(
|
|
81
|
+
openapi({
|
|
82
|
+
path: openapiOpts?.path ?? "/openapi",
|
|
83
|
+
documentation: {
|
|
84
|
+
info: {
|
|
85
|
+
title: openapiOpts?.title ?? "Piggy API",
|
|
86
|
+
version: openapiOpts?.version ?? "1.0.0",
|
|
87
|
+
description: openapiOpts?.description ?? "Auto-generated docs for all registered piggy routes",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// ── Health ────────────────────────────────────────────────────────────────
|
|
52
94
|
_app.get("/health", () => ({
|
|
53
95
|
status: "ok",
|
|
54
96
|
routes: routeRegistry.size,
|
|
55
97
|
cacheEntries: cache.size(),
|
|
56
98
|
uptime: process.uptime(),
|
|
57
|
-
})
|
|
99
|
+
}), {
|
|
100
|
+
detail: { tags: ["_piggy"], summary: "Health check" },
|
|
101
|
+
});
|
|
58
102
|
|
|
59
|
-
// ── Cache
|
|
103
|
+
// ── Cache ─────────────────────────────────────────────────────────────────
|
|
60
104
|
_app.delete("/cache", () => {
|
|
61
105
|
cache.clear();
|
|
62
106
|
return { cleared: true };
|
|
107
|
+
}, {
|
|
108
|
+
detail: { tags: ["_piggy"], summary: "Clear all cache entries" },
|
|
63
109
|
});
|
|
64
110
|
|
|
65
|
-
_app.get("/cache/keys", () => ({ keys: cache.keys() })
|
|
111
|
+
_app.get("/cache/keys", () => ({ keys: cache.keys() }), {
|
|
112
|
+
detail: { tags: ["_piggy"], summary: "List all cache keys" },
|
|
113
|
+
});
|
|
66
114
|
|
|
67
|
-
// ── Registered site routes
|
|
115
|
+
// ── Registered site routes ────────────────────────────────────────────────
|
|
68
116
|
for (const [registryKey, config] of routeRegistry.entries()) {
|
|
69
|
-
// registryKey format is "siteName:path" e.g. "movie:/title"
|
|
70
117
|
const colonIdx = registryKey.indexOf(":");
|
|
71
118
|
const siteName = registryKey.substring(0, colonIdx);
|
|
72
119
|
const fullPath = `/${siteName}${config.path}`;
|
|
@@ -75,7 +122,6 @@ export async function startServer(port: number, hostname = "0.0.0.0"): Promise<E
|
|
|
75
122
|
logger.info(`[server] mounting ${config.method} ${fullPath} (ttl=${config.ttl}ms)`);
|
|
76
123
|
|
|
77
124
|
const routeHandler = async ({ params, query, body, headers, set }: any) => {
|
|
78
|
-
// 1. Run before middleware
|
|
79
125
|
for (const mw of config.before) {
|
|
80
126
|
try {
|
|
81
127
|
await mw({ params, query, body, headers, set });
|
|
@@ -85,7 +131,6 @@ export async function startServer(port: number, hostname = "0.0.0.0"): Promise<E
|
|
|
85
131
|
}
|
|
86
132
|
}
|
|
87
133
|
|
|
88
|
-
// 2. Cache check
|
|
89
134
|
const cacheKey = `${siteName}:${fullPath}:${JSON.stringify({ params, query })}`;
|
|
90
135
|
const hit = cache.get(cacheKey);
|
|
91
136
|
if (hit !== null) {
|
|
@@ -94,7 +139,6 @@ export async function startServer(port: number, hostname = "0.0.0.0"): Promise<E
|
|
|
94
139
|
return hit;
|
|
95
140
|
}
|
|
96
141
|
|
|
97
|
-
// 3. Execute handler
|
|
98
142
|
let result: any;
|
|
99
143
|
try {
|
|
100
144
|
result = await config.handler(params, query, body);
|
|
@@ -104,7 +148,6 @@ export async function startServer(port: number, hostname = "0.0.0.0"): Promise<E
|
|
|
104
148
|
return mapped;
|
|
105
149
|
}
|
|
106
150
|
|
|
107
|
-
// 4. Store in cache
|
|
108
151
|
if (config.ttl > 0) {
|
|
109
152
|
cache.set(cacheKey, result, config.ttl);
|
|
110
153
|
set.headers["x-cache"] = "MISS";
|
|
@@ -113,15 +156,25 @@ export async function startServer(port: number, hostname = "0.0.0.0"): Promise<E
|
|
|
113
156
|
return result;
|
|
114
157
|
};
|
|
115
158
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
159
|
+
const routeDetail = {
|
|
160
|
+
tags: config.detail?.tags ?? [siteName],
|
|
161
|
+
summary: config.detail?.summary ?? `${config.method} ${fullPath}`,
|
|
162
|
+
description: config.detail?.description,
|
|
163
|
+
deprecated: config.detail?.deprecated ?? false,
|
|
164
|
+
hide: config.detail?.hide ?? false,
|
|
165
|
+
parameters: config.detail?.parameters ?? [],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (method === "get") _app.get(fullPath, routeHandler, { detail: routeDetail });
|
|
169
|
+
else if (method === "post") _app.post(fullPath, routeHandler, { detail: routeDetail });
|
|
170
|
+
else if (method === "put") _app.put(fullPath, routeHandler, { detail: routeDetail });
|
|
171
|
+
else if (method === "delete") _app.delete(fullPath, routeHandler, { detail: routeDetail });
|
|
120
172
|
}
|
|
121
173
|
|
|
122
174
|
_app.listen({ port, hostname });
|
|
123
175
|
|
|
124
176
|
logger.success(`🚀 Piggy API server → http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${port}`);
|
|
177
|
+
logger.success(`📖 OpenAPI docs → http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${port}/openapi`);
|
|
125
178
|
logger.info(` Routes mounted: ${routeRegistry.size}`);
|
|
126
179
|
routeRegistry.forEach((cfg, key) => {
|
|
127
180
|
const siteName = key.substring(0, key.indexOf(":"));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// piggy/store/index.d.ts
|
|
2
|
+
export type FieldType = "string" | "number" | "boolean" | "object" | "array";
|
|
3
|
+
|
|
4
|
+
export interface FieldSchema {
|
|
5
|
+
type: FieldType;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
default?: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface StoreSchema {
|
|
11
|
+
name: string;
|
|
12
|
+
destination: string;
|
|
13
|
+
fields: Record<string, FieldSchema>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PiggyStoreConfig {
|
|
17
|
+
stores: StoreSchema[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export declare function loadStoreConfig(configPath?: string): PiggyStoreConfig;
|
|
21
|
+
export declare function getSchema(storeName: string): StoreSchema | null;
|
|
22
|
+
export declare function shapeRecord(data: Record<string, any>, schema: StoreSchema): Record<string, any>;
|
|
23
|
+
export declare function storeRecord(
|
|
24
|
+
storeName: string,
|
|
25
|
+
data: Record<string, any> | Record<string, any>[]
|
|
26
|
+
): Promise<{ stored: number; skipped: number }>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// piggy/store/index.ts
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
import logger from "../logger";
|
|
5
|
+
|
|
6
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export type FieldType = "string" | "number" | "boolean" | "object" | "array";
|
|
9
|
+
|
|
10
|
+
export interface FieldSchema {
|
|
11
|
+
type: FieldType;
|
|
12
|
+
required?: boolean; // default false — missing = NULL not error
|
|
13
|
+
default?: any; // fallback if missing (overrides NULL)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StoreSchema {
|
|
17
|
+
name: string; // store identifier (matches site name or custom)
|
|
18
|
+
destination: string; // "./data.json" or "./data.db"
|
|
19
|
+
fields: Record<string, FieldSchema>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PiggyStoreConfig {
|
|
23
|
+
stores: StoreSchema[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Load piggy.store.json ─────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
let _config: PiggyStoreConfig | null = null;
|
|
29
|
+
|
|
30
|
+
export function loadStoreConfig(configPath = "./piggy.store.json"): PiggyStoreConfig {
|
|
31
|
+
if (_config) return _config;
|
|
32
|
+
|
|
33
|
+
const abs = resolve(configPath);
|
|
34
|
+
if (!existsSync(abs)) {
|
|
35
|
+
logger.warn(`[store] piggy.store.json not found at ${abs} — store() calls will no-op`);
|
|
36
|
+
return { stores: [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
_config = JSON.parse(readFileSync(abs, "utf8")) as PiggyStoreConfig;
|
|
41
|
+
logger.success(`[store] loaded ${_config.stores.length} schema(s) from ${abs}`);
|
|
42
|
+
return _config;
|
|
43
|
+
} catch (e: any) {
|
|
44
|
+
logger.error(`[store] failed to parse piggy.store.json: ${e.message}`);
|
|
45
|
+
return { stores: [] };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getSchema(storeName: string): StoreSchema | null {
|
|
50
|
+
const config = loadStoreConfig();
|
|
51
|
+
return config.stores.find(s => s.name === storeName) ?? null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Validate & shape one record against schema ────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export function shapeRecord(data: Record<string, any>, schema: StoreSchema): Record<string, any> {
|
|
57
|
+
const shaped: Record<string, any> = {};
|
|
58
|
+
|
|
59
|
+
for (const [field, def] of Object.entries(schema.fields)) {
|
|
60
|
+
const raw = data[field];
|
|
61
|
+
|
|
62
|
+
// Missing field
|
|
63
|
+
if (raw === undefined || raw === null) {
|
|
64
|
+
if (def.default !== undefined) {
|
|
65
|
+
shaped[field] = def.default;
|
|
66
|
+
} else {
|
|
67
|
+
shaped[field] = null; // NULL — not an error, just absent
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Type coercion / validation
|
|
73
|
+
switch (def.type) {
|
|
74
|
+
case "string":
|
|
75
|
+
shaped[field] = String(raw);
|
|
76
|
+
break;
|
|
77
|
+
case "number":
|
|
78
|
+
const n = Number(raw);
|
|
79
|
+
shaped[field] = isNaN(n) ? null : n;
|
|
80
|
+
break;
|
|
81
|
+
case "boolean":
|
|
82
|
+
shaped[field] = Boolean(raw);
|
|
83
|
+
break;
|
|
84
|
+
case "object":
|
|
85
|
+
shaped[field] = typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
86
|
+
break;
|
|
87
|
+
case "array":
|
|
88
|
+
shaped[field] = Array.isArray(raw) ? raw : null;
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
shaped[field] = raw;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Extra fields on incoming data are silently dropped — not in schema = ignored
|
|
96
|
+
return shaped;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── JSON backend ──────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
function appendToJson(destination: string, record: Record<string, any>): void {
|
|
102
|
+
const abs = resolve(destination);
|
|
103
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
104
|
+
|
|
105
|
+
let existing: any[] = [];
|
|
106
|
+
if (existsSync(abs)) {
|
|
107
|
+
try {
|
|
108
|
+
existing = JSON.parse(readFileSync(abs, "utf8"));
|
|
109
|
+
if (!Array.isArray(existing)) existing = [existing];
|
|
110
|
+
} catch {
|
|
111
|
+
existing = [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
existing.push({ ...record, _storedAt: new Date().toISOString() });
|
|
116
|
+
writeFileSync(abs, JSON.stringify(existing, null, 2), "utf8");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── SQLite backend ────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
function appendToSqlite(destination: string, record: Record<string, any>, schema: StoreSchema): void {
|
|
122
|
+
// Use bun:sqlite if available, else require better-sqlite3
|
|
123
|
+
const abs = resolve(destination);
|
|
124
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
125
|
+
|
|
126
|
+
const isBun = typeof (globalThis as any).Bun !== "undefined";
|
|
127
|
+
|
|
128
|
+
if (isBun) {
|
|
129
|
+
const { Database } = require("bun:sqlite") as typeof import("bun:sqlite");
|
|
130
|
+
const db = new Database(abs);
|
|
131
|
+
ensureTable(db, schema, "bun");
|
|
132
|
+
insertRecord(db, schema, record, "bun");
|
|
133
|
+
db.close();
|
|
134
|
+
} else {
|
|
135
|
+
const Database = require("better-sqlite3");
|
|
136
|
+
const db = new Database(abs);
|
|
137
|
+
ensureTable(db, schema, "node");
|
|
138
|
+
insertRecord(db, schema, record, "node");
|
|
139
|
+
db.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ensureTable(db: any, schema: StoreSchema, runtime: "bun" | "node"): void {
|
|
144
|
+
const tableName = schema.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
145
|
+
const cols = Object.entries(schema.fields).map(([name, def]) => {
|
|
146
|
+
const sqlType = def.type === "number" ? "REAL"
|
|
147
|
+
: def.type === "boolean" ? "INTEGER"
|
|
148
|
+
: def.type === "object" || def.type === "array" ? "TEXT" // JSON stringified
|
|
149
|
+
: "TEXT";
|
|
150
|
+
return ` ${name} ${sqlType}`;
|
|
151
|
+
});
|
|
152
|
+
cols.push(" _storedAt TEXT");
|
|
153
|
+
|
|
154
|
+
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n${cols.join(",\n")}\n)`;
|
|
155
|
+
|
|
156
|
+
if (runtime === "bun") {
|
|
157
|
+
db.run(sql);
|
|
158
|
+
} else {
|
|
159
|
+
db.prepare(sql).run();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function insertRecord(db: any, schema: StoreSchema, record: Record<string, any>, runtime: "bun" | "node"): void {
|
|
164
|
+
const tableName = schema.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
165
|
+
const fields = [...Object.keys(schema.fields), "_storedAt"];
|
|
166
|
+
const values = fields.map(f => {
|
|
167
|
+
if (f === "_storedAt") return new Date().toISOString();
|
|
168
|
+
const v = record[f];
|
|
169
|
+
// Serialize objects/arrays as JSON strings for SQLite
|
|
170
|
+
if (v !== null && (typeof v === "object" || Array.isArray(v))) return JSON.stringify(v);
|
|
171
|
+
return v ?? null;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const placeholders = runtime === "bun"
|
|
175
|
+
? fields.map((_, i) => `?${i + 1}`).join(", ")
|
|
176
|
+
: fields.map(() => "?").join(", ");
|
|
177
|
+
|
|
178
|
+
const sql = `INSERT INTO ${tableName} (${fields.join(", ")}) VALUES (${placeholders})`;
|
|
179
|
+
|
|
180
|
+
if (runtime === "bun") {
|
|
181
|
+
db.run(sql, values);
|
|
182
|
+
} else {
|
|
183
|
+
db.prepare(sql).run(values);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Main store function ───────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export async function storeRecord(
|
|
190
|
+
storeName: string,
|
|
191
|
+
data: Record<string, any> | Record<string, any>[]
|
|
192
|
+
): Promise<{ stored: number; skipped: number }> {
|
|
193
|
+
const schema = getSchema(storeName);
|
|
194
|
+
|
|
195
|
+
if (!schema) {
|
|
196
|
+
logger.warn(`[store] no schema found for "${storeName}" in piggy.store.json — data not stored`);
|
|
197
|
+
return { stored: 0, skipped: 1 };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const records = Array.isArray(data) ? data : [data];
|
|
201
|
+
let stored = 0;
|
|
202
|
+
let skipped = 0;
|
|
203
|
+
|
|
204
|
+
const isJson = schema.destination.endsWith(".json");
|
|
205
|
+
const isSqlite = schema.destination.endsWith(".db") || schema.destination.endsWith(".sqlite");
|
|
206
|
+
|
|
207
|
+
for (const record of records) {
|
|
208
|
+
try {
|
|
209
|
+
const shaped = shapeRecord(record, schema);
|
|
210
|
+
|
|
211
|
+
if (isJson) {
|
|
212
|
+
appendToJson(schema.destination, shaped);
|
|
213
|
+
} else if (isSqlite) {
|
|
214
|
+
appendToSqlite(schema.destination, shaped, schema);
|
|
215
|
+
} else {
|
|
216
|
+
logger.error(`[store] unsupported destination format: ${schema.destination} (use .json or .db)`);
|
|
217
|
+
skipped++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
stored++;
|
|
222
|
+
logger.success(`[store][${storeName}] stored record → ${schema.destination}`);
|
|
223
|
+
} catch (e: any) {
|
|
224
|
+
logger.error(`[store][${storeName}] failed to store record: ${e.message}`);
|
|
225
|
+
skipped++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { stored, skipped };
|
|
230
|
+
}
|