@cloud-cli/d0 1.4.1 → 1.6.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/console.html CHANGED
@@ -1,16 +1,11 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
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
- />
6
+ <link rel="component" href="https://sodium.static.apphor.de/code-editor.html" />
7
+ <link rel="component" href="https://sodium.static.apphor.de/code-block.html" />
8
+ <link rel="component" href="https://sodium.static.apphor.de/mermaid-graph.html" />
14
9
  <title></title>
15
10
  <script type="importmap">
16
11
  {
@@ -21,30 +16,24 @@
21
16
  }
22
17
  }
23
18
  </script>
24
- <link
25
- rel="stylesheet"
26
- href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
27
- />
19
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" />
28
20
  </head>
29
- <body
30
- class="bg-gray-900 text-white font-sans h-screen flex flex-col overflow-hidden"
31
- >
21
+ <body class="bg-gray-900 text-white font-sans h-screen flex flex-col overflow-hidden">
32
22
  <app-main class="contents"></app-main>
33
23
 
34
24
  <script type="module">
35
- import "@li3/web";
25
+ import '@li3/web';
36
26
  </script>
37
27
 
38
28
  <template component="app-main">
39
- <div
40
- class="flex flex-col items-stretch justify-between space-y-2 p-4 h-full"
41
- >
29
+ <div class="flex flex-col items-stretch justify-between space-y-2 p-4 h-full">
42
30
  <div class="p-2 overflow-auto font-mono text-xs h-full">
43
31
  <template for="r of responses">
44
32
  <code-block
45
33
  bind-source="r || ''"
46
34
  language="json"
47
- class="py-1 border-b"
35
+ class="py-1 border-b block w-full whitespace-pre-wrap overflow-auto"
36
+ style="max-height: 10rem"
48
37
  ></code-block>
49
38
  </template>
50
39
  <div class="text-red-400 text-sm">{{ error || '' }}</div>
@@ -65,6 +54,7 @@
65
54
  language="sql"
66
55
  bind-value="query"
67
56
  on-change="setQuery($event.target.value)"
57
+ on-keyup="onKeyUp($event)"
68
58
  class="font-mono text-xs border rounded-md w-full block h-32"
69
59
  ></code-editor>
70
60
  <div class="flex items-center justify-end mt-2">
@@ -75,42 +65,124 @@
75
65
  >
76
66
  Run
77
67
  </button>
68
+
69
+ <button
70
+ type="button"
71
+ on-click="onUpdateGraph()"
72
+ bind-disabled="!name"
73
+ class="bg-gray-400 text-black px-4 py-2 rounded focus:bg-white hover:bg-white"
74
+ >
75
+ Graph
76
+ </button>
78
77
  </div>
79
78
  </form>
80
79
  </div>
80
+ <mermaid-graph class="flex-1 h-64 overflow-auto" bind-input="diagram"></mermaid-graph>
81
81
  </div>
82
+
82
83
  <script setup>
83
- import { hook } from "@li3/web";
84
+ import { hook, onInit } from '@li3/web';
84
85
 
85
86
  export default function () {
86
- const [name, setName] = hook("");
87
- const [query, setQuery] = hook("");
88
- const [responses] = hook("");
89
- const [error, setError] = hook("");
87
+ const [diagram, setDiagram] = hook(null);
88
+ const url = new URL(location.href);
89
+ const [name, setName] = hook(url.hostname.replace('.db.apphor.de', ''));
90
+ const [query, setQuery] = hook('');
91
+ const [responses] = hook('');
92
+ const [error, setError] = hook('');
90
93
 
91
94
  function append(r) {
92
95
  responses.value = [...responses.value, r];
93
96
  }
94
97
 
98
+ function onKeyUp(event) {
99
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
100
+ event.preventDefault();
101
+ onRun();
102
+ }
103
+ }
104
+
105
+ async function runQuery(q) {
106
+ const db = await import(`https://${name}.db.apphor.de/index.mjs`);
107
+ if (!q.endsWith(';')) {
108
+ q += ';';
109
+ }
110
+
111
+ const s = await (q.toLowerCase().includes('select ') ? db.all(q) : db.run(q));
112
+ return s;
113
+ }
114
+
95
115
  async function onRun() {
96
116
  if (!name.value) return;
97
117
 
98
118
  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
-
119
+ setError('');
120
+ const q = query.value.trim();
121
+ const s = await runQuery(q);
107
122
  append(JSON.stringify(s, null, 2));
108
123
  } catch (e) {
109
124
  setError(e);
110
125
  }
111
126
  }
112
127
 
