morphdb 0.1.1__tar.gz → 0.1.3__tar.gz
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.
- {morphdb-0.1.1 → morphdb-0.1.3}/PKG-INFO +125 -60
- {morphdb-0.1.1 → morphdb-0.1.3}/README.md +124 -59
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/__init__.py +1 -1
- morphdb-0.1.3/morphdb/cli/__init__.py +24 -0
- morphdb-0.1.3/morphdb/cli/dashboard.py +134 -0
- morphdb-0.1.3/morphdb/cli/main.py +185 -0
- morphdb-0.1.3/morphdb/cli/service.py +169 -0
- morphdb-0.1.3/morphdb/cli/skill.py +56 -0
- morphdb-0.1.3/morphdb/skill/SKILL.md +259 -0
- morphdb-0.1.3/morphdb/skill/scripts/morphdb_schema.py +285 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb.egg-info/PKG-INFO +125 -60
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb.egg-info/SOURCES.txt +8 -0
- morphdb-0.1.3/morphdb.egg-info/entry_points.txt +2 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/pyproject.toml +8 -3
- morphdb-0.1.3/tests/test_cli.py +152 -0
- morphdb-0.1.1/morphdb.egg-info/entry_points.txt +0 -2
- {morphdb-0.1.1 → morphdb-0.1.3}/LICENSE +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/__main__.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/apps.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/associations.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/db.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/errors.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/fieldtypes.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/objects.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/router.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/routes.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/schema.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/server.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb/util.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb.egg-info/dependency_links.txt +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb.egg-info/requires.txt +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/morphdb.egg-info/top_level.txt +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/setup.cfg +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/tests/test_apps.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/tests/test_core.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/tests/test_hardening.py +0 -0
- {morphdb-0.1.1 → morphdb-0.1.3}/tests/test_relations.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: morphdb
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A coding-agent-friendly, multi-tenant backend for vibe-coded websites. One process hosts many isolated apps; reshape each app's schema as fast as your agent iterates while the frontend keeps calling the same generic endpoints.
|
|
5
5
|
Author: morphdb contributors
|
|
6
6
|
License: MIT
|
|
@@ -30,21 +30,119 @@ Dynamic: license-file
|
|
|
30
30
|
**A coding-agent-friendly, multi-tenant backend for vibe-coded websites.**
|
|
31
31
|
|
|
32
32
|
Reshape the data model as fast as your coding agent iterates — the frontend
|
|
33
|
-
keeps calling the same small set of generic, deterministic endpoints.
|
|
33
|
+
keeps calling the same small set of generic, deterministic endpoints. One
|
|
34
|
+
process hosts many isolated apps (one per site), zero dependencies, backed by
|
|
35
|
+
SQLite.
|
|
34
36
|
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install morphdb
|
|
35
41
|
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
|
|
43
|
+
Manage the local server with the `morphdb` CLI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
morphdb start # run in the background (default 127.0.0.1:8787)
|
|
47
|
+
morphdb status # running? where? how many apps?
|
|
48
|
+
morphdb stop # stop it
|
|
49
|
+
morphdb run # run in the foreground instead (blocking)
|
|
50
|
+
morphdb dashboard # read-only web view of every app + its tables
|
|
51
|
+
morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
|
|
55
|
+
`--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
|
|
56
|
+
`--host`, `--port`, `--db`. From a source checkout with no install, the
|
|
57
|
+
foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
|
|
58
|
+
|
|
59
|
+
To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
|
|
60
|
+
to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
|
|
61
|
+
|
|
62
|
+
**Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
|
|
63
|
+
`https://db.example.com`) and the schema CLI — plus any frontend that reads
|
|
64
|
+
`window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
|
|
65
|
+
of localhost. It's a client-side setting that names a *backend*, not a database
|
|
66
|
+
connection string.
|
|
67
|
+
|
|
68
|
+
## Use it
|
|
69
|
+
|
|
70
|
+
With the server running (`morphdb start`):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
BASE=http://127.0.0.1:8787
|
|
74
|
+
|
|
75
|
+
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
76
|
+
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
77
|
+
H="X-App-Key: my-site"
|
|
78
|
+
|
|
79
|
+
# 1. define types + a relation
|
|
80
|
+
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
81
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
82
|
+
"fields": {"title":"string","done":"boolean","priority":"number"},
|
|
83
|
+
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
84
|
+
|
|
85
|
+
# 2. create + read + query
|
|
86
|
+
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
87
|
+
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
88
|
+
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
89
|
+
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
90
|
+
|
|
91
|
+
# 3. morph the schema later — existing rows just gain the new field as null
|
|
92
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
96
|
+
|
|
97
|
+
## Command-line interface
|
|
98
|
+
|
|
99
|
+
`morphdb` runs the server as a **background service** — `start` launches it
|
|
100
|
+
detached and hands your terminal straight back; `status` / `stop` find it again
|
|
101
|
+
via a pid file under the state dir.
|
|
102
|
+
|
|
103
|
+
| Command | What it does |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
|
|
106
|
+
| `morphdb status` | Is it running? URL, pid, health, and app count. |
|
|
107
|
+
| `morphdb stop` | Stop the background server. |
|
|
108
|
+
| `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
|
|
109
|
+
| `morphdb run` | Run in the **foreground** (blocking) instead. |
|
|
110
|
+
| `morphdb dashboard` | Open a read-only web view of every app and its tables. |
|
|
111
|
+
| `morphdb install-skill` | Install the bundled Claude Code skill (below). |
|
|
112
|
+
| `morphdb --version` | Print the version. |
|
|
113
|
+
|
|
114
|
+
`start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
|
|
115
|
+
and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
|
|
116
|
+
`dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
|
|
117
|
+
state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
|
|
118
|
+
`$MORPHDB_HOME`.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
morphdb start # background, default 127.0.0.1:8787
|
|
122
|
+
morphdb start --port 9000 --db ./my.sqlite3
|
|
123
|
+
morphdb status # -> running (pid …) at http://… [healthy]
|
|
124
|
+
morphdb dashboard # opens http://127.0.0.1:8788
|
|
125
|
+
morphdb stop
|
|
126
|
+
morphdb run # foreground instead (Ctrl-C to quit)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Install the Claude Code skill
|
|
130
|
+
|
|
131
|
+
`install-skill` writes the bundled MorphDB skill into a Claude skills directory,
|
|
132
|
+
so a coding agent automatically reaches for MorphDB when building a data-backed
|
|
133
|
+
site:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
|
|
137
|
+
morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
|
|
138
|
+
morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
|
|
46
139
|
```
|
|
47
140
|
|
|
141
|
+
It installs the skill **bundled in the installed package** (not live from
|
|
142
|
+
GitHub) and is **idempotent** — re-running overwrites with the current version.
|
|
143
|
+
To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
|
|
144
|
+
Claude Code afterward to pick it up.
|
|
145
|
+
|
|
48
146
|
## Why
|
|
49
147
|
|
|
50
148
|
AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
|
|
@@ -58,6 +156,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
|
|
|
58
156
|
**no migration, no row rewrite, no downtime** — regardless of how much data
|
|
59
157
|
exists. Meanwhile the frontend talks to generic endpoints that never change.
|
|
60
158
|
|
|
159
|
+
```
|
|
160
|
+
you (the coding agent) the frontend you build
|
|
161
|
+
────────────────────── ──────────────────────
|
|
162
|
+
reshape the schema freely │ calls fixed generic endpoints
|
|
163
|
+
PUT /schema/{type} │ POST /objects/{type}
|
|
164
|
+
GET /schema │ GET /objects/{type}?field=…
|
|
165
|
+
DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
|
|
166
|
+
│ │
|
|
167
|
+
└────────────── MorphDB ───────────┘
|
|
168
|
+
(one process · many apps · SQLite)
|
|
169
|
+
every call: X-App-Key: <app>
|
|
170
|
+
```
|
|
171
|
+
|
|
61
172
|
## The shape of it
|
|
62
173
|
|
|
63
174
|
One MorphDB process hosts **many apps** (one per website), fully isolated from
|
|
@@ -120,58 +231,12 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
|
|
|
120
231
|
- **Query layer**: filter operators, sorting, pagination — all generic.
|
|
121
232
|
- **Multi-tenant by app** — one process backs many isolated sites; every call is scoped by an `X-App-Key`, and deleting an app cascades away all its data.
|
|
122
233
|
- **Wide-open CORS** so any frontend origin can call it in dev.
|
|
123
|
-
- **A
|
|
234
|
+
- **A management CLI** — `morphdb start/status/stop`, a read-only admin dashboard, and one-command skill install.
|
|
235
|
+
- **A Claude Code skill** (`morphdb/skill/SKILL.md`, install with `morphdb install-skill`) with a schema CLI so the agent edits the model without hand-writing curl.
|
|
124
236
|
|
|
125
237
|
> Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
|
|
126
238
|
> horizontal scale, or production durability guarantees.
|
|
127
239
|
|
|
128
|
-
## Install / run
|
|
129
|
-
|
|
130
|
-
No install required:
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
python3 -m morphdb --port 8787 --db ./app.sqlite3
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Or install the console script:
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
pip install -e .
|
|
140
|
-
morphdb --port 8787 --db ./app.sqlite3
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Flags: `--host` (default `127.0.0.1`), `--port` (default `8787`),
|
|
144
|
-
`--db` (default `morphdb.sqlite3`; use `:memory:` for ephemeral).
|
|
145
|
-
|
|
146
|
-
Then: `curl http://127.0.0.1:8787/help` for a live reference.
|
|
147
|
-
|
|
148
|
-
## Quickstart
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
BASE=http://127.0.0.1:8787
|
|
152
|
-
|
|
153
|
-
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
154
|
-
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
155
|
-
H="X-App-Key: my-site"
|
|
156
|
-
|
|
157
|
-
# 1. define types + a relation
|
|
158
|
-
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
159
|
-
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
160
|
-
"fields": {"title":"string","done":"boolean","priority":"number"},
|
|
161
|
-
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
162
|
-
|
|
163
|
-
# 2. create + read + query
|
|
164
|
-
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
165
|
-
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
166
|
-
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
167
|
-
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
168
|
-
|
|
169
|
-
# 3. morph the schema later — existing rows just gain the new field as null
|
|
170
|
-
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
174
|
-
|
|
175
240
|
## Data model
|
|
176
241
|
|
|
177
242
|
| Concept | What it is |
|
|
@@ -3,21 +3,119 @@
|
|
|
3
3
|
**A coding-agent-friendly, multi-tenant backend for vibe-coded websites.**
|
|
4
4
|
|
|
5
5
|
Reshape the data model as fast as your coding agent iterates — the frontend
|
|
6
|
-
keeps calling the same small set of generic, deterministic endpoints.
|
|
6
|
+
keeps calling the same small set of generic, deterministic endpoints. One
|
|
7
|
+
process hosts many isolated apps (one per site), zero dependencies, backed by
|
|
8
|
+
SQLite.
|
|
7
9
|
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install morphdb
|
|
8
14
|
```
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
Manage the local server with the `morphdb` CLI:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
morphdb start # run in the background (default 127.0.0.1:8787)
|
|
20
|
+
morphdb status # running? where? how many apps?
|
|
21
|
+
morphdb stop # stop it
|
|
22
|
+
morphdb run # run in the foreground instead (blocking)
|
|
23
|
+
morphdb dashboard # read-only web view of every app + its tables
|
|
24
|
+
morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
|
|
28
|
+
`--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
|
|
29
|
+
`--host`, `--port`, `--db`. From a source checkout with no install, the
|
|
30
|
+
foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
|
|
31
|
+
|
|
32
|
+
To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
|
|
33
|
+
to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
|
|
34
|
+
|
|
35
|
+
**Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
|
|
36
|
+
`https://db.example.com`) and the schema CLI — plus any frontend that reads
|
|
37
|
+
`window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
|
|
38
|
+
of localhost. It's a client-side setting that names a *backend*, not a database
|
|
39
|
+
connection string.
|
|
40
|
+
|
|
41
|
+
## Use it
|
|
42
|
+
|
|
43
|
+
With the server running (`morphdb start`):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
BASE=http://127.0.0.1:8787
|
|
47
|
+
|
|
48
|
+
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
49
|
+
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
50
|
+
H="X-App-Key: my-site"
|
|
51
|
+
|
|
52
|
+
# 1. define types + a relation
|
|
53
|
+
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
54
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
55
|
+
"fields": {"title":"string","done":"boolean","priority":"number"},
|
|
56
|
+
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
57
|
+
|
|
58
|
+
# 2. create + read + query
|
|
59
|
+
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
60
|
+
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
61
|
+
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
62
|
+
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
63
|
+
|
|
64
|
+
# 3. morph the schema later — existing rows just gain the new field as null
|
|
65
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
69
|
+
|
|
70
|
+
## Command-line interface
|
|
71
|
+
|
|
72
|
+
`morphdb` runs the server as a **background service** — `start` launches it
|
|
73
|
+
detached and hands your terminal straight back; `status` / `stop` find it again
|
|
74
|
+
via a pid file under the state dir.
|
|
75
|
+
|
|
76
|
+
| Command | What it does |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
|
|
79
|
+
| `morphdb status` | Is it running? URL, pid, health, and app count. |
|
|
80
|
+
| `morphdb stop` | Stop the background server. |
|
|
81
|
+
| `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
|
|
82
|
+
| `morphdb run` | Run in the **foreground** (blocking) instead. |
|
|
83
|
+
| `morphdb dashboard` | Open a read-only web view of every app and its tables. |
|
|
84
|
+
| `morphdb install-skill` | Install the bundled Claude Code skill (below). |
|
|
85
|
+
| `morphdb --version` | Print the version. |
|
|
86
|
+
|
|
87
|
+
`start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
|
|
88
|
+
and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
|
|
89
|
+
`dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
|
|
90
|
+
state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
|
|
91
|
+
`$MORPHDB_HOME`.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
morphdb start # background, default 127.0.0.1:8787
|
|
95
|
+
morphdb start --port 9000 --db ./my.sqlite3
|
|
96
|
+
morphdb status # -> running (pid …) at http://… [healthy]
|
|
97
|
+
morphdb dashboard # opens http://127.0.0.1:8788
|
|
98
|
+
morphdb stop
|
|
99
|
+
morphdb run # foreground instead (Ctrl-C to quit)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Install the Claude Code skill
|
|
103
|
+
|
|
104
|
+
`install-skill` writes the bundled MorphDB skill into a Claude skills directory,
|
|
105
|
+
so a coding agent automatically reaches for MorphDB when building a data-backed
|
|
106
|
+
site:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
|
|
110
|
+
morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
|
|
111
|
+
morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
|
|
19
112
|
```
|
|
20
113
|
|
|
114
|
+
It installs the skill **bundled in the installed package** (not live from
|
|
115
|
+
GitHub) and is **idempotent** — re-running overwrites with the current version.
|
|
116
|
+
To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
|
|
117
|
+
Claude Code afterward to pick it up.
|
|
118
|
+
|
|
21
119
|
## Why
|
|
22
120
|
|
|
23
121
|
AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
|
|
@@ -31,6 +129,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
|
|
|
31
129
|
**no migration, no row rewrite, no downtime** — regardless of how much data
|
|
32
130
|
exists. Meanwhile the frontend talks to generic endpoints that never change.
|
|
33
131
|
|
|
132
|
+
```
|
|
133
|
+
you (the coding agent) the frontend you build
|
|
134
|
+
────────────────────── ──────────────────────
|
|
135
|
+
reshape the schema freely │ calls fixed generic endpoints
|
|
136
|
+
PUT /schema/{type} │ POST /objects/{type}
|
|
137
|
+
GET /schema │ GET /objects/{type}?field=…
|
|
138
|
+
DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
|
|
139
|
+
│ │
|
|
140
|
+
└────────────── MorphDB ───────────┘
|
|
141
|
+
(one process · many apps · SQLite)
|
|
142
|
+
every call: X-App-Key: <app>
|
|
143
|
+
```
|
|
144
|
+
|
|
34
145
|
## The shape of it
|
|
35
146
|
|
|
36
147
|
One MorphDB process hosts **many apps** (one per website), fully isolated from
|
|
@@ -93,58 +204,12 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
|
|
|
93
204
|
- **Query layer**: filter operators, sorting, pagination — all generic.
|
|
94
205
|
- **Multi-tenant by app** — one process backs many isolated sites; every call is scoped by an `X-App-Key`, and deleting an app cascades away all its data.
|
|
95
206
|
- **Wide-open CORS** so any frontend origin can call it in dev.
|
|
96
|
-
- **A
|
|
207
|
+
- **A management CLI** — `morphdb start/status/stop`, a read-only admin dashboard, and one-command skill install.
|
|
208
|
+
- **A Claude Code skill** (`morphdb/skill/SKILL.md`, install with `morphdb install-skill`) with a schema CLI so the agent edits the model without hand-writing curl.
|
|
97
209
|
|
|
98
210
|
> Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
|
|
99
211
|
> horizontal scale, or production durability guarantees.
|
|
100
212
|
|
|
101
|
-
## Install / run
|
|
102
|
-
|
|
103
|
-
No install required:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
python3 -m morphdb --port 8787 --db ./app.sqlite3
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Or install the console script:
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
pip install -e .
|
|
113
|
-
morphdb --port 8787 --db ./app.sqlite3
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Flags: `--host` (default `127.0.0.1`), `--port` (default `8787`),
|
|
117
|
-
`--db` (default `morphdb.sqlite3`; use `:memory:` for ephemeral).
|
|
118
|
-
|
|
119
|
-
Then: `curl http://127.0.0.1:8787/help` for a live reference.
|
|
120
|
-
|
|
121
|
-
## Quickstart
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
BASE=http://127.0.0.1:8787
|
|
125
|
-
|
|
126
|
-
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
127
|
-
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
128
|
-
H="X-App-Key: my-site"
|
|
129
|
-
|
|
130
|
-
# 1. define types + a relation
|
|
131
|
-
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
132
|
-
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
133
|
-
"fields": {"title":"string","done":"boolean","priority":"number"},
|
|
134
|
-
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
135
|
-
|
|
136
|
-
# 2. create + read + query
|
|
137
|
-
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
138
|
-
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
139
|
-
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
140
|
-
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
141
|
-
|
|
142
|
-
# 3. morph the schema later — existing rows just gain the new field as null
|
|
143
|
-
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
147
|
-
|
|
148
213
|
## Data model
|
|
149
214
|
|
|
150
215
|
| Concept | What it is |
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""MorphDB command-line interface — process management + admin dashboard.
|
|
2
|
+
|
|
3
|
+
Intentionally separate from the core engine (db / schema / objects / server):
|
|
4
|
+
this package only *orchestrates* a server process and offers a read-only admin
|
|
5
|
+
view. It never changes how the core stores or serves data.
|
|
6
|
+
|
|
7
|
+
Commands (see :mod:`morphdb.cli.main`):
|
|
8
|
+
|
|
9
|
+
morphdb start the server in the background (alias of `start`)
|
|
10
|
+
morphdb start same, explicit
|
|
11
|
+
morphdb status is it running? where? how many apps?
|
|
12
|
+
morphdb stop stop the background server
|
|
13
|
+
morphdb logs show the server log (-f to follow)
|
|
14
|
+
morphdb run run in the foreground (blocking; for dev)
|
|
15
|
+
morphdb dashboard open a read-only web view of every app + its tables
|
|
16
|
+
morphdb install-skill install/update the bundled Claude Code skill
|
|
17
|
+
|
|
18
|
+
Storage: the local server keeps data in a per-user SQLite file at
|
|
19
|
+
``~/.morphdb/data.sqlite3`` (override the file with ``--db``, or move the state
|
|
20
|
+
dir with ``$MORPHDB_HOME``). To talk to a MorphDB hosted somewhere else instead
|
|
21
|
+
of a local one, point *clients* at it with ``$MORPHDB_HOST`` (a full URL) — that
|
|
22
|
+
is a client-side setting, not a database connection string; the engine is always
|
|
23
|
+
SQLite.
|
|
24
|
+
"""
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Read-only admin dashboard: every app and its tables, in one local page.
|
|
2
|
+
|
|
3
|
+
Operator-facing and local-only — it opens the SQLite file directly (read-only)
|
|
4
|
+
rather than going through the HTTP API, so it can list apps without adding a
|
|
5
|
+
"list apps" endpoint to the public surface (which is intentionally absent).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import html
|
|
9
|
+
import json
|
|
10
|
+
import sqlite3
|
|
11
|
+
import webbrowser
|
|
12
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def gather(db):
|
|
16
|
+
"""A read-only snapshot: {apps: [{app, types:[{name,fields,count}], relations, edges}]}.
|
|
17
|
+
|
|
18
|
+
Tolerates a missing/empty/locked database by returning an ``error`` string.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=2)
|
|
22
|
+
c.row_factory = sqlite3.Row
|
|
23
|
+
except Exception as e:
|
|
24
|
+
return {"error": f"cannot open database: {e}", "apps": []}
|
|
25
|
+
try:
|
|
26
|
+
try:
|
|
27
|
+
apps = [r["key"] for r in c.execute("SELECT key FROM apps ORDER BY key")]
|
|
28
|
+
except sqlite3.OperationalError:
|
|
29
|
+
return {"error": "no MorphDB schema in this database yet", "apps": []}
|
|
30
|
+
out = []
|
|
31
|
+
for app in apps:
|
|
32
|
+
types = []
|
|
33
|
+
for r in c.execute(
|
|
34
|
+
"SELECT name, fields FROM object_schemas WHERE app=? ORDER BY name",
|
|
35
|
+
(app,)):
|
|
36
|
+
try:
|
|
37
|
+
fields = list(json.loads(r["fields"]).keys())
|
|
38
|
+
except Exception:
|
|
39
|
+
fields = []
|
|
40
|
+
count = c.execute(
|
|
41
|
+
"SELECT COUNT(*) FROM objects WHERE app=? AND object_type=?",
|
|
42
|
+
(app, r["name"])).fetchone()[0]
|
|
43
|
+
types.append({"name": r["name"], "fields": fields, "count": count})
|
|
44
|
+
relations = c.execute(
|
|
45
|
+
"SELECT COUNT(*) FROM association_schemas WHERE app=?", (app,)).fetchone()[0]
|
|
46
|
+
edges = c.execute(
|
|
47
|
+
"SELECT COUNT(*) FROM associations WHERE app=?", (app,)).fetchone()[0]
|
|
48
|
+
out.append({"app": app, "types": types,
|
|
49
|
+
"relations": relations, "edges": edges})
|
|
50
|
+
return {"apps": out}
|
|
51
|
+
finally:
|
|
52
|
+
c.close()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_CSS = """
|
|
56
|
+
:root { color-scheme: light dark; }
|
|
57
|
+
body { font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
58
|
+
max-width: 900px; margin: 32px auto; padding: 0 16px; }
|
|
59
|
+
header { display:flex; align-items:baseline; justify-content:space-between; gap:12px;
|
|
60
|
+
border-bottom:1px solid #8884; padding-bottom:12px; margin-bottom:20px; }
|
|
61
|
+
h1 { font-size:1.3rem; margin:0; }
|
|
62
|
+
.db { color:#888; font-size:12px; font-family:ui-monospace,Menlo,monospace; }
|
|
63
|
+
.card { border:1px solid #8884; border-radius:10px; padding:14px 16px; margin-bottom:16px; }
|
|
64
|
+
.card h2 { font-size:1.05rem; margin:0 0 2px; }
|
|
65
|
+
.meta { color:#888; font-size:12px; margin-bottom:10px; }
|
|
66
|
+
table { width:100%; border-collapse:collapse; font-size:14px; }
|
|
67
|
+
th, td { text-align:left; padding:6px 8px; border-bottom:1px solid #8882; }
|
|
68
|
+
th { color:#888; font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
|
|
69
|
+
td.n { text-align:right; font-variant-numeric:tabular-nums; }
|
|
70
|
+
.muted { color:#999; } .err { color:#ef4444; }
|
|
71
|
+
code { font-family:ui-monospace,Menlo,monospace; background:#8881; padding:1px 5px; border-radius:5px; }
|
|
72
|
+
footer { color:#999; font-size:12px; margin-top:24px; }
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def render(data, db):
|
|
77
|
+
esc = html.escape
|
|
78
|
+
if data.get("error"):
|
|
79
|
+
body = f"<p class='err'>{esc(data['error'])}</p>"
|
|
80
|
+
elif not data["apps"]:
|
|
81
|
+
body = "<p class='muted'>No apps registered yet. Create one with <code>POST /app</code>.</p>"
|
|
82
|
+
else:
|
|
83
|
+
cards = []
|
|
84
|
+
for a in data["apps"]:
|
|
85
|
+
if a["types"]:
|
|
86
|
+
trows = "".join(
|
|
87
|
+
f"<tr><td><b>{esc(t['name'])}</b></td>"
|
|
88
|
+
f"<td>{esc(', '.join(t['fields']) or '—')}</td>"
|
|
89
|
+
f"<td class='n'>{t['count']}</td></tr>"
|
|
90
|
+
for t in a["types"])
|
|
91
|
+
else:
|
|
92
|
+
trows = "<tr><td colspan='3' class='muted'>no types yet</td></tr>"
|
|
93
|
+
cards.append(
|
|
94
|
+
f"<section class='card'><h2>{esc(a['app'])}</h2>"
|
|
95
|
+
f"<div class='meta'>{len(a['types'])} types · "
|
|
96
|
+
f"{a['relations']} relations · {a['edges']} edges</div>"
|
|
97
|
+
"<table><thead><tr><th>type</th><th>fields</th><th>objects</th></tr></thead>"
|
|
98
|
+
f"<tbody>{trows}</tbody></table></section>")
|
|
99
|
+
body = "\n".join(cards)
|
|
100
|
+
return (
|
|
101
|
+
"<!doctype html><html><head><meta charset='utf-8'>"
|
|
102
|
+
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
|
|
103
|
+
f"<title>MorphDB admin</title><style>{_CSS}</style></head><body>"
|
|
104
|
+
f"<header><h1>MorphDB admin</h1><div class='db'>{esc(str(db))}</div></header>"
|
|
105
|
+
f"{body}<footer>read-only view · refresh to update</footer></body></html>")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def serve(db, host="127.0.0.1", port=8788, open_browser=True):
|
|
109
|
+
class Handler(BaseHTTPRequestHandler):
|
|
110
|
+
def do_GET(self):
|
|
111
|
+
body = render(gather(db), db).encode("utf-8")
|
|
112
|
+
self.send_response(200)
|
|
113
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
114
|
+
self.send_header("Content-Length", str(len(body)))
|
|
115
|
+
self.end_headers()
|
|
116
|
+
self.wfile.write(body)
|
|
117
|
+
|
|
118
|
+
def log_message(self, *a):
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
httpd = ThreadingHTTPServer((host, port), Handler)
|
|
122
|
+
url = f"http://{host}:{port}"
|
|
123
|
+
print(f"MorphDB admin dashboard: {url}\n reading: {db}\n Ctrl-C to stop.")
|
|
124
|
+
if open_browser:
|
|
125
|
+
try:
|
|
126
|
+
webbrowser.open(url)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
try:
|
|
130
|
+
httpd.serve_forever()
|
|
131
|
+
except KeyboardInterrupt:
|
|
132
|
+
pass
|
|
133
|
+
finally:
|
|
134
|
+
httpd.server_close()
|