@cloud-cli/d0 1.3.1 → 1.4.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/.dockerignore +4 -1
- package/Dockerfile +9 -3
- package/README.md +2 -0
- package/client.mjs +13 -12
- package/console.html +118 -0
- package/dist/index.d.ts +3 -10
- package/dist/index.js +40 -37
- package/package.json +7 -7
- package/pnpm-workspace.yaml +2 -0
package/.dockerignore
CHANGED
package/Dockerfile
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
FROM ghcr.io/cloud-cli/node:latest
|
|
1
|
+
FROM ghcr.io/cloud-cli/node:latest AS builder
|
|
2
2
|
|
|
3
3
|
USER root
|
|
4
4
|
WORKDIR /home/app
|
|
5
5
|
COPY . /home/app
|
|
6
|
-
|
|
6
|
+
ENV CI=true
|
|
7
|
+
RUN pnpm i && pnpm build && rm -rf node_modules/ src/ && pnpm store prune
|
|
8
|
+
|
|
9
|
+
FROM ghcr.io/cloud-cli/node:latest
|
|
10
|
+
|
|
7
11
|
ENV NODE_ENV=production
|
|
8
|
-
|
|
12
|
+
WORKDIR /home/app
|
|
13
|
+
COPY --from=builder /home/app/ ./
|
|
14
|
+
RUN pnpm install --prod
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Accepts a JSON with these properties:
|
|
|
14
14
|
|`s` | string with the statement | **yes** |
|
|
15
15
|
|`d` | data to bind on a statement | no |
|
|
16
16
|
|`m` | method to execute: `all`, `run` or `get`. Run is the default | no |
|
|
17
|
+
|`p` | pragma statements as an array of strings. They run before the query | no |
|
|
17
18
|
|
|
18
19
|
```js
|
|
19
20
|
// select all items using fetch
|
|
@@ -23,6 +24,7 @@ fetch('https://db.example.com/query', {
|
|
|
23
24
|
s: 'SELECT * FROM user WHERE id = ?',
|
|
24
25
|
d: [123],
|
|
25
26
|
m: 'all',
|
|
27
|
+
p: ['foreign_keys = ON']
|
|
26
28
|
});
|
|
27
29
|
});
|
|
28
30
|
|
package/client.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
const baseURL = "https://__API_URL__";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
let pragmas = [];
|
|
4
|
+
|
|
5
|
+
async function query(method, statement, data, pragma = pragmas) {
|
|
4
6
|
const req = await fetch(new URL("/query", baseURL), {
|
|
5
7
|
method: "POST",
|
|
6
8
|
body: JSON.stringify({
|
|
7
9
|
s: statement,
|
|
8
|
-
d: data
|
|
10
|
+
d: data,
|
|
9
11
|
m: method,
|
|
12
|
+
p: pragma
|
|
10
13
|
}),
|
|
11
14
|
});
|
|
12
15
|
|
|
@@ -17,16 +20,14 @@ export async function query(statement, data, method = "run") {
|
|
|
17
20
|
throw new Error(await req.text());
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
export const get = query.bind(null, 'get');
|
|
24
|
+
export const run = query.bind(null, 'run');
|
|
25
|
+
export const all = query.bind(null, 'all');
|
|
23
26
|
|
|
24
|
-
export function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export function all(statement, data) {
|
|
29
|
-
return query(statement, data, "all");
|
|
27
|
+
export function pragma(p) {
|
|
28
|
+
if (Array.isArray(p) && p.every(s => typeof s === 'string')) {
|
|
29
|
+
pragmas = p;
|
|
30
|
+
}
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
export default { query, get, run, all };
|
|
33
|
+
export default { query, get, run, all, pragma };
|
package/console.html
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link
|
|
7
|
+
rel="component"
|
|
8
|
+
href="https://sodium.static.apphor.de/code-editor.html"
|
|
9
|
+
/>
|
|
10
|
+
<link
|
|
11
|
+
rel="component"
|
|
12
|
+
href="https://sodium.static.apphor.de/code-block.html"
|
|
13
|
+
/>
|
|
14
|
+
<title></title>
|
|
15
|
+
<script type="importmap">
|
|
16
|
+
{
|
|
17
|
+
"imports": {
|
|
18
|
+
"@li3/": "https://at-li3.static.apphor.de/",
|
|
19
|
+
"@sodium/": "https://at-sodium.static.apphor.de/",
|
|
20
|
+
"@app/": "./"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
<link
|
|
25
|
+
rel="stylesheet"
|
|
26
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
|
|
27
|
+
/>
|
|
28
|
+
</head>
|
|
29
|
+
<body
|
|
30
|
+
class="bg-gray-900 text-white font-sans h-screen flex flex-col overflow-hidden"
|
|
31
|
+
>
|
|
32
|
+
<app-main class="contents"></app-main>
|
|
33
|
+
|
|
34
|
+
<script type="module">
|
|
35
|
+
import "@li3/web";
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template component="app-main">
|
|
39
|
+
<div
|
|
40
|
+
class="flex flex-col items-stretch justify-between space-y-2 p-4 h-full"
|
|
41
|
+
>
|
|
42
|
+
<div class="p-2 overflow-auto font-mono text-xs h-full">
|
|
43
|
+
<template for="r of responses">
|
|
44
|
+
<code-block
|
|
45
|
+
bind-source="r || ''"
|
|
46
|
+
language="json"
|
|
47
|
+
class="py-1 border-b"
|
|
48
|
+
></code-block>
|
|
49
|
+
</template>
|
|
50
|
+
<div class="text-red-400 text-sm">{{ error || '' }}</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="flex-1">
|
|
53
|
+
<input
|
|
54
|
+
bind-value="name"
|
|
55
|
+
placeholder="Name"
|
|
56
|
+
class="block w-full p-2 rounded-md bg-gray-300 border text-black text-xs font-mono"
|
|
57
|
+
on-change="setName($event.target.value)"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="flex-1">
|
|
61
|
+
<form on-submit.prevent="onRun()">
|
|
62
|
+
<code-editor
|
|
63
|
+
nolines="1"
|
|
64
|
+
nostatus="1"
|
|
65
|
+
language="sql"
|
|
66
|
+
bind-value="query"
|
|
67
|
+
on-change="setQuery($event.target.value)"
|
|
68
|
+
class="font-mono text-xs border rounded-md w-full block h-32"
|
|
69
|
+
></code-editor>
|
|
70
|
+
<div class="flex items-center justify-end mt-2">
|
|
71
|
+
<button
|
|
72
|
+
type="submit"
|
|
73
|
+
bind-disabled="!name || !query"
|
|
74
|
+
class="bg-gray-400 text-black px-4 py-2 rounded focus:bg-white hover:bg-white"
|
|
75
|
+
>
|
|
76
|
+
Run
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</form>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<script setup>
|
|
83
|
+
import { hook } from "@li3/web";
|
|
84
|
+
|
|
85
|
+
export default function () {
|
|
86
|
+
const [name, setName] = hook("");
|
|
87
|
+
const [query, setQuery] = hook("");
|
|
88
|
+
const [responses] = hook("");
|
|
89
|
+
const [error, setError] = hook("");
|
|
90
|
+
|
|
91
|
+
function append(r) {
|
|
92
|
+
responses.value = [...responses.value, r];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function onRun() {
|
|
96
|
+
if (!name.value) return;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
setError("");
|
|
100
|
+
|
|
101
|
+
const db = await import(`https://${name}.db.apphor.de/index.mjs`);
|
|
102
|
+
const q = query.value;
|
|
103
|
+
const s = await (q.toLowerCase().includes("select")
|
|
104
|
+
? db.all(q)
|
|
105
|
+
: db.run(q));
|
|
106
|
+
|
|
107
|
+
append(JSON.stringify(s, null, 2));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
setError(e);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { name, query, responses, setName, setQuery, error, onRun };
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
</template>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import { IncomingMessage, ServerResponse } from
|
|
2
|
-
import { Database } from
|
|
3
|
-
type Query = {
|
|
4
|
-
db: string;
|
|
5
|
-
request: IncomingMessage;
|
|
6
|
-
response: ServerResponse;
|
|
7
|
-
args: Record<string, string>;
|
|
8
|
-
};
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { Database } from 'better-sqlite3';
|
|
9
3
|
export declare function getDatabase(file: string): Database;
|
|
10
4
|
export declare function serve(): Promise<any>;
|
|
11
5
|
export declare function handleRequest(request: IncomingMessage, response: ServerResponse, db: string): Promise<void>;
|
|
12
|
-
export declare function onQuery(
|
|
13
|
-
export {};
|
|
6
|
+
export declare function onQuery(request: IncomingMessage, response: ServerResponse, db: string): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,81 +1,84 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import SQLite from
|
|
3
|
-
import { join } from
|
|
4
|
-
import createServer from
|
|
5
|
-
const methods = [
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import SQLite from 'better-sqlite3';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import createServer from '@cloud-cli/http';
|
|
5
|
+
const methods = ['all', 'run', 'get'];
|
|
6
6
|
const baseDomain = process.env.BASE_DOMAIN;
|
|
7
|
-
const dataPath = process.env.DATA_PATH || join(import.meta.dirname,
|
|
7
|
+
const dataPath = process.env.DATA_PATH || join(import.meta.dirname, 'data');
|
|
8
8
|
export function getDatabase(file) {
|
|
9
9
|
const fullPath = join(dataPath, file);
|
|
10
10
|
const db = new SQLite(fullPath);
|
|
11
|
-
db.pragma(
|
|
11
|
+
db.pragma('journal_mode = WAL');
|
|
12
12
|
return db;
|
|
13
13
|
}
|
|
14
14
|
export function serve() {
|
|
15
15
|
if (baseDomain) {
|
|
16
16
|
return createServer((req, res) => {
|
|
17
|
-
const hostname = String(req.headers[
|
|
18
|
-
const subdomain = hostname
|
|
17
|
+
const hostname = String(req.headers['x-forwarded-for'] || '');
|
|
18
|
+
const subdomain = hostname
|
|
19
|
+
.replace(baseDomain, '')
|
|
20
|
+
.replace('.', '')
|
|
21
|
+
.replace(/[^a-z0-9-]+/g, '');
|
|
19
22
|
if (subdomain) {
|
|
20
|
-
return handleRequest(req, res, subdomain +
|
|
23
|
+
return handleRequest(req, res, subdomain + '.sqlite3');
|
|
21
24
|
}
|
|
22
25
|
res.writeHead(400).end();
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
|
-
return createServer((req, res) => handleRequest(req, res,
|
|
28
|
+
return createServer((req, res) => handleRequest(req, res, 'db.sqlite3'));
|
|
26
29
|
}
|
|
27
|
-
export function handleRequest(request, response, db) {
|
|
28
|
-
const url = new URL(request.url,
|
|
30
|
+
export async function handleRequest(request, response, db) {
|
|
31
|
+
const url = new URL(request.url, 'http://localhost');
|
|
29
32
|
const route = `${request.method} ${url.pathname}`.trim();
|
|
30
|
-
const args = Object.fromEntries(url.searchParams.entries());
|
|
31
|
-
const q = { db, request, response, args };
|
|
32
33
|
switch (route) {
|
|
33
|
-
case
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
34
|
+
case 'GET /console.html':
|
|
35
|
+
const consolePage = await readFile('./console.html', 'utf8');
|
|
36
|
+
response.writeHead(200, { 'content-type': 'text/html' }).end(consolePage);
|
|
37
|
+
return;
|
|
38
|
+
case 'GET /index.mjs':
|
|
39
|
+
return onEsModule(request, response);
|
|
40
|
+
case 'POST /query':
|
|
41
|
+
return onQuery(request, response, db);
|
|
37
42
|
default:
|
|
38
43
|
response.writeHead(404).end();
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
|
-
export async function onQuery(
|
|
46
|
+
export async function onQuery(request, response, db) {
|
|
42
47
|
const query = Buffer.concat(await request.toArray());
|
|
43
48
|
if (!query.length) {
|
|
44
49
|
response.writeHead(400).end();
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
47
52
|
try {
|
|
48
|
-
const { s =
|
|
53
|
+
const { s = '', d, m = 'run', p } = JSON.parse(query.toString('utf-8'));
|
|
49
54
|
if (!s.trim()) {
|
|
50
|
-
throw new Error(
|
|
55
|
+
throw new Error('Invalid statement.');
|
|
51
56
|
}
|
|
52
57
|
if (!methods.includes(m)) {
|
|
53
|
-
throw new Error(
|
|
58
|
+
throw new Error('Invalid method. Must be one of ' + methods.join(', '));
|
|
54
59
|
}
|
|
55
60
|
const sqlite = getDatabase(db);
|
|
61
|
+
if (p && Array.isArray(p) && p.every(s => typeof s === 'string')) {
|
|
62
|
+
for (const s of p) {
|
|
63
|
+
sqlite.pragma(s);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
56
66
|
const runner = sqlite.prepare(s.trim());
|
|
57
67
|
const result = d ? runner[m](d) : runner[m]();
|
|
58
|
-
response.end(JSON.stringify(result));
|
|
68
|
+
response.end(JSON.stringify(result || null));
|
|
59
69
|
}
|
|
60
70
|
catch (error) {
|
|
61
71
|
process.env.DEBUG && console.error(error);
|
|
62
72
|
response.writeHead(400).end(String(error));
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
file = await readFile("./client.mjs", "utf8");
|
|
69
|
-
}
|
|
70
|
-
return file;
|
|
71
|
-
}
|
|
72
|
-
async function onEsModule({ request, response }) {
|
|
73
|
-
const hostname = String(request.headers["x-forwarded-for"]);
|
|
74
|
-
const code = await getFile();
|
|
75
|
+
async function onEsModule(request, response) {
|
|
76
|
+
const hostname = String(request.headers['x-forwarded-host']);
|
|
77
|
+
const code = await readFile('./client.mjs', 'utf8');
|
|
75
78
|
response
|
|
76
79
|
.writeHead(200, {
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
'Content-Type': 'text/javascript',
|
|
81
|
+
'Access-Control-Allow-Origin': '*',
|
|
79
82
|
})
|
|
80
|
-
.end(code.replace(
|
|
83
|
+
.end(code.replace('__API_URL__', hostname));
|
|
81
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloud-cli/d0",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"@cloud-cli/http": "^1.1.0",
|
|
22
|
+
"better-sqlite3": "^12.11.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@cloud-cli/prettier-config": "^1.0.0",
|
|
26
|
-
"@cloud-cli/typescript-config": "^1.0.
|
|
27
|
-
"@types/better-sqlite3": "^7.6.
|
|
28
|
-
"@types/node": "^20.
|
|
29
|
-
"typescript": "^5.
|
|
26
|
+
"@cloud-cli/typescript-config": "^1.0.1",
|
|
27
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
28
|
+
"@types/node": "^20.19.43",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
30
|
}
|
|
31
31
|
}
|