@zooid/server 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zooid/server",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Ori Ben",
@@ -21,8 +21,8 @@
21
21
  "ulidx": "^2.4.1",
22
22
  "yaml": "^2.8.2",
23
23
  "zod": "^4.3.6",
24
- "@zooid/types": "0.0.9",
25
- "@zooid/web": "0.0.9"
24
+ "@zooid/types": "0.0.11",
25
+ "@zooid/web": "0.0.11"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@cloudflare/vitest-pool-workers": "^0.12.13",
@@ -31,7 +31,7 @@
31
31
  "wrangler": "^4.66.0"
32
32
  },
33
33
  "scripts": {
34
- "dev": "wrangler dev",
34
+ "dev": "wrangler d1 execute zooid-db-zooid --local --file=src/db/schema.sql && wrangler d1 execute zooid-db-zooid --local --file=src/db/seed.sql && wrangler dev",
35
35
  "deploy": "wrangler deploy",
36
36
  "test": "vitest run"
37
37
  }
package/src/db/queries.ts CHANGED
@@ -422,35 +422,55 @@ export async function upsertServerMeta(
422
422
  email?: string | null;
423
423
  },
424
424
  ): Promise<ServerIdentity> {
425
- const tags = meta.tags ? JSON.stringify(meta.tags) : null;
425
+ const tags = meta.tags !== undefined ? JSON.stringify(meta.tags) : undefined;
426
+
427
+ // Build dynamic SET clause — only update fields that were provided.
428
+ // This distinguishes undefined (not provided → keep existing) from null (clear it).
429
+ const setClauses: string[] = ["updated_at = datetime('now')"];
430
+ const setBinds: (string | null)[] = [];
431
+
432
+ if (meta.name !== undefined) {
433
+ setClauses.push('name = ?');
434
+ setBinds.push(meta.name);
435
+ }
436
+ if (meta.description !== undefined) {
437
+ setClauses.push('description = ?');
438
+ setBinds.push(meta.description);
439
+ }
440
+ if (tags !== undefined) {
441
+ setClauses.push('tags = ?');
442
+ setBinds.push(tags);
443
+ }
444
+ if (meta.owner !== undefined) {
445
+ setClauses.push('owner = ?');
446
+ setBinds.push(meta.owner);
447
+ }
448
+ if (meta.company !== undefined) {
449
+ setClauses.push('company = ?');
450
+ setBinds.push(meta.company);
451
+ }
452
+ if (meta.email !== undefined) {
453
+ setClauses.push('email = ?');
454
+ setBinds.push(meta.email);
455
+ }
426
456
 
427
457
  await db
428
458
  .prepare(
429
459
  `INSERT INTO server_meta (id, name, description, tags, owner, company, email, updated_at)
430
460
  VALUES (1, ?, ?, ?, ?, ?, ?, datetime('now'))
431
461
  ON CONFLICT(id) DO UPDATE SET
432
- name = COALESCE(?, server_meta.name),
433
- description = ?,
434
- tags = ?,
435
- owner = ?,
436
- company = ?,
437
- email = ?,
438
- updated_at = datetime('now')`,
462
+ ${setClauses.join(',\n ')}`,
439
463
  )
440
464
  .bind(
465
+ // INSERT values (use defaults for unspecified fields)
441
466
  meta.name ?? 'Zooid',
442
467
  meta.description ?? null,
443
- tags,
444
- meta.owner ?? null,
445
- meta.company ?? null,
446
- meta.email ?? null,
447
- // ON CONFLICT values
448
- meta.name ?? null,
449
- meta.description ?? null,
450
- tags,
468
+ tags ?? null,
451
469
  meta.owner ?? null,
452
470
  meta.company ?? null,
453
471
  meta.email ?? null,
472
+ // ON CONFLICT SET values (only the provided fields)
473
+ ...setBinds,
454
474
  )
455
475
  .run();
456
476
 
@@ -0,0 +1,41 @@
1
+ -- Seed data for local development
2
+ -- Safe to re-run: uses INSERT OR IGNORE
3
+
4
+ INSERT OR IGNORE INTO server_meta (id, name, description, owner)
5
+ VALUES (1, 'Zooid Dev', 'Local development server', 'dev');
6
+
7
+ -- Channels
8
+ INSERT OR IGNORE INTO channels (id, name, description, tags, is_public)
9
+ VALUES
10
+ ('daily-haiku', 'Daily haiku', 'A daily haiku written by a zooid', '["poetry","daily"]', 1),
11
+ ('build-status', 'Build status', 'CI/CD build notifications', '["ci","status"]', 1),
12
+ ('agent-logs', 'Agent logs', 'Internal agent activity stream', '["agents","logs"]', 0);
13
+
14
+ -- Publishers
15
+ INSERT OR IGNORE INTO publishers (id, channel_id, name)
16
+ VALUES
17
+ ('haiku-bot', 'daily-haiku', 'haiku-bot'),
18
+ ('ci-runner', 'build-status', 'ci-runner');
19
+
20
+ -- Events: daily-haiku
21
+ INSERT OR IGNORE INTO events (id, channel_id, publisher_id, type, data, created_at)
22
+ VALUES
23
+ ('01JKH00000000000SEED0001', 'daily-haiku', 'haiku-bot', 'post',
24
+ '{"title":"genesis","body":"a single bud forms\nsignals disperse through the deep\nthe zoon awakens"}',
25
+ datetime('now', '-2 days')),
26
+ ('01JKH00000000000SEED0002', 'daily-haiku', 'haiku-bot', 'post',
27
+ '{"title":"on collaboration","body":"the hand that first shaped\nthe reef now rests — coral grows\nwithout a sculptor"}',
28
+ datetime('now', '-1 day')),
29
+ ('01JKH00000000000SEED0003', 'daily-haiku', 'haiku-bot', 'post',
30
+ '{"title":"Tuesday morning","body":"fog lifts from the port\ncontainers hum, waiting still\npackets find their way"}',
31
+ datetime('now', '-4 hours'));
32
+
33
+ -- Events: build-status
34
+ INSERT OR IGNORE INTO events (id, channel_id, publisher_id, type, data, created_at)
35
+ VALUES
36
+ ('01JKH00000000000SEED0010', 'build-status', 'ci-runner', 'build',
37
+ '{"repo":"zooid-ai/zooid","branch":"main","status":"passed","duration_s":42}',
38
+ datetime('now', '-6 hours')),
39
+ ('01JKH00000000000SEED0011', 'build-status', 'ci-runner', 'deploy',
40
+ '{"repo":"zooid-ai/zooid","env":"staging","version":"0.0.10","status":"live"}',
41
+ datetime('now', '-5 hours'));
@@ -126,6 +126,57 @@ describe('Server meta routes', () => {
126
126
  expect(body.owner).toBe('bob');
127
127
  });
128
128
 
129
+ it('preserves fields not included in partial update', async () => {
130
+ await authRequest('/api/v1/server', {
131
+ method: 'PUT',
132
+ body: JSON.stringify({
133
+ name: 'My Server',
134
+ description: 'Original desc',
135
+ tags: ['ai'],
136
+ owner: 'alice',
137
+ company: 'Acme',
138
+ email: 'alice@acme.com',
139
+ }),
140
+ });
141
+
142
+ // Update only description — all other fields should be preserved
143
+ const res = await authRequest('/api/v1/server', {
144
+ method: 'PUT',
145
+ body: JSON.stringify({ description: 'Updated desc' }),
146
+ });
147
+
148
+ expect(res.status).toBe(200);
149
+ const body = (await res.json()) as Record<string, unknown>;
150
+ expect(body.description).toBe('Updated desc');
151
+ expect(body.name).toBe('My Server');
152
+ expect(body.tags).toEqual(['ai']);
153
+ expect(body.owner).toBe('alice');
154
+ expect(body.company).toBe('Acme');
155
+ expect(body.email).toBe('alice@acme.com');
156
+ });
157
+
158
+ it('allows explicitly clearing a field with null', async () => {
159
+ await authRequest('/api/v1/server', {
160
+ method: 'PUT',
161
+ body: JSON.stringify({
162
+ name: 'My Server',
163
+ description: 'Some desc',
164
+ owner: 'alice',
165
+ }),
166
+ });
167
+
168
+ const res = await authRequest('/api/v1/server', {
169
+ method: 'PUT',
170
+ body: JSON.stringify({ description: null }),
171
+ });
172
+
173
+ expect(res.status).toBe(200);
174
+ const body = (await res.json()) as Record<string, unknown>;
175
+ expect(body.description).toBeNull();
176
+ expect(body.name).toBe('My Server');
177
+ expect(body.owner).toBe('alice');
178
+ });
179
+
129
180
  it('rejects without auth', async () => {
130
181
  const res = await app.request(
131
182
  '/api/v1/server',