feat: uuid ids everywhere, pickers for linked tickets, @pi/ui checkboxes

- Postgres: every table has a uuid `id` (gen_random_uuid()); the event
  store keeps its relay order in `seq`, and the lookup tables their
  uniqueness in constraints of their own.
- MongoDB: `_id` is Mongo's ObjectId; `id` is the domain id (the same
  uuid as the aggregate's stream in Postgres), unique, and every query
  goes by it. Production starts from empty databases.
- Web: linking tickets and hanging one from a parent are picked from a
  searchable select (links across projects, parents within the project)
  instead of typing a key; every checkbox is @pi/ui's pi-checkbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 19:16:43 +02:00
parent 10c8f21ff4
commit 2c501a50d0
30 changed files with 292 additions and 207 deletions

View File

@@ -79,7 +79,7 @@ export class MongoFindProjectsRepository extends FindProjectsRepository {
}
async findById(id: string): Promise<ProjectReadModel | null> {
const doc = await this.projects.findById(id).lean<ProjectDoc>();
const doc = await this.projects.findOne({ id: id }).lean<ProjectDoc>();
return doc ? toProject(doc) : null;
}
@@ -109,7 +109,7 @@ export class MongoFindProjectsRepository extends FindProjectsRepository {
userId: string,
): Promise<ProjectRole | null> {
const doc = await this.projects
.findOne({ _id: projectId, memberIds: userId }, { members: 1 })
.findOne({ id: projectId, memberIds: userId }, { members: 1 })
.lean<Pick<ProjectDoc, 'members'>>();
return doc?.members.find((m) => m.user.id === userId)?.role ?? null;
}
@@ -124,7 +124,7 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
}
async findById(id: string): Promise<IssueReadModel | null> {
const doc = await this.issues.findById(id).lean<IssueDoc>();
const doc = await this.issues.findOne({ id: id }).lean<IssueDoc>();
return doc ? issueFromDoc(doc) : null;
}
@@ -136,7 +136,7 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
async findByIds(ids: string[]): Promise<IssueReadModel[]> {
if (ids.length === 0) return [];
const docs = await this.issues
.find({ _id: { $in: ids } })
.find({ id: { $in: ids } })
.lean<IssueDoc[]>();
return docs.map(issueFromDoc);
}
@@ -322,15 +322,15 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
// Only the top of each tree: a ticket with subtasks under an epic is not one.
const docs = await this.issues
.find({
_id: { $in: [...progress.keys()] },
id: { $in: [...progress.keys()] },
'parent.id': null,
trashedAt: null,
})
.sort({ number: 1 })
.lean<IssueDoc[]>();
return docs.map((doc) => {
const { _id: _ignored, ...row } = progress.get(doc._id) ?? {
_id: doc._id,
const { _id: _ignored, ...row } = progress.get(doc.id) ?? {
_id: doc.id,
total: 0,
finished: 0,
storyPoints: 0,
@@ -456,7 +456,7 @@ export class MongoFindCommentsRepository extends FindCommentsRepository {
}
async findById(id: string): Promise<CommentReadModel | null> {
const doc = await this.comments.findById(id).lean<CommentDoc>();
const doc = await this.comments.findOne({ id: id }).lean<CommentDoc>();
return doc ? commentFromDoc(doc) : null;
}
@@ -496,7 +496,7 @@ export class MongoFindReleasesRepository extends FindReleasesRepository {
}
async findById(id: string): Promise<ReleaseReadModel | null> {
const doc = await this.releases.findById(id).lean<ReleaseDoc>();
const doc = await this.releases.findOne({ id: id }).lean<ReleaseDoc>();
return doc ? fromDoc(doc) : null;
}