Skip to content

Commit 07f7d28

Browse files
committed
Implementation with sonnet 4.6
1 parent ae72fcb commit 07f7d28

28 files changed

Lines changed: 945 additions & 724 deletions

routes/profile.js

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { readdir, readFile, writeFile, unlink } from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { buildProfileState } from '../shared/profile-domain.mjs';
4+
5+
const USERNAME_RE = /^[a-z0-9][a-z0-9-]{1,63}$/i;
6+
7+
const usernameToPath = (username, profileDir) => {
8+
if (!USERNAME_RE.test(username)) return null;
9+
return path.join(profileDir, `${username}.json`);
10+
};
11+
12+
const readProfile = async (username, profileDir) => {
13+
const filePath = usernameToPath(username, profileDir);
14+
if (!filePath) return null;
15+
const raw = await readFile(filePath, 'utf8');
16+
return JSON.parse(raw);
17+
};
18+
19+
const saveProfileState = async (username, incoming, profileDir) => {
20+
const filePath = usernameToPath(username, profileDir);
21+
if (!filePath) {
22+
return {
23+
ok: false,
24+
status: 422,
25+
payload: { ok: false, errors: { id: 'Invalid username' } },
26+
};
27+
}
28+
29+
const merged = { ...(incoming || {}), id: username };
30+
const state = buildProfileState(merged);
31+
32+
if (!state.valid) {
33+
return {
34+
ok: false,
35+
status: 422,
36+
payload: {
37+
ok: false,
38+
profile: state.profile,
39+
computed: state.computed,
40+
errors: state.errors,
41+
valid: false,
42+
},
43+
};
44+
}
45+
46+
await writeFile(filePath, JSON.stringify(state.profile, null, 2), 'utf8');
47+
return {
48+
ok: true,
49+
status: 200,
50+
payload: {
51+
ok: true,
52+
profile: state.profile,
53+
computed: state.computed,
54+
errors: state.errors,
55+
valid: true,
56+
},
57+
};
58+
};
59+
60+
const getProfile = async (
61+
req,
62+
res,
63+
{ username },
64+
{ profileDir, sendJson, serveIndex, staticDir },
65+
) => {
66+
if (!USERNAME_RE.test(username)) {
67+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
68+
res.end('Not found');
69+
return;
70+
}
71+
const accept = req.headers.accept || '';
72+
if (accept.includes('text/html') && !accept.includes('application/json')) {
73+
await serveIndex(res, staticDir);
74+
return;
75+
}
76+
try {
77+
const source = await readProfile(username, profileDir);
78+
const state = buildProfileState(source);
79+
sendJson(res, 200, {
80+
ok: true,
81+
profile: state.profile,
82+
computed: state.computed,
83+
errors: state.errors,
84+
valid: state.valid,
85+
});
86+
} catch (error) {
87+
if (error?.code === 'ENOENT') {
88+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
89+
res.end('Not found');
90+
return;
91+
}
92+
if (error instanceof SyntaxError) {
93+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
94+
res.end('Corrupt profile JSON');
95+
return;
96+
}
97+
throw error;
98+
}
99+
};
100+
101+
const updateProfile = async (
102+
req,
103+
res,
104+
{ username },
105+
{ profileDir, sendJson, parseJsonBody },
106+
) => {
107+
if (!USERNAME_RE.test(username)) {
108+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
109+
res.end('Not found');
110+
return;
111+
}
112+
let body;
113+
try {
114+
body = await parseJsonBody(req);
115+
} catch (error) {
116+
if (error.message === 'INVALID_JSON') {
117+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
118+
res.end('Invalid JSON');
119+
return;
120+
}
121+
throw error;
122+
}
123+
const result = await saveProfileState(username, body, profileDir);
124+
sendJson(res, result.status, result.payload);
125+
};
126+
127+
const removeProfile = async (
128+
req,
129+
res,
130+
{ username },
131+
{ profileDir, sendJson },
132+
) => {
133+
const target = usernameToPath(username, profileDir);
134+
if (!target) {
135+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
136+
res.end('Not found');
137+
return;
138+
}
139+
try {
140+
await unlink(target);
141+
sendJson(res, 200, { ok: true });
142+
} catch (error) {
143+
if (error?.code === 'ENOENT') {
144+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
145+
res.end('Not found');
146+
return;
147+
}
148+
throw error;
149+
}
150+
};
151+
152+
const listProfiles = async (req, res, _params, { profileDir, sendJson }) => {
153+
const url = new URL(req.url, 'http://localhost');
154+
const nameFilter = url.searchParams.get('name')?.toLowerCase() || '';
155+
const emailFilter = url.searchParams.get('email')?.toLowerCase() || '';
156+
157+
const files = await readdir(profileDir).catch(() => []);
158+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
159+
160+
const profiles = await Promise.all(
161+
jsonFiles.map(async (f) => {
162+
try {
163+
const raw = await readFile(path.join(profileDir, f), 'utf8');
164+
const { profile, computed } = buildProfileState(JSON.parse(raw));
165+
return {
166+
id: profile.id,
167+
displayName: computed.displayName,
168+
email: profile.email,
169+
};
170+
} catch {
171+
return null;
172+
}
173+
}),
174+
);
175+
176+
const items = profiles
177+
.filter(Boolean)
178+
.filter(
179+
(p) => !nameFilter || p.displayName.toLowerCase().includes(nameFilter),
180+
)
181+
.filter((p) => !emailFilter || p.email.toLowerCase().includes(emailFilter))
182+
.sort((a, b) => a.id.localeCompare(b.id));
183+
184+
sendJson(res, 200, { ok: true, items });
185+
};
186+
187+
export { USERNAME_RE, usernameToPath, readProfile };
188+
export const routes = [
189+
{ pattern: '/profile', handlers: { GET: listProfiles } },
190+
{
191+
pattern: '/profile/:username',
192+
handlers: {
193+
GET: getProfile,
194+
POST: updateProfile,
195+
PUT: updateProfile,
196+
DELETE: removeProfile,
197+
},
198+
},
199+
];

