blogimport: Q-M4 live source — internal-data query adapter (75/75)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m5s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m5s
source.sx: live-source adapter resolving Q-M4 (internal-data query, not direct PG). Injected fetch-fn transport port (hexagonal seam); parse-row maps a blog post-row to the importer post dict and parses the :lexical JSON string via dream-json-parse. End-to-end drivers: backfill! (enumerate->fetch->import) and sync-verify (enumerate->fetch->verify), + backfill-ids! explicit-id fallback. Tests mock the transport against the documented response contract incl. a real lexical JSON string. README flags the one blog-side gap (add a published-posts enumeration query) + production fetch_data wiring (lives in lib/host). source 20/20; total 75/75. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
92
lib/blogimport/source.sx
Normal file
92
lib/blogimport/source.sx
Normal file
@@ -0,0 +1,92 @@
|
||||
; lib/blogimport/source.sx
|
||||
; Live source adapter — Q-M4 RESOLVED: import via the blog INTERNAL-DATA QUERY
|
||||
; surface (decoupled), not direct Postgres. Reuses the existing query contracts
|
||||
; (blog/queries.sx: post-by-id/post-by-slug/posts-by-ids) and keeps the importer in
|
||||
; the SX/host world (plans/migration/data-migration.md §7 recommended default).
|
||||
;
|
||||
; TRANSPORT SEAM (hexagonal, like every other subsystem): a `fetch-fn` port is
|
||||
; INJECTED. Contract:
|
||||
; (fetch-fn query-name params-dict) -> response-data
|
||||
; In production `fetch-fn` is the host's HMAC-signed fetch_data wrapper
|
||||
; (GET /internal/data/{query}); in tests it's a mock. The importer never knows how
|
||||
; the bytes arrive.
|
||||
;
|
||||
; RESPONSE CONTRACT (one published-post row), the blog `get-post-by-*` data handler:
|
||||
; {:uuid|:id :slug :title :status :visibility :tags :authors :lexical}
|
||||
; :lexical is the Ghost body as a JSON STRING (the Post.lexical DB column) — parsed
|
||||
; here with dream-json-parse into the SX dict shape blogimport/lex-blocks expects.
|
||||
; (If a handler returns :lexical already-structured, it is used as-is.)
|
||||
;
|
||||
; REQUIRED BLOG-SIDE ADDITION (the one gap): blog/queries.sx exposes fetch-by-id/slug
|
||||
; but NO enumeration query. The corpus (Q-D2 = every published post) needs a
|
||||
; `published-posts` query returning the published ids/slugs (Python: list_posts(
|
||||
; status="published"), blog/bp/blog/ghost_db.py:102). Flagged for the blog app; mocked
|
||||
; in tests. Until it exists, callers can pass an explicit id list to backfill-ids!.
|
||||
|
||||
(define blogimport/dep-json-parse dream-json-parse)
|
||||
|
||||
; --- lexical field -> SX dict (string from DB column, or already structured) -----
|
||||
(define
|
||||
blogimport/parse-lexical
|
||||
(fn (lx)
|
||||
(cond
|
||||
((equal? lx nil) {:root {:children (list)}})
|
||||
((string? lx) (blogimport/dep-json-parse lx))
|
||||
(else lx))))
|
||||
|
||||
; --- service post-row -> importer `post` dict -----------------------------------
|
||||
(define
|
||||
blogimport/parse-row
|
||||
(fn (row)
|
||||
{:id (or (get row :uuid) (get row :id))
|
||||
:slug (or (get row :slug) "")
|
||||
:title (or (get row :title) "")
|
||||
:status (or (get row :status) "")
|
||||
:visibility (or (get row :visibility) "")
|
||||
:tags (or (get row :tags) (list))
|
||||
:authors (or (get row :authors) (list))
|
||||
:lexical (blogimport/parse-lexical (get row :lexical))}))
|
||||
|
||||
; --- fetch one post via an internal-data query ----------------------------------
|
||||
(define
|
||||
blogimport/fetch-post
|
||||
(fn (fetch-fn query params)
|
||||
(blogimport/parse-row (fetch-fn query params))))
|
||||
|
||||
; --- enumerate published post ids (needs the `published-posts` query) -----------
|
||||
(define
|
||||
blogimport/published-ids
|
||||
(fn (fetch-fn) (fetch-fn "published-posts" {})))
|
||||
|
||||
; --- fetch all published posts as importer `post` dicts -------------------------
|
||||
(define
|
||||
blogimport/source-posts
|
||||
(fn (fetch-fn)
|
||||
(map
|
||||
(fn (id) (blogimport/fetch-post fetch-fn "post-by-id" {:id id}))
|
||||
(blogimport/published-ids fetch-fn))))
|
||||
|
||||
; --- fetch an explicit id list (fallback before the enumeration query lands) ----
|
||||
(define
|
||||
blogimport/source-posts-by-ids
|
||||
(fn (fetch-fn ids)
|
||||
(map (fn (id) (blogimport/fetch-post fetch-fn "post-by-id" {:id id})) ids)))
|
||||
|
||||
; --- end-to-end drivers ---------------------------------------------------------
|
||||
; backfill = enumerate -> fetch -> genesis-import (idempotent). Re-runnable as the
|
||||
; one-way DB->persist sync (data-migration.md Strategy 1).
|
||||
(define
|
||||
blogimport/backfill!
|
||||
(fn (b fetch-fn at)
|
||||
(blogimport/import-all! b (blogimport/source-posts fetch-fn) at)))
|
||||
|
||||
(define
|
||||
blogimport/backfill-ids!
|
||||
(fn (b fetch-fn ids at)
|
||||
(blogimport/import-all! b (blogimport/source-posts-by-ids fetch-fn ids) at)))
|
||||
|
||||
; sync-verify = enumerate -> fetch -> shadow-diff the persisted streams at rest.
|
||||
(define
|
||||
blogimport/sync-verify
|
||||
(fn (b fetch-fn)
|
||||
(blogimport/verify-all b (blogimport/source-posts fetch-fn))))
|
||||
Reference in New Issue
Block a user