113
- return { name, query, responses, setName, setQuery, error, onRun };
128
+ async function onUpdateGraph() {
129
+ if (!name.value) return;
130
+ const schema = await runQuery('select * from sqlite_schema;');
131
+ setDiagram(schemaToMermaid(schema));
132
+ }
133
+
134
+ onInit(onUpdateGraph);
135
+
136
+ return { name, diagram, query, responses, setName, setQuery, error, onRun, onKeyUp, onUpdateGraph };
137
+ }
138
+
139
+ function schemaToMermaid(schema) {
140
+ let diagram = 'erDiagram\n';
141
+
142
+ for (const row of schema) {
143
+ // 1. Ignore indexes, views, and system tables
144
+ if (row.type !== 'table' || row.name.startsWith('sqlite_') || !row.sql) continue;
145
+
146
+ const tableName = row.name;
147
+ const sql = row.sql;
148
+
149
+ // 2. Extract relationships (matches both inline 'REFERENCES tbl' and 'FOREIGN KEY (...) REFERENCES tbl')
150
+ const fkRegex = /REFERENCES\s+([^\s(;]+)/gi;
151
+ let match;
152
+ const referencedTables = new Set();
153
+
154
+ while ((match = fkRegex.exec(sql)) !== null) {
155
+ // Clean up target table name (strip quotes/brackets)
156
+ const parentTable = match[1].replace(/[`"'[\]]/g, '').split('(')[0];
157
+ referencedTables.add(parentTable);
158
+ }
159
+
160
+ referencedTables.forEach((parentTable) => {
161
+ diagram += ` ${parentTable} ||--o{ ${tableName} : "references"\n`;
162
+ });
163
+
164
+ // 3. Extract columns
165
+ diagram += ` ${tableName} {\n`;
166
+ const bodyMatch = sql.match(/\(([\s\S]+)\)/);
167
+
168
+ if (bodyMatch) {
169
+ const definitions = bodyMatch[1].split(/,\s*(?![^()]*\))/);
170
+ definitions.forEach((def) => {
171
+ const parts = def.trim().split(/\s+/);
172
+ const keyword = parts[0] ? parts[0].toUpperCase() : '';
173
+
174
+ // Skip table-level constraints
175
+ if (parts[0] && !['CONSTRAINT', 'PRIMARY', 'FOREIGN', 'UNIQUE', 'CHECK'].includes(keyword)) {
176
+ const colName = parts[0].replace(/[`"'[\]]/g, '');
177
+ const colType = parts[1] ? parts[1].replace(/,/g, '') : 'TEXT';
178
+ diagram += ` ${colType} ${colName}\n`;
179
+ }
180
+ });
181
+ }
182
+ diagram += ` }\n`;
183
+ }
184
+
185
+ return diagram;
114
186
  }
115
187
  </script>
116
188
  </template>
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { Database } from 'better-sqlite3';
3
3
  export declare function getDatabase(file: string): Database;
4
- export declare function serve(): Promise<any>;
4
+ export declare function serve(): any;
5
5
  export declare function handleRequest(request: IncomingMessage, response: ServerResponse, db: string): Promise<void>;
6
6
  export declare function onQuery(request: IncomingMessage, response: ServerResponse, db: string): Promise<void>;
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { createServer } from 'node:http';
1
2
  import { readFile } from 'node:fs/promises';
2
3
  import SQLite from 'better-sqlite3';
3
4
  import { join } from 'node:path';
4
- import createServer from '@cloud-cli/http';
5
- const methods = ['all', 'run', 'get'];
5
+ const DEBUG = !!process.env.DEBUG;
6
+ const methods = ['all', 'run', 'get', 'exec'];
6
7
  const baseDomain = process.env.BASE_DOMAIN;
7
8
  const dataPath = process.env.DATA_PATH || join(import.meta.dirname, 'data');
8
9
  export function getDatabase(file) {
@@ -12,9 +13,10 @@ export function getDatabase(file) {
12
13
  return db;
13
14
  }
14
15
  export function serve() {
16
+ let server;
15
17
  if (baseDomain) {
16
- return createServer((req, res) => {
17
- const hostname = String(req.headers['x-forwarded-for'] || '');
18
+ server = createServer((req, res) => {
19
+ const hostname = String(req.headers['x-forwarded-host'] || '');
18
20
  const subdomain = hostname
19
21
  .replace(baseDomain, '')
20
22
  .replace('.', '')
@@ -25,9 +27,19 @@ export function serve() {
25
27
  res.writeHead(400).end();
26
28
  });
27
29
  }
28
- return createServer((req, res) => handleRequest(req, res, 'db.sqlite3'));
30
+ else {
31
+ server = createServer((req, res) => handleRequest(req, res, 'db.sqlite3'));
32
+ }
33
+ server.listen(+process.env.PORT, () => {
34
+ console.log(`Started on ${process.env.PORT}`);
35
+ });
36
+ return server;
29
37
  }
30
38
  export async function handleRequest(request, response, db) {
39
+ DEBUG &&
40
+ response.on('finish', () => {
41
+ console.log(`[${new Date().toISOString().slice(0, 19)}] [${response.statusCode} ${String(request.headers['x-forwarded-host'] || '')}] ${request.method} ${request.url}`);
42
+ });
31
43
  const url = new URL(request.url, 'http://localhost');
32
44
  const route = `${request.method} ${url.pathname}`.trim();
33
45
  switch (route) {
@@ -58,17 +70,24 @@ export async function onQuery(request, response, db) {
58
70
  throw new Error('Invalid method. Must be one of ' + methods.join(', '));
59
71
  }
60
72
  const sqlite = getDatabase(db);
61
- if (p && Array.isArray(p) && p.every(s => typeof s === 'string')) {
73
+ if (p && Array.isArray(p) && p.every((s) => typeof s === 'string')) {
62
74
  for (const s of p) {
63
75
  sqlite.pragma(s);
64
76
  }
65
77
  }
66
- const runner = sqlite.prepare(s.trim());
67
- const result = d ? runner[m](d) : runner[m]();
78
+ let result;
79
+ if (m === 'exec') {
80
+ result = sqlite.exec(s.trim());
81
+ }
82
+ else {
83
+ const runner = sqlite.prepare(s.trim());
84
+ result = d ? runner[m](d) : runner[m]();
85
+ }
68
86
  response.end(JSON.stringify(result || null));
87
+ DEBUG && console.log(s.trim(), d, result);
69
88
  }
70
89
  catch (error) {
71
- process.env.DEBUG && console.error(error);
90
+ DEBUG && console.error(error);
72
91
  response.writeHead(400).end(String(error));
73
92
  }
74
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-cli/d0",
3
- "version": "1.4.1",
3
+ "version": "1.6.1",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",