feat: wire BuildPulse to Appwrite-backed persistence
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
const endpoint = process.env.APPWRITE_SELF_HOSTED_URL || 'https://app.friborg.uk/v1'
|
||||
const projectId = process.env.APPWRITE_SELF_HOSTED_PROJECT_ID || 'freecastle'
|
||||
|
||||
export const BUILDPULSE_DATABASE_ID = process.env.BUILDPULSE_APPWRITE_DATABASE_ID || process.env.APPWRITE_SELF_HOSTED_DATABASE_ID || 'freecastle'
|
||||
export const BUILDPULSE_COLLECTION_ID = process.env.BUILDPULSE_APPWRITE_COLLECTION_ID || 'runtime'
|
||||
export const BUILDPULSE_DOCUMENT_ID = process.env.BUILDPULSE_APPWRITE_DOCUMENT_ID || 'buildpulse_state'
|
||||
export const BUILDPULSE_DOCUMENT_KEY = process.env.BUILDPULSE_APPWRITE_DOCUMENT_KEY || 'buildpulse_state'
|
||||
|
||||
const baseHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Appwrite-Project': projectId,
|
||||
}
|
||||
|
||||
const documentUrl = `${endpoint}/databases/${BUILDPULSE_DATABASE_ID}/collections/${BUILDPULSE_COLLECTION_ID}/documents/${BUILDPULSE_DOCUMENT_ID}`
|
||||
const collectionUrl = `${endpoint}/databases/${BUILDPULSE_DATABASE_ID}/collections/${BUILDPULSE_COLLECTION_ID}/documents`
|
||||
|
||||
async function appwriteFetch(url, init = {}) {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
...baseHeaders,
|
||||
...(init.headers || {}),
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 204) return null
|
||||
|
||||
const text = await response.text()
|
||||
const payload = text ? JSON.parse(text) : null
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(payload?.message || `Appwrite request failed with ${response.status}`)
|
||||
error.status = response.status
|
||||
error.payload = payload
|
||||
throw error
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export async function fetchStoredAppState() {
|
||||
try {
|
||||
const document = await appwriteFetch(documentUrl)
|
||||
return document?.value ? JSON.parse(document.value) : null
|
||||
} catch (error) {
|
||||
if (error?.status === 404) return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function persistAppState(state) {
|
||||
const body = {
|
||||
data: {
|
||||
key: BUILDPULSE_DOCUMENT_KEY,
|
||||
value: JSON.stringify(state),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
return await appwriteFetch(documentUrl, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
} catch (error) {
|
||||
if (error?.status !== 404) throw error
|
||||
}
|
||||
|
||||
return appwriteFetch(collectionUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
documentId: BUILDPULSE_DOCUMENT_ID,
|
||||
...body,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkBackendHealth() {
|
||||
const document = await appwriteFetch(documentUrl).catch((error) => {
|
||||
if (error?.status === 404) return null
|
||||
throw error
|
||||
})
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
backend: 'appwrite',
|
||||
endpoint,
|
||||
databaseId: BUILDPULSE_DATABASE_ID,
|
||||
collectionId: BUILDPULSE_COLLECTION_ID,
|
||||
documentPresent: Boolean(document),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import express from 'express'
|
||||
import { checkBackendHealth, fetchStoredAppState, persistAppState } from './appwriteBackend.mjs'
|
||||
|
||||
const app = express()
|
||||
const port = Number(process.env.BUILDPULSE_API_PORT || 8788)
|
||||
|
||||
app.use(express.json({ limit: '2mb' }))
|
||||
|
||||
app.get('/api/health', async (_req, res) => {
|
||||
try {
|
||||
const health = await checkBackendHealth()
|
||||
res.json(health)
|
||||
} catch (error) {
|
||||
res.status(500).json({ ok: false, backend: 'appwrite', error: error?.message || 'Unknown backend error' })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/api/state', async (_req, res) => {
|
||||
try {
|
||||
const state = await fetchStoredAppState()
|
||||
res.json({ ok: true, backend: 'appwrite', state })
|
||||
} catch (error) {
|
||||
res.status(500).json({ ok: false, backend: 'appwrite', error: error?.message || 'Failed to fetch state' })
|
||||
}
|
||||
})
|
||||
|
||||
app.put('/api/state', async (req, res) => {
|
||||
const state = req.body?.state
|
||||
if (!state || typeof state !== 'object') {
|
||||
res.status(400).json({ ok: false, error: 'Request body must include a state object.' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await persistAppState(state)
|
||||
res.json({ ok: true, backend: 'appwrite' })
|
||||
} catch (error) {
|
||||
res.status(500).json({ ok: false, backend: 'appwrite', error: error?.message || 'Failed to persist state' })
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`BuildPulse API listening on http://127.0.0.1:${port}`)
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { BUILDPULSE_COLLECTION_ID, BUILDPULSE_DATABASE_ID, BUILDPULSE_DOCUMENT_ID, checkBackendHealth } from './appwriteBackend.mjs'
|
||||
|
||||
async function main() {
|
||||
const health = await checkBackendHealth()
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
...health,
|
||||
databaseId: BUILDPULSE_DATABASE_ID,
|
||||
collectionId: BUILDPULSE_COLLECTION_ID,
|
||||
documentId: BUILDPULSE_DOCUMENT_ID,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user