diff --git a/system/backend/kidlisp-read.mjs b/system/backend/kidlisp-read.mjs new file mode 100644 index 0000000000..106228b8f2 --- /dev/null +++ b/system/backend/kidlisp-read.mjs @@ -0,0 +1,44 @@ +// kidlisp-read.mjs — Datomic-aware piece lookup. +// +// When KIDLISP_DATOMIC=on, new pieces land in Datomic via store-kidlisp +// (routed through store-kidlisp-datomic.mjs), NOT in MongoDB. Callers like +// keep-prepare that still read Mongo directly miss those new pieces and +// return "Piece '$code' not found" for freshly-created work. +// +// This helper mirrors the store-kidlisp pattern: prefer the sidecar when +// Datomic is enabled (authoritative source for identity + source), then +// overlay the MongoDB doc if present so caller-specific fields like +// ipfsMedia, mediaHistory, kept, tezos, and pendingRebake — which still +// live in Mongo — continue to work for rebake / sync flows. + +import { sidecar, kidlispDatomicEnabled } from "./kidlisp-sidecar.mjs"; + +/** + * Fetch a kidlisp piece by its short code. + * + * @param {object} database - { db } from backend/database.mjs :: connect(). + * @param {string} code - Short piece code without the leading `$`. + * @returns {Promise} Piece doc or null. + */ +export async function loadKidlispPiece(database, code) { + if (!code) return null; + const collection = database.db.collection("kidlisp"); + + if (kidlispDatomicEnabled()) { + try { + const datomic = await sidecar.lookupByCode(code); + if (datomic) { + // Authoritative fields come from Datomic. Merge any MongoDB row on + // top so rebake-era fields (ipfsMedia, kept, tezos, pendingRebake, + // mediaHistory) keep flowing through. Missing Mongo row just means + // this is a brand-new piece that has never been kept yet. + const mongo = await collection.findOne({ code }); + return mongo ? { ...datomic, ...mongo } : { ...datomic }; + } + } catch (err) { + console.warn(`kidlisp-read: sidecar lookupByCode(${code}) failed — falling back to Mongo:`, err?.message || err); + } + } + + return collection.findOne({ code }); +} diff --git a/system/netlify/functions/keep-confirm.mjs b/system/netlify/functions/keep-confirm.mjs index 293d6f01ed..e7d390daa1 100644 --- a/system/netlify/functions/keep-confirm.mjs +++ b/system/netlify/functions/keep-confirm.mjs @@ -7,6 +7,7 @@ import { authorize } from "../../backend/authorization.mjs"; import { connect } from "../../backend/database.mjs"; import { respond } from "../../backend/http.mjs"; +import { loadKidlispPiece } from "../../backend/kidlisp-read.mjs"; import { getKeepsContractAddress, LEGACY_KEEPS_CONTRACT } from "../../backend/tezos-keeps-contract.mjs"; import { mirrorRecordMint } from "../../backend/kidlisp-dual-write.mjs"; @@ -240,9 +241,9 @@ export async function handler(event, context) { requestedVersion: contractVersion, }); - // Find the kidlisp record + // Find the kidlisp record (Datomic-aware; new pieces live only in Datomic) const collection = database.db.collection("kidlisp"); - const record = await collection.findOne({ code: cleanPiece }); + const record = await loadKidlispPiece(database, cleanPiece); if (!record) { await database.disconnect(); @@ -339,15 +340,19 @@ export async function handler(event, context) { unsetOps.pendingKeep = ""; } + // upsert: pieces that live only in Datomic (KIDLISP_DATOMIC=on) don't + // yet have a Mongo row on first keep. Writing on upsert creates one so + // ipfsMedia / kept persist. const updateResult = await collection.updateOne( { code: cleanPiece }, { $set: setOps, ...(Object.keys(unsetOps).length > 0 ? { $unset: unsetOps } : {}), - } + }, + { upsert: true } ); - if (updateResult.modifiedCount === 0 && updateResult.matchedCount === 0) { + if (updateResult.modifiedCount === 0 && updateResult.matchedCount === 0 && !updateResult.upsertedId) { console.warn(`❌ Failed to update piece ${cleanPiece}`); await database.disconnect(); return respond(500, { error: "Failed to record mint" }); diff --git a/system/netlify/functions/keep-prepare-background.mjs b/system/netlify/functions/keep-prepare-background.mjs index f711666fe6..dfa22bbc00 100644 --- a/system/netlify/functions/keep-prepare-background.mjs +++ b/system/netlify/functions/keep-prepare-background.mjs @@ -7,6 +7,7 @@ // Invoked by keep-prepare.mjs after job creation and validation. import { connect } from "../../backend/database.mjs"; +import { loadKidlispPiece } from "../../backend/kidlisp-read.mjs"; import { analyzeKidLisp } from "../../backend/kidlisp-analyzer.mjs"; import { getKeepsContractAddress, LEGACY_KEEPS_CONTRACT } from "../../backend/tezos-keeps-contract.mjs"; import { handleFor } from "../../backend/authorization.mjs"; @@ -527,7 +528,9 @@ async function runPipeline({ jobId, pieceName, isRebake, regenerate, creatorWall contractVersion = VERSION_BY_PROFILE[contractProfile] || null; } const col = database.db.collection("kidlisp"); - const piece = await col.findOne({ code: pieceName }); + // Datomic-aware read so new pieces (which only live in Datomic when + // KIDLISP_DATOMIC=on) are visible to the bake pipeline. + const piece = await loadKidlispPiece(database, pieceName); if (!piece) { await markJobFailed(jobId, `Piece '$${pieceName}' not found`, "validate"); @@ -738,7 +741,10 @@ async function runPipeline({ jobId, pieceName, isRebake, regenerate, creatorWall }, }; } - await col.updateOne({ code: pieceName }, updateOps); + // upsert: pieces that live only in Datomic don't have a Mongo row yet. + // Write one on first bake so ipfsMedia / mediaHistory / kept can land + // and subsequent keep operations (rebake, confirm, sync) work. + await col.updateOne({ code: pieceName }, updateOps, { upsert: true }); await mirrorIpfsMedia(pieceName, updateOps.$set.ipfsMedia); } @@ -758,7 +764,8 @@ async function runPipeline({ jobId, pieceName, isRebake, regenerate, creatorWall }; await col.updateOne( { code: pieceName }, - { $set: { pendingRebake: rebakePayload } } + { $set: { pendingRebake: rebakePayload } }, + { upsert: true } ); await mirrorPendingRebake(pieceName, rebakePayload); const mintStatus = await checkMintStatus(pieceName, CONTRACT_ADDRESS); diff --git a/system/netlify/functions/keep-prepare.mjs b/system/netlify/functions/keep-prepare.mjs index c6c62af1e4..9d002ef6b7 100644 --- a/system/netlify/functions/keep-prepare.mjs +++ b/system/netlify/functions/keep-prepare.mjs @@ -11,6 +11,7 @@ import { authorize, handleFor, hasAdmin } from "../../backend/authorization.mjs"; import { connect } from "../../backend/database.mjs"; +import { loadKidlispPiece } from "../../backend/kidlisp-read.mjs"; import { getKeepsContractAddress, LEGACY_KEEPS_CONTRACT } from "../../backend/tezos-keeps-contract.mjs"; import { upsertJob, @@ -94,10 +95,9 @@ export const handler = async (event) => { // ── Auth ──────────────────────────────────────────────────────────── const user = await authorize(event.headers); - // ── Load piece from DB ───────────────────────────────────────────── + // ── Load piece from DB (Datomic-aware; new pieces live only in Datomic) ─ const database = await connect(); - const collection = database.db.collection("kidlisp"); - const piece = await collection.findOne({ code: pieceName }); + const piece = await loadKidlispPiece(database, pieceName); if (!piece) return jsonResponse(404, { error: `Piece '$${pieceName}' not found` }); // ── Check mint status ────────────────────────────────────────────── diff --git a/system/netlify/functions/keep-update-confirm.mjs b/system/netlify/functions/keep-update-confirm.mjs index d1a571959c..e7d06308fe 100644 --- a/system/netlify/functions/keep-update-confirm.mjs +++ b/system/netlify/functions/keep-update-confirm.mjs @@ -7,6 +7,7 @@ import { authorize, hasAdmin } from "../../backend/authorization.mjs"; import { connect } from "../../backend/database.mjs"; import { respond } from "../../backend/http.mjs"; +import { loadKidlispPiece } from "../../backend/kidlisp-read.mjs"; import { getKeepsContractAddress, LEGACY_KEEPS_CONTRACT } from "../../backend/tezos-keeps-contract.mjs"; import { mirrorRecordMint } from "../../backend/kidlisp-dual-write.mjs"; @@ -179,9 +180,9 @@ export async function handler(event, context) { }); const normalizedTokenId = normalizeTokenId(tokenId); - // Find the kidlisp record + // Find the kidlisp record (Datomic-aware) const collection = database.db.collection("kidlisp"); - const record = await collection.findOne({ code: cleanPiece }); + const record = await loadKidlispPiece(database, cleanPiece); if (!record) { await database.disconnect(); @@ -232,10 +233,11 @@ export async function handler(event, context) { [`tezos.contracts.${effectiveContract}.pendingArtifactUri`]: "", [`tezos.contracts.${effectiveContract}.pendingThumbnailUri`]: "", } - } + }, + { upsert: true } ); - if (updateResult.modifiedCount === 0 && updateResult.matchedCount === 0) { + if (updateResult.modifiedCount === 0 && updateResult.matchedCount === 0 && !updateResult.upsertedId) { console.warn(`❌ Failed to update piece ${cleanPiece}`); await database.disconnect(); return respond(500, { error: "Failed to record update" }); diff --git a/system/netlify/functions/keep-update.mjs b/system/netlify/functions/keep-update.mjs index 6323312944..e0000e591b 100644 --- a/system/netlify/functions/keep-update.mjs +++ b/system/netlify/functions/keep-update.mjs @@ -8,6 +8,7 @@ import { authorize, hasAdmin } from "../../backend/authorization.mjs"; import { connect } from "../../backend/database.mjs"; +import { loadKidlispPiece } from "../../backend/kidlisp-read.mjs"; import { getKeepsContractAddress, LEGACY_KEEPS_CONTRACT } from "../../backend/tezos-keeps-contract.mjs"; import { mirrorRecordMint } from "../../backend/kidlisp-dual-write.mjs"; import { stream } from "@netlify/functions"; @@ -218,10 +219,10 @@ export const handler = stream(async (event) => { const CONTRACT_ADDRESS = await getKeepsContractAddress({ network: NETWORK, fallback: LEGACY_KEEPS_CONTRACT }); console.log(`🪙 KEEP-UPDATE: Using contract ${CONTRACT_ADDRESS}`); - // Get piece data from database first + // Get piece data from database (Datomic-aware) database = await connect(); const collection = database.db.collection("kidlisp"); - const pieceDoc = await collection.findOne({ code: pieceName }); + const pieceDoc = await loadKidlispPiece(database, pieceName); if (!pieceDoc) { await send("error", { error: `Piece '$${pieceName}' not found` });