routes/profiles.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { buildProfileState } from '../shared/profile-domain.mjs';
2+
import { stat, writeFile } from 'node:fs/promises';
3+
import path from 'node:path';
4+
import { USERNAME_RE } from './profile.js';
5+
6+
const createProfile = async (
7+
req,
8+
res,
9+
_params,
10+
{ profileDir, sendJson, parseJsonBody },
11+
) => {
12+
let body;
13+
try {
14+
body = await parseJsonBody(req);
15+
} catch (error) {
16+
if (error.message === 'INVALID_JSON') {
17+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
18+
res.end('Invalid JSON');
19+
return;
20+
}
21+
throw error;
22+
}
23+
24+
const requestedId = typeof body.id === 'string' ? body.id.trim() : '';
25+
if (!USERNAME_RE.test(requestedId)) {
26+
sendJson(res, 422, { ok: false, errors: { id: 'Invalid username' } });
27+
return;
28+
}
29+
30+
const filePath = path.join(profileDir, `${requestedId}.json`);
31+
try {
32+
await stat(filePath);
33+
sendJson(res, 409, { ok: false, error: 'Profile already exists' });
34+
return;
35+
} catch (error) {
36+
if (error?.code !== 'ENOENT') throw error;
37+
}
38+
39+
const merged = { ...body, id: requestedId };
40+
const state = buildProfileState(merged);
41+
42+
if (!state.valid) {
43+
sendJson(res, 422, {
44+
ok: false,
45+
profile: state.profile,
46+
computed: state.computed,
47+
errors: state.errors,
48+
valid: false,
49+
});
50+
return;
51+
}
52+
53+
await writeFile(filePath, JSON.stringify(state.profile, null, 2), 'utf8');
54+
sendJson(res, 200, {
55+
ok: true,
56+
profile: state.profile,
57+
computed: state.computed,
58+
errors: state.errors,
59+
valid: true,
60+
});
61+
};
62+
63+
export default { POST: createProfile };

routes/static.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { readFile, readdir } from 'node:fs/promises';
2+
import path from 'node:path';
3+
4+
const MIME_TYPES = {
5+
'.html': 'text/html; charset=utf-8',
6+
'.css': 'text/css; charset=utf-8',
7+
'.mjs': 'application/javascript; charset=utf-8',
8+
'.js': 'application/javascript; charset=utf-8',
9+
'.json': 'application/json; charset=utf-8',
10+
'.txt': 'text/plain; charset=utf-8',
11+
};
12+
13+
const TEMPLATES_PLACEHOLDER = '<!-- {{templates}} -->';
14+
15+
const serveFile = async (res, filePath) => {
16+
try {
17+
const data = await readFile(filePath);
18+
const ext = path.extname(filePath);
19+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
20+
res.writeHead(200, { 'Content-Type': contentType });
21+
res.end(data);
22+
} catch (error) {
23+
if (error?.code === 'ENOENT') {
24+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
25+
res.end('Not found');
26+
return;
27+
}
28+
throw error;
29+
}
30+
};
31+
32+
const serveIndex = async (res, staticDir) => {
33+
const componentsDir = path.join(staticDir, 'components');
34+
const [indexHtml, files] = await Promise.all([
35+
readFile(path.join(staticDir, 'index.html'), 'utf8'),
36+
readdir(componentsDir),
37+
]);
38+
const htmlFiles = files.filter((f) => f.endsWith('.html')).sort();
39+
const parts = await Promise.all(
40+
htmlFiles.map((f) => readFile(path.join(componentsDir, f), 'utf8')),
41+
);
42+
const html = indexHtml.replace(TEMPLATES_PLACEHOLDER, parts.join('\n'));
43+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
44+
res.end(html);
45+
};
46+
47+
export { serveFile, serveIndex };

0 commit comments

Comments
 (0)