morphdb 0.1.3__tar.gz → 0.1.5__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.3 → morphdb-0.1.5}/PKG-INFO +57 -8
- {morphdb-0.1.3 → morphdb-0.1.5}/README.md +56 -7
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/__init__.py +1 -1
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/cli/__init__.py +7 -0
- morphdb-0.1.5/morphdb/cli/dashboard.py +879 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/cli/main.py +33 -0
- morphdb-0.1.5/morphdb/cli/mcp.py +519 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/db.py +31 -0
- morphdb-0.1.5/morphdb/fieldindex.py +204 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/fieldtypes.py +13 -3
- morphdb-0.1.5/morphdb/objects.py +667 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/routes.py +13 -7
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/schema.py +19 -0
- morphdb-0.1.5/morphdb/skill/SKILL.md +327 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/skill/scripts/morphdb_schema.py +12 -2
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb.egg-info/PKG-INFO +57 -8
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb.egg-info/SOURCES.txt +5 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/pyproject.toml +1 -1
- {morphdb-0.1.3 → morphdb-0.1.5}/tests/test_cli.py +2 -1
- {morphdb-0.1.3 → morphdb-0.1.5}/tests/test_core.py +5 -3
- morphdb-0.1.5/tests/test_field_index.py +341 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/tests/test_hardening.py +1 -1
- morphdb-0.1.5/tests/test_includes.py +99 -0
- morphdb-0.1.5/tests/test_mcp.py +447 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/tests/test_relations.py +136 -1
- morphdb-0.1.3/morphdb/cli/dashboard.py +0 -134
- morphdb-0.1.3/morphdb/objects.py +0 -395
- morphdb-0.1.3/morphdb/skill/SKILL.md +0 -259
- {morphdb-0.1.3 → morphdb-0.1.5}/LICENSE +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/__main__.py +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/apps.py +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/associations.py +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/cli/service.py +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/cli/skill.py +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/errors.py +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/router.py +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/server.py +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb/util.py +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb.egg-info/dependency_links.txt +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb.egg-info/entry_points.txt +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb.egg-info/requires.txt +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/morphdb.egg-info/top_level.txt +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/setup.cfg +0 -0
- {morphdb-0.1.3 → morphdb-0.1.5}/tests/test_apps.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.5
|
|
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
|
|
@@ -34,6 +34,8 @@ keeps calling the same small set of generic, deterministic endpoints. One
|
|
|
34
34
|
process hosts many isolated apps (one per site), zero dependencies, backed by
|
|
35
35
|
SQLite.
|
|
36
36
|
|
|
37
|
+
📖 **[Visual explainer → morphdb.pages.dev](https://morphdb.pages.dev)** — the whole idea (schema-fluid, API-stable), the agent/frontend split, relations, and how Claude plugs in over MCP, on one page.
|
|
38
|
+
|
|
37
39
|
## Install
|
|
38
40
|
|
|
39
41
|
```bash
|
|
@@ -79,7 +81,9 @@ H="X-App-Key: my-site"
|
|
|
79
81
|
# 1. define types + a relation
|
|
80
82
|
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
81
83
|
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
82
|
-
"fields": {"title":"string",
|
|
84
|
+
"fields": {"title":"string",
|
|
85
|
+
"done":{"type":"boolean","index":true},
|
|
86
|
+
"priority":{"type":"number","index":true}},
|
|
83
87
|
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
84
88
|
|
|
85
89
|
# 2. create + read + query
|
|
@@ -321,15 +325,58 @@ Every schema and object request must send the app key as the `X-App-Key` header
|
|
|
321
325
|
|
|
322
326
|
### Query operators
|
|
323
327
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
328
|
+
A field is filterable/sortable only if its schema marks it **`"index": true`**
|
|
329
|
+
(opt-in, default off). Filtering or sorting an un-indexed field returns a 400
|
|
330
|
+
telling you to index it; turning the flag on backfills existing objects
|
|
331
|
+
automatically, turning it off is instant. `json` fields can't be indexed.
|
|
332
|
+
|
|
333
|
+
Append `__op` to an **indexed field** name: `eq` (default), `ne`, `gt`, `gte`,
|
|
334
|
+
`lt`, `lte`, `contains` (substring), `in` (comma-separated), `exists`
|
|
335
|
+
(`true`/`false`).
|
|
327
336
|
|
|
328
337
|
```
|
|
338
|
+
# priority, title, done, status all declared with "index": true
|
|
329
339
|
GET /objects/task?priority__gte=3&title__contains=buy&done=false
|
|
330
|
-
GET /objects/task?status__in=open,blocked&sort=
|
|
340
|
+
GET /objects/task?status__in=open,blocked&sort=priority&order=desc&limit=50
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
You can also filter by a **relation** — treat it like an ORM foreign key, not a
|
|
344
|
+
manual join. Filtering by a relation matches objects linked to a given neighbor,
|
|
345
|
+
and resolves through the indexed edge table (so it is index-backed):
|
|
346
|
+
|
|
347
|
+
| Query | Meaning |
|
|
348
|
+
| --- | --- |
|
|
349
|
+
| `?assignee=<guid>` | objects whose `assignee` is / includes that neighbor |
|
|
350
|
+
| `?assignee__in=<g1>,<g2>` | linked to any of those neighbors |
|
|
351
|
+
| `?assignee__ne=<guid>` | not linked to that neighbor (includes unlinked) |
|
|
352
|
+
| `?assignee__exists=true` | has any `assignee` (`false` → has none) |
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
# "stages of business X that are still in build" — relation + field, one query
|
|
356
|
+
GET /objects/stage?business=<bizguid>&status=build&sort=_created_at
|
|
331
357
|
```
|
|
332
358
|
|
|
359
|
+
Relation filters compose with field filters, `sort`, and pagination. Scalar
|
|
360
|
+
comparisons (`gt`/`lt`/`contains`) are field-only; relations support
|
|
361
|
+
`eq`/`ne`/`in`/`exists`. So **model a foreign key as a relation, not a string
|
|
362
|
+
field** — you keep one-read traversal *and* get filtering, indexed, for free.
|
|
363
|
+
|
|
364
|
+
### Including related objects
|
|
365
|
+
|
|
366
|
+
By default a relation reads back as a guid (to-one) or list of guids (to-many).
|
|
367
|
+
Add `?include=` with comma-separated relation paths (dots nest) to hydrate them
|
|
368
|
+
into the full neighbor objects, nested Prisma-style:
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
GET /objects/post?include=author,comments,comments.author
|
|
372
|
+
# each post.author becomes a full user; post.comments a list of full comments,
|
|
373
|
+
# and each comment.author a full user too.
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Works on the list endpoint and both single-object reads. Read-only, depth ≤ 4,
|
|
377
|
+
and batched (one query per relation per level — no N+1). Writes stay flat: create
|
|
378
|
+
and update with guids, never nested objects.
|
|
379
|
+
|
|
333
380
|
## Errors
|
|
334
381
|
|
|
335
382
|
JSON shape: `{"error": {"code": "...", "message": "...", ...extra}}`.
|
|
@@ -364,8 +411,10 @@ allowed, `413` body too large, `500` internal.
|
|
|
364
411
|
old type simply reads as unset (the field's default, or null) until it's
|
|
365
412
|
written again; reads and queries apply this rule identically, so they always
|
|
366
413
|
agree. Re-adding a dropped field at the same type recovers its values.
|
|
367
|
-
- **Filtering is field
|
|
368
|
-
|
|
414
|
+
- **Filtering/sorting is opt-in per field.** Only a field marked `"index": true`
|
|
415
|
+
can be filtered or sorted (it gets a row in the indexed `field_index` table);
|
|
416
|
+
an un-indexed field is storage-only and a filter/sort on it is a 400. Relations
|
|
417
|
+
are always filterable via the indexed edge table — no flag needed.
|
|
369
418
|
- **Integer magnitude.** Numbers are stored and read back exactly at any size.
|
|
370
419
|
Filtering/sorting on integers beyond ±2⁶³ uses floating-point comparison (a
|
|
371
420
|
SQLite limitation), so equality/range queries on such huge integers may be
|
|
@@ -7,6 +7,8 @@ keeps calling the same small set of generic, deterministic endpoints. One
|
|
|
7
7
|
process hosts many isolated apps (one per site), zero dependencies, backed by
|
|
8
8
|
SQLite.
|
|
9
9
|
|
|
10
|
+
📖 **[Visual explainer → morphdb.pages.dev](https://morphdb.pages.dev)** — the whole idea (schema-fluid, API-stable), the agent/frontend split, relations, and how Claude plugs in over MCP, on one page.
|
|
11
|
+
|
|
10
12
|
## Install
|
|
11
13
|
|
|
12
14
|
```bash
|
|
@@ -52,7 +54,9 @@ H="X-App-Key: my-site"
|
|
|
52
54
|
# 1. define types + a relation
|
|
53
55
|
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
54
56
|
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
55
|
-
"fields": {"title":"string",
|
|
57
|
+
"fields": {"title":"string",
|
|
58
|
+
"done":{"type":"boolean","index":true},
|
|
59
|
+
"priority":{"type":"number","index":true}},
|
|
56
60
|
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
57
61
|
|
|
58
62
|
# 2. create + read + query
|
|
@@ -294,15 +298,58 @@ Every schema and object request must send the app key as the `X-App-Key` header
|
|
|
294
298
|
|
|
295
299
|
### Query operators
|
|
296
300
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
301
|
+
A field is filterable/sortable only if its schema marks it **`"index": true`**
|
|
302
|
+
(opt-in, default off). Filtering or sorting an un-indexed field returns a 400
|
|
303
|
+
telling you to index it; turning the flag on backfills existing objects
|
|
304
|
+
automatically, turning it off is instant. `json` fields can't be indexed.
|
|
305
|
+
|
|
306
|
+
Append `__op` to an **indexed field** name: `eq` (default), `ne`, `gt`, `gte`,
|
|
307
|
+
`lt`, `lte`, `contains` (substring), `in` (comma-separated), `exists`
|
|
308
|
+
(`true`/`false`).
|
|
300
309
|
|
|
301
310
|
```
|
|
311
|
+
# priority, title, done, status all declared with "index": true
|
|
302
312
|
GET /objects/task?priority__gte=3&title__contains=buy&done=false
|
|
303
|
-
GET /objects/task?status__in=open,blocked&sort=
|
|
313
|
+
GET /objects/task?status__in=open,blocked&sort=priority&order=desc&limit=50
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
You can also filter by a **relation** — treat it like an ORM foreign key, not a
|
|
317
|
+
manual join. Filtering by a relation matches objects linked to a given neighbor,
|
|
318
|
+
and resolves through the indexed edge table (so it is index-backed):
|
|
319
|
+
|
|
320
|
+
| Query | Meaning |
|
|
321
|
+
| --- | --- |
|
|
322
|
+
| `?assignee=<guid>` | objects whose `assignee` is / includes that neighbor |
|
|
323
|
+
| `?assignee__in=<g1>,<g2>` | linked to any of those neighbors |
|
|
324
|
+
| `?assignee__ne=<guid>` | not linked to that neighbor (includes unlinked) |
|
|
325
|
+
| `?assignee__exists=true` | has any `assignee` (`false` → has none) |
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
# "stages of business X that are still in build" — relation + field, one query
|
|
329
|
+
GET /objects/stage?business=<bizguid>&status=build&sort=_created_at
|
|
304
330
|
```
|
|
305
331
|
|
|
332
|
+
Relation filters compose with field filters, `sort`, and pagination. Scalar
|
|
333
|
+
comparisons (`gt`/`lt`/`contains`) are field-only; relations support
|
|
334
|
+
`eq`/`ne`/`in`/`exists`. So **model a foreign key as a relation, not a string
|
|
335
|
+
field** — you keep one-read traversal *and* get filtering, indexed, for free.
|
|
336
|
+
|
|
337
|
+
### Including related objects
|
|
338
|
+
|
|
339
|
+
By default a relation reads back as a guid (to-one) or list of guids (to-many).
|
|
340
|
+
Add `?include=` with comma-separated relation paths (dots nest) to hydrate them
|
|
341
|
+
into the full neighbor objects, nested Prisma-style:
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
GET /objects/post?include=author,comments,comments.author
|
|
345
|
+
# each post.author becomes a full user; post.comments a list of full comments,
|
|
346
|
+
# and each comment.author a full user too.
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Works on the list endpoint and both single-object reads. Read-only, depth ≤ 4,
|
|
350
|
+
and batched (one query per relation per level — no N+1). Writes stay flat: create
|
|
351
|
+
and update with guids, never nested objects.
|
|
352
|
+
|
|
306
353
|
## Errors
|
|
307
354
|
|
|
308
355
|
JSON shape: `{"error": {"code": "...", "message": "...", ...extra}}`.
|
|
@@ -337,8 +384,10 @@ allowed, `413` body too large, `500` internal.
|
|
|
337
384
|
old type simply reads as unset (the field's default, or null) until it's
|
|
338
385
|
written again; reads and queries apply this rule identically, so they always
|
|
339
386
|
agree. Re-adding a dropped field at the same type recovers its values.
|
|
340
|
-
- **Filtering is field
|
|
341
|
-
|
|
387
|
+
- **Filtering/sorting is opt-in per field.** Only a field marked `"index": true`
|
|
388
|
+
can be filtered or sorted (it gets a row in the indexed `field_index` table);
|
|
389
|
+
an un-indexed field is storage-only and a filter/sort on it is a 400. Relations
|
|
390
|
+
are always filterable via the indexed edge table — no flag needed.
|
|
342
391
|
- **Integer magnitude.** Numbers are stored and read back exactly at any size.
|
|
343
392
|
Filtering/sorting on integers beyond ±2⁶³ uses floating-point comparison (a
|
|
344
393
|
SQLite limitation), so equality/range queries on such huge integers may be
|
|
@@ -13,8 +13,15 @@ Commands (see :mod:`morphdb.cli.main`):
|
|
|
13
13
|
morphdb logs show the server log (-f to follow)
|
|
14
14
|
morphdb run run in the foreground (blocking; for dev)
|
|
15
15
|
morphdb dashboard open a read-only web view of every app + its tables
|
|
16
|
+
morphdb mcp run the MCP server (stdio; spawned by Claude Code, not you)
|
|
16
17
|
morphdb install-skill install/update the bundled Claude Code skill
|
|
17
18
|
|
|
19
|
+
The ``mcp`` command is a thin HTTP *client* of the running backend daemon (it
|
|
20
|
+
auto-starts the daemon if needed). It exposes schema + app operations to a coding
|
|
21
|
+
agent as MCP tools, so the agent calls real tools instead of shelling out to the
|
|
22
|
+
bundled schema script. It is pure stdlib — no MCP SDK — so the package stays
|
|
23
|
+
dependency-free.
|
|
24
|
+
|
|
18
25
|
Storage: the local server keeps data in a per-user SQLite file at
|
|
19
26
|
``~/.morphdb/data.sqlite3`` (override the file with ``--db``, or move the state
|
|
20
27
|
dir with ``$MORPHDB_HOME``). To talk to a MorphDB hosted somewhere else instead
|