@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 CHANGED
@@ -1 +1,4 @@
1
- .github/
1
+ .github/
2
+ .npmignore/
3
+ .gitignore/
4
+ README.md
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
- RUN npm i && npm run build && rm -r src/ node_modules/ && npm cache clean --force
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
- RUN npm install --omit=dev && npm cache clean --force
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
- export async function query(statement, data, method = "run") {
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 || null,
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 function get(statement, data) {
21
- return query(statement, data, "get");
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 run(statement, data) {
25
- return query(statement, data, "run");
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 "node:http";
2
- import { Database } from "better-sqlite3";
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({ db, request, response }: Query): Promise<void>;
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 "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"];
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, "data");
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("journal_mode = WAL");
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["x-forwarded-for"] || "");
18
- const subdomain = hostname.replace(baseDomain, "").replace(".", "");
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 + ".sqlite3");
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, "db.sqlite3"));
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, "http://localhost");
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 "GET /index.mjs":
34
- return onEsModule(q);
35
- case "POST /query":
36
- return onQuery(q);
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({ db, request, response }) {
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 = "", d, m = "run" } = JSON.parse(query.toString("utf-8"));
53
+ const { s = '', d, m = 'run', p } = JSON.parse(query.toString('utf-8'));
49
54
  if (!s.trim()) {
50
- throw new Error("Invalid statement.");
55
+ throw new Error('Invalid statement.');
51
56
  }
52
57
  if (!methods.includes(m)) {
53
- throw new Error("Invalid method. Must be one of " + methods.join(", "));
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
- let file;
66
- async function getFile() {
67
- if (!file) {
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
- "Content-Type": "text/javascript",
78
- "Access-Control-Allow-Origin": "*",
80
+ 'Content-Type': 'text/javascript',
81
+ 'Access-Control-Allow-Origin': '*',
79
82
  })
80
- .end(code.replace("__API_URL__", hostname));
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.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
- "better-sqlite3": "^11.0.0",
22
- "@cloud-cli/http": "^1.0.0"
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.0",
27
- "@types/better-sqlite3": "^7.6.3",
28
- "@types/node": "^20.3.3",
29
- "typescript": "^5.4.3"
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
  }
@@ -0,0 +1,2 @@
1
+ allowBuilds:
2
+ better-sqlite3: true