@cloud-cli/d0 1.0.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/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # d0
2
+
3
+ SQLite server over HTTP
4
+
5
+ ## Usage
6
+
7
+ **POST /query**
8
+
9
+ Run a prepared SQLite statement.
10
+ Send a JSON with two properties: `s` for statement and `d` for data (optional)
11
+
12
+ ```js
13
+ // using fetch
14
+ fetch('https://db.example.com/query', {
15
+ body: JSON.stringify({
16
+ s: 'SELECT * FROM user WHERE id = ?',
17
+ d: [123]
18
+ });
19
+ });
20
+
21
+ // using the server-provided library
22
+ import db from 'https://db.example.com/index.mjs';
23
+
24
+ const user = await db.query('SELECT * FROM user WHERE id = ?', [123]);
25
+ ```
26
+
27
+ ## Environment variables
28
+
29
+ | Variable | Description |
30
+ |-|-|
31
+ | PORT | HTTP port |
32
+ | SQLITE_DB_PATH | Path to file where the database files are stored |
package/client.mjs ADDED
@@ -0,0 +1,18 @@
1
+ const baseURL = 'https://__API_URL__';
2
+
3
+ export default {
4
+ async query(statement, data) {
5
+ const req = await fetch(new URL('/query', baseURL), {
6
+ body: JSON.stringify({
7
+ s: statement,
8
+ d: data || null,
9
+ }),
10
+ });
11
+
12
+ if (req.ok) {
13
+ return await req.json();
14
+ }
15
+
16
+ throw new Error(await req.text());
17
+ },
18
+ };
@@ -0,0 +1,5 @@
1
+ /// <reference types="node" />
2
+ import { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { Database } from 'better-sqlite3';
4
+ export declare function getDatabase(): Database;
5
+ export declare function serve(): import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFile } from 'node:fs/promises';
3
+ import SQLite from 'better-sqlite3';
4
+ export function getDatabase() {
5
+ const db = new SQLite(process.env.SQLITE_DB_PATH || './db.sqlite3');
6
+ db.pragma('journal_mode = WAL');
7
+ return db;
8
+ }
9
+ export function serve() {
10
+ const db = getDatabase();
11
+ const server = createServer((request, response) => {
12
+ const url = new URL(request.url, 'http://localhost');
13
+ const route = `${request.method} ${url.pathname}`.trim();
14
+ const args = Object.fromEntries(url.searchParams.entries());
15
+ const q = { db, request, response, args };
16
+ switch (route) {
17
+ case 'GET /index.mjs':
18
+ return onEsModule(q);
19
+ case 'POST /query':
20
+ return onQuery(q);
21
+ default:
22
+ response.writeHead(404).end();
23
+ }
24
+ });
25
+ server.listen(process.env.PORT);
26
+ return server;
27
+ }
28
+ async function onQuery({ db, request, response }) {
29
+ const query = await readStream(request);
30
+ if (!query.length) {
31
+ response.writeHead(400).end();
32
+ return;
33
+ }
34
+ try {
35
+ const { s, d } = JSON.parse(query);
36
+ const result = db.prepare(s).run(d);
37
+ response.end(JSON.stringify(result));
38
+ }
39
+ catch (error) {
40
+ process.env.DEBUG && console.error(error);
41
+ response.writeHead(400).end(String(error));
42
+ }
43
+ }
44
+ let file;
45
+ async function getFile() {
46
+ if (!file) {
47
+ file = await readFile('./client.mjs', 'utf8');
48
+ }
49
+ return file;
50
+ }
51
+ async function onEsModule({ request, response }) {
52
+ const hostname = request.headers['x-forwarded-for'];
53
+ const code = await getFile();
54
+ response
55
+ .writeHead(200, {
56
+ 'Content-Type': 'text/javascript',
57
+ 'Access-Control-Allow-Origin': '*',
58
+ })
59
+ .end(code.replace('__API_URL__', hostname));
60
+ }
61
+ function readStream(stream) {
62
+ return new Promise((resolve, reject) => {
63
+ const all = [];
64
+ stream.on('data', (c) => all.push(c));
65
+ stream.on('end', () => resolve(Buffer.concat(all).toString('utf8')));
66
+ stream.on('error', reject);
67
+ });
68
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/serve.js ADDED
@@ -0,0 +1,2 @@
1
+ import { serve } from './index.js';
2
+ serve();
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@cloud-cli/d0",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "echo true",
10
+ "start": "node dist/serve.js"
11
+ },
12
+ "prettier": "@cloud-cli/prettier-config",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/cloud-cli/d0.git"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "dependencies": {
21
+ "better-sqlite3": "^8.2.0"
22
+ },
23
+ "devDependencies": {
24
+ "@cloud-cli/prettier-config": "^1.0.0",
25
+ "@cloud-cli/typescript-config": "^1.0.0",
26
+ "@types/better-sqlite3": "^7.6.3",
27
+ "@types/node": "^20.3.3",
28
+ "typescript": "^5.4.3"
29
+ }
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { createServer, IncomingMessage, ServerResponse } from 'node:http';
2
+ import { readFile } from 'node:fs/promises';
3
+ import SQLite, { Database } from 'better-sqlite3';
4
+
5
+ type Q = {
6
+ db: Database;
7
+ request: IncomingMessage;
8
+ response: ServerResponse;
9
+ args: Record<string, string>;
10
+ };
11
+
12
+ export function getDatabase(): Database {
13
+ const db = new SQLite(process.env.SQLITE_DB_PATH || './db.sqlite3');
14
+ db.pragma('journal_mode = WAL');
15
+ return db;
16
+ }
17
+
18
+ export function serve() {
19
+ const db = getDatabase();
20
+
21
+ const server = createServer((request, response) => {
22
+ const url = new URL(request.url, 'http://localhost');
23
+ const route = `${request.method} ${url.pathname}`.trim();
24
+ const args = Object.fromEntries(url.searchParams.entries()) as Q['args'];
25
+ const q: Q = { db, request, response, args };
26
+
27
+ switch (route) {
28
+ case 'GET /index.mjs':
29
+ return onEsModule(q);
30
+
31
+ case 'POST /query':
32
+ return onQuery(q);
33
+
34
+ default:
35
+ response.writeHead(404).end();
36
+ }
37
+ });
38
+
39
+ server.listen(process.env.PORT);
40
+
41
+ return server;
42
+ }
43
+
44
+ async function onQuery({ db, request, response }: Q) {
45
+ const query = await readStream(request);
46
+
47
+ if (!query.length) {
48
+ response.writeHead(400).end();
49
+ return;
50
+ }
51
+
52
+ try {
53
+ const { s, d } = JSON.parse(query);
54
+ const result = db.prepare(s).run(d);
55
+ response.end(JSON.stringify(result));
56
+ } catch (error) {
57
+ process.env.DEBUG && console.error(error);
58
+ response.writeHead(400).end(String(error));
59
+ }
60
+ }
61
+
62
+ let file;
63
+ async function getFile() {
64
+ if (!file) {
65
+ file = await readFile('./client.mjs', 'utf8');
66
+ }
67
+
68
+ return file;
69
+ }
70
+
71
+ async function onEsModule({ request, response }: Q) {
72
+ const hostname = request.headers['x-forwarded-for'];
73
+ const code = await getFile();
74
+
75
+ response
76
+ .writeHead(200, {
77
+ 'Content-Type': 'text/javascript',
78
+ 'Access-Control-Allow-Origin': '*',
79
+ })
80
+ .end(code.replace('__API_URL__', hostname));
81
+ }
82
+
83
+ function readStream(stream): Promise<string> {
84
+ return new Promise((resolve, reject) => {
85
+ const all = [];
86
+ stream.on('data', (c) => all.push(c));
87
+ stream.on('end', () => resolve(Buffer.concat(all).toString('utf8')));
88
+ stream.on('error', reject);
89
+ });
90
+ }
package/src/serve.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { serve } from './index.js';
2
+ serve();