trickle-backend 0.1.64 → 0.1.65

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/Dockerfile ADDED
@@ -0,0 +1,23 @@
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY package.json package-lock.json* ./
7
+ RUN npm ci --production 2>/dev/null || npm install --production
8
+
9
+ # Copy built files
10
+ COPY dist/ dist/
11
+
12
+ # Create data directory
13
+ RUN mkdir -p /data
14
+
15
+ ENV PORT=4888
16
+ ENV TRICKLE_DB_PATH=/data/trickle.db
17
+
18
+ EXPOSE 4888
19
+
20
+ HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
21
+ CMD wget -qO- http://localhost:4888/api/health || exit 1
22
+
23
+ CMD ["node", "dist/index.js"]
@@ -7,9 +7,9 @@ exports.db = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
10
- const trickleDir = path_1.default.join(process.env.HOME || "~", ".trickle");
10
+ const dbPath = process.env.TRICKLE_DB_PATH || path_1.default.join(process.env.HOME || "~", ".trickle", "trickle.db");
11
+ const trickleDir = path_1.default.dirname(dbPath);
11
12
  fs_1.default.mkdirSync(trickleDir, { recursive: true });
12
- const dbPath = path_1.default.join(trickleDir, "trickle.db");
13
13
  const db = new better_sqlite3_1.default(dbPath);
14
14
  exports.db = db;
15
15
  db.pragma("journal_mode = WAL");
@@ -70,7 +70,38 @@ router.post("/keys", (req, res) => {
70
70
  message: "Save this key — it cannot be retrieved later.",
71
71
  });
72
72
  });
73
- // ── POST /api/v1/pushUpload project data ──
73
+ // ── POST /api/v1/ingestReal-time streaming ingest ──
74
+ // Accepts batched observations and appends to project data files.
75
+ // This enables `trickle run` to stream data to the cloud in real-time.
76
+ router.post("/ingest", requireAuth, (req, res) => {
77
+ const { project, file, lines } = req.body;
78
+ if (!project || !file || !lines) {
79
+ res.status(400).json({ error: "project, file, and lines required" });
80
+ return;
81
+ }
82
+ const projectId = `${req.keyId}:${project}`;
83
+ // Auto-create project
84
+ connection_1.db.prepare(`
85
+ INSERT INTO projects (id, name, owner_key_id, updated_at)
86
+ VALUES (?, ?, ?, datetime('now'))
87
+ ON CONFLICT(id) DO UPDATE SET updated_at = datetime('now')
88
+ `).run(projectId, project, req.keyId);
89
+ // Append to existing content (or create new)
90
+ const existing = connection_1.db.prepare("SELECT content FROM project_data WHERE project_id = ? AND filename = ?").get(projectId, file);
91
+ const newContent = typeof lines === "string" ? lines : lines.join("\n") + "\n";
92
+ const content = existing ? existing.content + newContent : newContent;
93
+ const bytes = Buffer.byteLength(content, "utf-8");
94
+ connection_1.db.prepare(`
95
+ INSERT INTO project_data (project_id, filename, content, size_bytes, pushed_at)
96
+ VALUES (?, ?, ?, ?, datetime('now'))
97
+ ON CONFLICT(project_id, filename) DO UPDATE SET
98
+ content = excluded.content,
99
+ size_bytes = excluded.size_bytes,
100
+ pushed_at = datetime('now')
101
+ `).run(projectId, file, content, bytes);
102
+ res.json({ ok: true, file, bytes });
103
+ });
104
+ // ── POST /api/v1/push — Upload project data (full replace) ──
74
105
  router.post("/push", requireAuth, (req, res) => {
75
106
  const { project, files, timestamp } = req.body;
76
107
  if (!project || typeof project !== "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-backend",
3
- "version": "0.1.64",
3
+ "version": "0.1.65",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "build": "tsc",
@@ -2,12 +2,11 @@ import path from "path";
2
2
  import fs from "fs";
3
3
  import Database, { Database as DatabaseType } from "better-sqlite3";
4
4
 
5
- const trickleDir = path.join(process.env.HOME || "~", ".trickle");
5
+ const dbPath = process.env.TRICKLE_DB_PATH || path.join(process.env.HOME || "~", ".trickle", "trickle.db");
6
+ const trickleDir = path.dirname(dbPath);
6
7
 
7
8
  fs.mkdirSync(trickleDir, { recursive: true });
8
9
 
9
- const dbPath = path.join(trickleDir, "trickle.db");
10
-
11
10
  const db: DatabaseType = new Database(dbPath);
12
11
 
13
12
  db.pragma("journal_mode = WAL");
@@ -94,7 +94,49 @@ router.post("/keys", (req: Request, res: Response) => {
94
94
  });
95
95
  });
96
96
 
97
- // ── POST /api/v1/pushUpload project data ──
97
+ // ── POST /api/v1/ingestReal-time streaming ingest ──
98
+ // Accepts batched observations and appends to project data files.
99
+ // This enables `trickle run` to stream data to the cloud in real-time.
100
+
101
+ router.post("/ingest", requireAuth, (req: AuthedRequest, res: Response) => {
102
+ const { project, file, lines } = req.body;
103
+
104
+ if (!project || !file || !lines) {
105
+ res.status(400).json({ error: "project, file, and lines required" });
106
+ return;
107
+ }
108
+
109
+ const projectId = `${req.keyId}:${project}`;
110
+
111
+ // Auto-create project
112
+ db.prepare(`
113
+ INSERT INTO projects (id, name, owner_key_id, updated_at)
114
+ VALUES (?, ?, ?, datetime('now'))
115
+ ON CONFLICT(id) DO UPDATE SET updated_at = datetime('now')
116
+ `).run(projectId, project, req.keyId);
117
+
118
+ // Append to existing content (or create new)
119
+ const existing = db.prepare(
120
+ "SELECT content FROM project_data WHERE project_id = ? AND filename = ?"
121
+ ).get(projectId, file) as any;
122
+
123
+ const newContent = typeof lines === "string" ? lines : (lines as string[]).join("\n") + "\n";
124
+ const content = existing ? existing.content + newContent : newContent;
125
+ const bytes = Buffer.byteLength(content, "utf-8");
126
+
127
+ db.prepare(`
128
+ INSERT INTO project_data (project_id, filename, content, size_bytes, pushed_at)
129
+ VALUES (?, ?, ?, ?, datetime('now'))
130
+ ON CONFLICT(project_id, filename) DO UPDATE SET
131
+ content = excluded.content,
132
+ size_bytes = excluded.size_bytes,
133
+ pushed_at = datetime('now')
134
+ `).run(projectId, file, content, bytes);
135
+
136
+ res.json({ ok: true, file, bytes });
137
+ });
138
+
139
+ // ── POST /api/v1/push — Upload project data (full replace) ──
98
140
 
99
141
  router.post("/push", requireAuth, (req: AuthedRequest, res: Response) => {
100
142
  const { project, files, timestamp } = req.body;