; 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))))