@cloud-cli/d0 1.4.1 → 1.6.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/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">
@@ -78,39 +68,109 @@
78
68
  </div>
79
69
  </form>
80
70
  </div>
71
+ <mermaid-graph class="flex-1 h-64 overflow-auto" bind-input="diagram"></mermaid-graph>
81
72
  </div>
73
+
82
74
  <script setup>
83
- import { hook } from "@li3/web";
75
+ import { hook, onInit } from '@li3/web';
84
76
 
85
77
  export default function () {
86
- const [name, setName] = hook("");
87
- const [query, setQuery] = hook("");
88
- const [responses] = hook("");
89
- const [error, setError] = hook("");
78
+ const [diagram, setDiagram] = hook(null);
79
+ const url = new URL(location.href);
80
+ const [name, setName] = hook(url.hostname.replace('.db.apphor.de', ''));
81
+ const [query, setQuery] = hook('');
82
+ const [responses] = hook('');
83
+ const [error, setError] = hook('');
90
84
 
91
85
  function append(r) {
92
86
  responses.value = [...responses.value, r];
93
87
  }
94
88
 
89
+ function onKeyUp(event) {
90
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
91
+ event.preventDefault();
92
+ onRun();
93
+ }
94
+ }
95
+
96
+ async function runQuery(q) {
97
+ const db = await import(`https://${name}.db.apphor.de/index.mjs`);
98
+ if (!q.endsWith(';')) {
99
+ q += ';';
100
+ }
101
+
102
+ const s = await (q.toLowerCase().includes('select ') ? db.all(q) : db.run(q));
103
+ return s;
104
+ }
105
+
95
106
  async function onRun() {
96
107
  if (!name.value) return;
97
108
 
98
109
  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
-
110
+ setError('');
111
+ const q = query.value.trim();
112
+ const s = await runQuery(q);
107
113
  append(JSON.stringify(s, null, 2));
108
114
  } catch (e) {
109
115
  setError(e);
110
116
  }
111
117
  }
112
118
 
113
- return { name, query, responses, setName, setQuery, error, onRun };
119
+ onInit(async () => {
120
+ const schema = await runQuery('select * from sqlite_schema;');
121
+ setDiagram(schemaToMermaid(schema));
122
+ });
123
+
124
+ return { name, diagram, query, responses, setName, setQuery, error, onRun, onKeyUp };
125
+ }
126
+
127
+ function schemaToMermaid(schema) {
128
+ let diagram = 'erDiagram\n';
129
+
130
+ for (const row of schema) {
131
+ // 1. Ignore indexes, views, and system tables
132
+ if (row.type !== 'table' || row.name.startsWith('sqlite_') || !row.sql) continue;
133
+
134
+ const tableName = row.name;
135
+ const sql = row.sql;
136
+
137
+ // 2. Extract relationships (matches both inline 'REFERENCES tbl' and 'FOREIGN KEY (...) REFERENCES tbl')
138
+ const fkRegex = /REFERENCES\s+([^\s(;]+)/gi;
139
+ let match;
140
+ const referencedTables = new Set();
141
+
142
+ while ((match = fkRegex.exec(sql)) !== null) {
143
+ // Clean up target table name (strip quotes/brackets)
144
+ const parentTable = match[1].replace(/[`"'[\]]/g, '').split('(')[0];
145
+ referencedTables.add(parentTable);
146
+ }
147
+
148
+ referencedTables.forEach((parentTable) => {
149
+ diagram += ` ${parentTable} ||--o{ ${tableName} : "references"\n`;
150
+ });
151
+
152
+ // 3. Extract columns
153
+ diagram += ` ${tableName} {\n`;
154
+ const bodyMatch = sql.match(/\(([\s\S]+)\)/);
155
+
156
+ if (bodyMatch) {
157
+ const definitions = bodyMatch[1].split(/,\s*(?![^()]*\))/);
158
+ definitions.forEach((def) => {
159
+ const parts = def.trim().split(/\s+/);
160
+ const keyword = parts[0] ? parts[0].toUpperCase() : '';
161
+
162
+ // Skip table-level constraints
163
+ if (parts[0] && !['CONSTRAINT', 'PRIMARY', 'FOREIGN', 'UNIQUE', 'CHECK'].includes(keyword)) {
164
+ const colName = parts[0].replace(/[`"'[\]]/g, '');
165
+ const colType = parts[1] ? parts[1].replace(/,/g, '') : 'TEXT';
166
+ diagram += ` ${colType} ${colName}\n`;
167
+ }
168
+ });
169
+ }
170
+ diagram += ` }\n`;
171
+ }
172
+
173
+ return diagram;
114
174
  }
115
175
  </script>
116
176
  </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.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",