203 lines
6.5 KiB
Markdown
203 lines
6.5 KiB
Markdown
# **File: COMMIT\_06\_WRITE\_FLOW.md**
|
|
|
|
## **Commit 6: Core Write-Through Cache API**
|
|
|
|
### **Objective**
|
|
|
|
Implement the POST /api/nodes route. This is the core "write-through cache" logic, which is the architectural foundation of the application. It must:
|
|
|
|
1. Authenticate the user via their SurrealDB JWT.
|
|
2. Retrieve their ATproto access token (from the encrypted cookie).
|
|
3. **Step 1 (Truth):** Publish the new node to their PDS using the com.ponderants.node lexicon.
|
|
4. **Step 2 (Cache):** Generate a gemini-embedding-001 vector from the node's body.
|
|
5. **Step 3 (Cache):** Write the node, its atp\_uri, and its embedding to our SurrealDB cache.
|
|
|
|
### **Implementation Specification**
|
|
|
|
**1\. Create lib/db.ts**
|
|
|
|
Create a helper file at /lib/db.ts for connecting to SurrealDB:
|
|
|
|
TypeScript
|
|
|
|
import { Surreal } from 'surrealdb.js';
|
|
|
|
const db \= new Surreal();
|
|
|
|
/\*\*
|
|
\* Connects to the SurrealDB instance.
|
|
\* @param {string} token \- The user's app-specific (SurrealDB) JWT.
|
|
\*/
|
|
export async function connectToDB(token: string) {
|
|
if (\!db.connected) {
|
|
await db.connect(process.env.SURREALDB\_URL\!);
|
|
}
|
|
|
|
// Authenticate as the user for this request.
|
|
// This enforces the row-level security (PERMISSIONS)
|
|
// defined in the schema for all subsequent queries.
|
|
await db.authenticate(token);
|
|
|
|
return db;
|
|
}
|
|
|
|
**2\. Create lib/ai.ts**
|
|
|
|
Create a helper file at /lib/ai.ts for AI operations:
|
|
|
|
TypeScript
|
|
|
|
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
|
|
const genAI \= new GoogleGenerativeAI(process.env.GOOGLE\_API\_KEY\!);
|
|
|
|
const embeddingModel \= genAI.getGenerativeModel({
|
|
model: 'gemini-embedding-001',
|
|
});
|
|
|
|
/\*\*
|
|
\* Generates a vector embedding for a given text.
|
|
\* @param text The text to embed.
|
|
\* @returns A 1536-dimension vector (Array\<number\>).
|
|
\*/
|
|
export async function generateEmbedding(text: string): Promise\<number\> {
|
|
try {
|
|
const result \= await embeddingModel.embedContent(text);
|
|
return result.embedding.values;
|
|
} catch (error) {
|
|
console.error('Error generating embedding:', error);
|
|
throw new Error('Failed to generate AI embedding.');
|
|
}
|
|
}
|
|
|
|
**3\. Create Write API Route (app/api/nodes/route.ts)**
|
|
|
|
Create the main API file at /app/api/nodes/route.ts:
|
|
|
|
TypeScript
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { cookies } from 'next/headers';
|
|
import { AtpAgent, RichText } from '@atproto/api';
|
|
import { connectToDB } from '@/lib/db';
|
|
import { generateEmbedding } from '@/lib/ai';
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const surrealJwt \= cookies().get('ponderants-auth')?.value;
|
|
const atpAccessToken \= cookies().get('atproto\_access\_token')?.value;
|
|
|
|
if (\!surrealJwt ||\!atpAccessToken) {
|
|
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
|
}
|
|
|
|
let userDid: string;
|
|
try {
|
|
// Decode the JWT to get the DID for the SurrealDB query
|
|
// In a real app, we'd verify it, but for now we just
|
|
// pass it to connectToDB which authenticates with it.
|
|
const { payload } \= jwt.decode(surrealJwt, { complete: true })\!;
|
|
userDid \= (payload as { did: string }).did;
|
|
} catch (e) {
|
|
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
|
|
}
|
|
|
|
const { title, body, links } \= (await request.json()) as {
|
|
title: string;
|
|
body: string;
|
|
links: string; // Array of at-uri strings
|
|
};
|
|
|
|
if (\!title ||\!body) {
|
|
return NextResponse.json({ error: 'Title and body are required' }, { status: 400 });
|
|
}
|
|
|
|
const createdAt \= new Date().toISOString();
|
|
|
|
// \--- Step 1: Write to Source of Truth (ATproto) \---
|
|
let atp\_uri: string;
|
|
let atp\_cid: string;
|
|
|
|
try {
|
|
const agent \= new AtpAgent({ service: 'https://bsky.social' }); // The service URL may need to be dynamic
|
|
await agent.resumeSession({ accessJwt: atpAccessToken, did: userDid, handle: '' }); // Simplified resume
|
|
|
|
// Format the body as RichText
|
|
const rt \= new RichText({ text: body });
|
|
await rt.detectFacets(agent); // Detect links, mentions
|
|
|
|
const response \= await agent.post({
|
|
$type: 'com.ponderants.node',
|
|
repo: userDid,
|
|
collection: 'com.ponderants.node',
|
|
record: {
|
|
title,
|
|
body: rt.text,
|
|
facets: rt.facets, // Include facets for rich text
|
|
links: links?.map(uri \=\> ({ $link: uri })) ||, // Convert URIs to strong refs
|
|
createdAt,
|
|
},
|
|
});
|
|
|
|
atp\_uri \= response.uri;
|
|
atp\_cid \= response.cid;
|
|
|
|
} catch (error) {
|
|
console.error('ATproto write error:', error);
|
|
return NextResponse.json({ error: 'Failed to publish to PDS' }, { status: 500 });
|
|
}
|
|
|
|
// \--- Step 2: Generate AI Embedding (Cache) \---
|
|
let embedding: number;
|
|
try {
|
|
embedding \= await generateEmbedding(title \+ '\\n' \+ body);
|
|
} catch (error) {
|
|
console.error('Embedding error:', error);
|
|
return NextResponse.json({ error: 'Failed to generate embedding' }, { status: 500 });
|
|
}
|
|
|
|
// \--- Step 3: Write to App View Cache (SurrealDB) \---
|
|
try {
|
|
const db \= await connectToDB(surrealJwt);
|
|
|
|
// Create the node record in our cache.
|
|
// The \`user\_did\` field is set, satisfying the 'PERMISSIONS'
|
|
// clause defined in the schema.
|
|
const newNode \= await db.create('node', {
|
|
user\_did: userDid,
|
|
atp\_uri: atp\_uri,
|
|
title: title,
|
|
body: body, // Store the raw text body
|
|
embedding: embedding,
|
|
// coords\_3d will be calculated later
|
|
});
|
|
|
|
// Handle linking
|
|
if (links && links.length \> 0) {
|
|
// Find the corresponding cache nodes for the AT-URIs
|
|
const targetNodes: { id: string } \= await db.query(
|
|
'SELECT id FROM node WHERE user\_did \= $did AND atp\_uri IN $links',
|
|
{ did: userDid, links: links }
|
|
);
|
|
|
|
// Create graph relations
|
|
for (const targetNode of targetNodes) {
|
|
await db.query('RELATE $from-\>links\_to-\>$to', {
|
|
from: (newNode as any).id,
|
|
to: targetNode.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
return NextResponse.json(newNode);
|
|
|
|
} catch (error) {
|
|
console.error('SurrealDB write error:', error);
|
|
// TODO: Implement rollback for the ATproto post?
|
|
return NextResponse.json({ error: 'Failed to save to app cache' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
### **Test Specification**
|
|
|
|
This is an API-only commit. It will be tested via the end-to-end flow in **Commit 10 (Linking)**, which will provide the UI (the "Publish" button) to trigger this route.
|