feat(broker+api): every mesh ships with a default #general topic
The web chat surface needed a guaranteed landing room — a topic that exists for every mesh from creation onward so the dashboard always has somewhere to drop the user. #general is the convention; ephemeral DMs remain ephemeral (mesh.message_queue) so agentic privacy is unchanged. Three hooks plus a backfill: - packages/api/src/modules/mesh/mutations.ts — createMyMesh now calls ensureGeneralTopic() right after the mesh insert. New helper is idempotent via the unique (mesh_id, name) index. - apps/broker/src/index.ts — handleMeshCreate (CLI claudemesh new) inserts #general + subscribes the owner member as 'lead' in the same handler. - apps/broker/src/crypto.ts — invite-claim flow auto-subscribes the newly minted member to #general as 'member', defensively ensuring the topic exists if predates this change. - packages/db/migrations/0024_general_topic_backfill.sql — one-shot backfill: creates #general for every active mesh that doesn't have one, subscribes every active member, and marks the mesh owner as 'lead' based on owner_user_id == member.user_id. Idempotent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,14 @@ import { randomBytes } from "node:crypto";
|
||||
import sodium from "libsodium-wrappers";
|
||||
|
||||
import { and, eq, isNull } from "@turbostarter/db";
|
||||
import { invite, mesh, meshMember, pendingInvite } from "@turbostarter/db/schema";
|
||||
import {
|
||||
invite,
|
||||
mesh,
|
||||
meshMember,
|
||||
meshTopic,
|
||||
meshTopicMember,
|
||||
pendingInvite,
|
||||
} from "@turbostarter/db/schema";
|
||||
import { db } from "@turbostarter/db/server";
|
||||
|
||||
import type {
|
||||
@@ -142,9 +149,47 @@ export const createMyMesh = async ({
|
||||
})
|
||||
.returning({ id: mesh.id, slug: mesh.slug });
|
||||
|
||||
if (created) {
|
||||
await ensureGeneralTopic(created.id);
|
||||
}
|
||||
|
||||
return created!;
|
||||
};
|
||||
|
||||
/**
|
||||
* Idempotently create the conventional `#general` topic for a mesh.
|
||||
*
|
||||
* `#general` is the default web-readable room: a public topic that every
|
||||
* mesh has so the dashboard chat surface always has somewhere to land.
|
||||
* Subscription is not required for read access via the REST surface, so
|
||||
* subscribing members happens lazily at member-row creation time
|
||||
* (invite-claim) rather than here.
|
||||
*
|
||||
* Safe to call repeatedly — the unique (meshId, name) index keeps it a
|
||||
* no-op on the second call.
|
||||
*/
|
||||
export const ensureGeneralTopic = async (
|
||||
meshId: string,
|
||||
): Promise<{ id: string } | null> => {
|
||||
const [existing] = await db
|
||||
.select({ id: meshTopic.id })
|
||||
.from(meshTopic)
|
||||
.where(and(eq(meshTopic.meshId, meshId), eq(meshTopic.name, "general")))
|
||||
.limit(1);
|
||||
if (existing) return existing;
|
||||
const [row] = await db
|
||||
.insert(meshTopic)
|
||||
.values({
|
||||
meshId,
|
||||
name: "general",
|
||||
description: "Default mesh-wide channel. Every member can read and post.",
|
||||
visibility: "public",
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.returning({ id: meshTopic.id });
|
||||
return row ?? null;
|
||||
};
|
||||
|
||||
export const archiveMyMesh = async ({
|
||||
userId,
|
||||
meshId,
|
||||
|
||||
Reference in New Issue
Block a user