Skip to content

Commit 424d632

Browse files
just-be-devclaude
andcommitted
feat(micro): migrate storage from R2 JSONL to D1
Replaces the R2-based JSONL file storage with a D1 SQLite database. Removes micro-r2.ts, adds micro.ts with a D1-backed microStore factory, creates the initial migration, and wires up the new MICRO_DB binding across all consumers. Drops unused assetsSchema and microSchema from schemas.ts, and demotes D1 unavailability in gen-url-manifest to a warning so fresh clones without local migrations don't break the build. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b01de83 commit 424d632

File tree

11 files changed

+113
-134
lines changed

11 files changed

+113
-134
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE IF NOT EXISTS micro_posts (
2+
id INTEGER PRIMARY KEY AUTOINCREMENT,
3+
content TEXT NOT NULL,
4+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')),
5+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')),
6+
syndicated_to TEXT NOT NULL DEFAULT '[]'
7+
);

scripts/gen-url-manifest.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function parseFrontmatter(content: string): { data: Record<string, any>; body: s
3131
}
3232

3333
/**
34-
* Process micro posts from R2 JSONL
34+
* Process micro posts from D1
3535
*/
3636
async function processMicroCollection(
3737
manifest: UrlManifest,
@@ -43,31 +43,24 @@ async function processMicroCollection(
4343
try {
4444
const { getPlatformProxy } = await import("wrangler");
4545
const projectRoot = import.meta.dir + "/..";
46-
const { env, dispose } = await getPlatformProxy<{ MICRO_BUCKET: R2Bucket }>({
46+
const { env, dispose } = await getPlatformProxy<{ MICRO_DB: D1Database }>({
4747
configPath: `${projectRoot}/wrangler.toml`,
4848
persist: { path: `${projectRoot}/.wrangler/state/v3` },
4949
});
5050

51-
if (!env.MICRO_BUCKET) {
52-
console.log(" [WARN] Skipping micro posts: MICRO_BUCKET not available\n");
51+
if (!env.MICRO_DB) {
52+
console.log(" [WARN] Skipping micro posts: MICRO_DB not available\n");
5353
await dispose();
5454
return { processed: 0, skipped: 0, errors: 0 };
5555
}
5656

57-
const obj = await env.MICRO_BUCKET.get("micro-posts.jsonl");
57+
const { results } = await env.MICRO_DB.prepare("SELECT id, created_at FROM micro_posts").all<{
58+
id: number;
59+
created_at: string;
60+
}>();
5861
await dispose();
5962

60-
if (!obj) {
61-
console.log(" [WARN] Skipping micro posts: micro-posts.jsonl not found in R2\n");
62-
return { processed: 0, skipped: 0, errors: 0 };
63-
}
64-
65-
const text = await obj.text();
66-
const posts = text
67-
.trim()
68-
.split("\n")
69-
.filter(Boolean)
70-
.map((line) => JSON.parse(line) as { id: number; createdAt: string });
63+
const posts = results.map((r) => ({ id: r.id, createdAt: r.created_at }));
7164

7265
let processedCount = 0;
7366
let errorCount = 0;
@@ -136,11 +129,10 @@ async function processMicroCollection(
136129
errors: errorCount,
137130
};
138131
} catch (error) {
139-
console.error(
140-
` [ERROR] Failed to load micro posts: ${error instanceof Error ? error.message : String(error)}`,
132+
console.log(
133+
` [WARN] Skipping micro posts: ${error instanceof Error ? error.message : String(error)}\n`,
141134
);
142-
console.log();
143-
return { processed: 0, skipped: 0, errors: 1 };
135+
return { processed: 0, skipped: 0, errors: 0 };
144136
}
145137
}
146138

@@ -359,7 +351,7 @@ async function genUrlManifest(collections: string[]) {
359351
for (const collection of collections) {
360352
let result;
361353
if (collection === "micro") {
362-
// Special handling for micro posts from R2
354+
// Special handling for micro posts from D1
363355
result = await processMicroCollection(manifest, existingSlugToCode, seenSlugsInCurrentRun);
364356
} else {
365357
// File-based collections

src/content.config.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
redirectsFileSchema,
1717
tagEntrySchema,
1818
tagsFileSchema,
19-
microSchema,
2019
} from "./content/schemas";
2120

2221
const blog = defineCollection({
@@ -136,11 +135,6 @@ const tags = defineCollection({
136135
schema: tagEntrySchema,
137136
});
138137

139-
const micro = defineCollection({
140-
loader: microLoader(),
141-
schema: microSchema,
142-
});
143-
144138
export const collections = {
145139
blog,
146140
games,
@@ -152,5 +146,4 @@ export const collections = {
152146
urls,
153147
redirects,
154148
tags,
155-
micro,
156149
};

src/content/schemas.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,6 @@ export const pagesSchema = z.object({
7373
description: z.string().optional(),
7474
});
7575

76-
export const assetsSchema = z.object({
77-
hash: z.string(),
78-
size: z.number(),
79-
ext: z.string(),
80-
});
81-
8276
export const urlsSchema = z.object({
8377
code: z.string(),
8478
});
@@ -114,10 +108,3 @@ export const tagsFileSchema = z.object({
114108
}),
115109
),
116110
});
117-
118-
export const microSchema = z.object({
119-
content: z.string().max(280, "Micro post content must be 280 characters or less"),
120-
createdAt: z.date(),
121-
updatedAt: z.date(),
122-
syndicatedTo: z.array(z.string()).default([]),
123-
});

src/env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// <reference types="@cloudflare/workers-types" />
22

33
type Runtime = import("@astrojs/cloudflare").Runtime<{
4-
MICRO_BUCKET: R2Bucket;
4+
MICRO_DB: D1Database;
55
MICRO_SECRET?: string;
66
BLUESKY_IDENTIFIER?: string;
77
BLUESKY_PASSWORD?: string;

src/lib/micro-r2.ts

Lines changed: 0 additions & 79 deletions
This file was deleted.

src/lib/micro.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
export interface MicroPost {
2+
id: number;
3+
content: string;
4+
createdAt: Date;
5+
updatedAt: Date;
6+
syndicatedTo: Array<{ platform: string; id: string; url: string }>;
7+
}
8+
9+
interface D1Row {
10+
id: number;
11+
content: string;
12+
created_at: string;
13+
updated_at: string;
14+
syndicated_to: string;
15+
}
16+
17+
function rowToPost(row: D1Row): MicroPost {
18+
return {
19+
id: row.id,
20+
content: row.content,
21+
createdAt: new Date(row.created_at),
22+
updatedAt: new Date(row.updated_at),
23+
syndicatedTo: JSON.parse(row.syndicated_to),
24+
};
25+
}
26+
27+
export function microStore(db: D1Database) {
28+
async function readPosts(): Promise<MicroPost[]> {
29+
const { results } = await db
30+
.prepare("SELECT * FROM micro_posts ORDER BY created_at DESC")
31+
.all<D1Row>();
32+
return results.map(rowToPost);
33+
}
34+
35+
async function addPost(content: string): Promise<MicroPost> {
36+
const now = new Date().toISOString();
37+
const { meta } = await db
38+
.prepare("INSERT INTO micro_posts (content, created_at, updated_at) VALUES (?, ?, ?)")
39+
.bind(content, now, now)
40+
.run();
41+
const row = await db
42+
.prepare("SELECT * FROM micro_posts WHERE id = ?")
43+
.bind(meta.last_row_id)
44+
.first<D1Row>();
45+
return rowToPost(row!);
46+
}
47+
48+
async function updatePost(id: number, updates: Partial<MicroPost>): Promise<void> {
49+
const now = new Date().toISOString();
50+
const sets: string[] = ["updated_at = ?"];
51+
const binds: unknown[] = [now];
52+
53+
if (updates.content !== undefined) {
54+
sets.push("content = ?");
55+
binds.push(updates.content);
56+
}
57+
if (updates.syndicatedTo !== undefined) {
58+
sets.push("syndicated_to = ?");
59+
binds.push(JSON.stringify(updates.syndicatedTo));
60+
}
61+
62+
binds.push(id);
63+
const result = await db
64+
.prepare(`UPDATE micro_posts SET ${sets.join(", ")} WHERE id = ?`)
65+
.bind(...binds)
66+
.run();
67+
if (result.meta.changes === 0) {
68+
throw new Error(`Post #${id} not found`);
69+
}
70+
}
71+
72+
async function deletePost(id: number): Promise<void> {
73+
await db.prepare("DELETE FROM micro_posts WHERE id = ?").bind(id).run();
74+
}
75+
76+
return { readPosts, addPost, updatePost, deletePost };
77+
}

src/pages/micro.astro

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
import Layout from "@/layouts/Layout.astro";
3-
import { createMicroStorage, type MicroPost } from "@/lib/micro-r2.ts";
3+
import { microStore, type MicroPost } from "@/lib/micro.ts";
44
import {
55
type SyndicationConfig,
66
type SyndicationResult,
@@ -47,7 +47,7 @@ if (Astro.request.method === "POST") {
4747
});
4848
}
4949
50-
const storage = createMicroStorage(env.MICRO_BUCKET);
50+
const storage = microStore(env.MICRO_DB);
5151
const post = await storage.addPost(content);
5252
5353
const config: SyndicationConfig = {};
@@ -96,7 +96,7 @@ if (
9696
Astro.request.headers.get("Accept")?.includes("application/json") &&
9797
isAuthorized()
9898
) {
99-
const storage = createMicroStorage(env.MICRO_BUCKET);
99+
const storage = microStore(env.MICRO_DB);
100100
const posts = await storage.readPosts();
101101
posts.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
102102
return new Response(JSON.stringify(posts), {
@@ -114,8 +114,8 @@ if (cursorParam !== null && !Number.isFinite(Number(cursorParam))) {
114114
return Astro.redirect("/micro", 302);
115115
}
116116
117-
// Read micro posts live from R2
118-
const storage = createMicroStorage(env.MICRO_BUCKET);
117+
// Read micro posts from D1
118+
const storage = microStore(env.MICRO_DB);
119119
const allPosts = (await storage.readPosts()).sort(
120120
(a, b) => b.createdAt.valueOf() - a.createdAt.valueOf()
121121
);

src/pages/micro/[id].astro

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
import { createMicroStorage } from "@/lib/micro-r2.ts";
2+
import { microStore } from "@/lib/micro.ts";
33
44
export const prerender = false;
55
@@ -21,7 +21,7 @@ if (Astro.request.method === "DELETE") {
2121
});
2222
}
2323
24-
const storage = createMicroStorage(env.MICRO_BUCKET);
24+
const storage = microStore(env.MICRO_DB);
2525
await storage.deletePost(postId);
2626
2727
return new Response(JSON.stringify({ success: true }), {
@@ -36,7 +36,7 @@ if (!Number.isFinite(postId)) {
3636
return new Response(null, { status: 404 });
3737
}
3838
39-
const storage = createMicroStorage(env.MICRO_BUCKET);
39+
const storage = microStore(env.MICRO_DB);
4040
const allPosts = (await storage.readPosts()).sort(
4141
(a, b) => b.createdAt.valueOf() - a.createdAt.valueOf()
4242
);

src/pages/micro/rss.xml.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import rss from "@astrojs/rss";
22
import { SITE_TITLE } from "@/consts";
3-
import { createMicroStorage } from "@/lib/micro-r2.ts";
3+
import { microStore } from "@/lib/micro.ts";
44

55
export const prerender = false;
66

77
export async function GET(context) {
88
const env = context.locals.runtime.env;
9-
const storage = createMicroStorage(env.MICRO_BUCKET);
9+
const storage = microStore(env.MICRO_DB);
1010
const posts = await storage.readPosts();
1111

1212
// Sort by createdAt descending (newest first)

0 commit comments

Comments
 (0)