morphdb 0.1.3__tar.gz → 0.1.4__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.
Files changed (44) hide show
  1. {morphdb-0.1.3 → morphdb-0.1.4}/PKG-INFO +57 -8
  2. {morphdb-0.1.3 → morphdb-0.1.4}/README.md +56 -7
  3. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/__init__.py +1 -1
  4. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/cli/__init__.py +7 -0
  5. morphdb-0.1.4/morphdb/cli/dashboard.py +879 -0
  6. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/cli/main.py +33 -0
  7. morphdb-0.1.4/morphdb/cli/mcp.py +519 -0
  8. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/db.py +31 -0
  9. morphdb-0.1.4/morphdb/fieldindex.py +204 -0
  10. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/fieldtypes.py +13 -3
  11. morphdb-0.1.4/morphdb/objects.py +667 -0
  12. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/routes.py +13 -7
  13. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/schema.py +19 -0
  14. morphdb-0.1.4/morphdb/skill/SKILL.md +311 -0
  15. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb.egg-info/PKG-INFO +57 -8
  16. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb.egg-info/SOURCES.txt +5 -0
  17. {morphdb-0.1.3 → morphdb-0.1.4}/pyproject.toml +1 -1
  18. {morphdb-0.1.3 → morphdb-0.1.4}/tests/test_cli.py +2 -1
  19. {morphdb-0.1.3 → morphdb-0.1.4}/tests/test_core.py +5 -3
  20. morphdb-0.1.4/tests/test_field_index.py +341 -0
  21. {morphdb-0.1.3 → morphdb-0.1.4}/tests/test_hardening.py +1 -1
  22. morphdb-0.1.4/tests/test_includes.py +99 -0
  23. morphdb-0.1.4/tests/test_mcp.py +447 -0
  24. {morphdb-0.1.3 → morphdb-0.1.4}/tests/test_relations.py +136 -1
  25. morphdb-0.1.3/morphdb/cli/dashboard.py +0 -134
  26. morphdb-0.1.3/morphdb/objects.py +0 -395
  27. morphdb-0.1.3/morphdb/skill/SKILL.md +0 -259
  28. {morphdb-0.1.3 → morphdb-0.1.4}/LICENSE +0 -0
  29. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/__main__.py +0 -0
  30. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/apps.py +0 -0
  31. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/associations.py +0 -0
  32. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/cli/service.py +0 -0
  33. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/cli/skill.py +0 -0
  34. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/errors.py +0 -0
  35. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/router.py +0 -0
  36. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/server.py +0 -0
  37. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/skill/scripts/morphdb_schema.py +0 -0
  38. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb/util.py +0 -0
  39. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb.egg-info/dependency_links.txt +0 -0
  40. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb.egg-info/entry_points.txt +0 -0
  41. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb.egg-info/requires.txt +0 -0
  42. {morphdb-0.1.3 → morphdb-0.1.4}/morphdb.egg-info/top_level.txt +0 -0
  43. {morphdb-0.1.3 → morphdb-0.1.4}/setup.cfg +0 -0
  44. {morphdb-0.1.3 → morphdb-0.1.4}/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
3
+ Version: 0.1.4
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","done":"boolean","priority":"number"},
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
- Append `__op` to a field name: `eq` (default), `ne`, `gt`, `gte`, `lt`, `lte`,
325
- `contains` (substring), `in` (comma-separated), `exists` (`true`/`false`).
326
- Filtering is on **fields**, not relations.
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=_created_at&order=desc&limit=50
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-only.** Query operators apply to raw fields; relations
368
- are read/written on the object body but not filtered server-side (yet).
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","done":"boolean","priority":"number"},
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
- Append `__op` to a field name: `eq` (default), `ne`, `gt`, `gte`, `lt`, `lte`,
298
- `contains` (substring), `in` (comma-separated), `exists` (`true`/`false`).
299
- Filtering is on **fields**, not relations.
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=_created_at&order=desc&limit=50
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-only.** Query operators apply to raw fields; relations
341
- are read/written on the object body but not filtered server-side (yet).
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
@@ -5,4 +5,4 @@ coding agent iterates, while the frontend keeps calling the same small set of
5
5
  generic, deterministic endpoints.
6
6
  """
7
7
 
8
- __version__ = "0.1.3"
8
+ __version__ = "0.1.4"
@@ -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