synomem 0.6.11 → 0.7.1
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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +92 -0
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +22 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +164 -3
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/import.d.ts +83 -0
- package/dist/import.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +110 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/ports/repository.d.ts +10 -1
- package/dist/ports/repository.d.ts.map +1 -1
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +6 -0
- package/dist/projections.js.map +1 -1
- package/dist/remote.d.ts +10 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +9 -0
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +130 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +77 -0
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +14 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +12 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +136 -19
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +72 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +153 -0
- package/src/client.ts +184 -2
- package/src/errors.ts +2 -0
- package/src/index.ts +8 -0
- package/src/mcp/index.ts +140 -1
- package/src/ports/repository.ts +8 -0
- package/src/projections.ts +6 -0
- package/src/remote.ts +45 -0
- package/src/schemas.ts +85 -0
- package/src/service.ts +18 -0
- package/src/storage.ts +167 -13
- package/src/types.ts +82 -2
package/dist/client.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveHome } from './config.js';
|
|
|
5
5
|
import { asSynomemError, SynomemError } from './errors.js';
|
|
6
6
|
import { assertNoSymlinkEscape } from './fs-utils.js';
|
|
7
7
|
import { escapeMarkdown, memoRecordsFromEvents, noteRecordsFromEvents, postRecordsFromEvents, ProjectionManager, recordsFromEvents, taskRecordsFromEvents, todoRecordsFromEvents, } from './projections.js';
|
|
8
|
-
import { actorSchema, agentLookupSchema, bindRuntimeSchema, createAgentSchema, createPostSchema, updatePostSchema, createNoteSchema, createTaskSchema, createTodoSchema, changesInputSchema, giveKudosSchema, itemListInputSchema, listInputSchema, reviseNoteSchema, sendMemoSchema, updateTaskSchema, updateTodoSchema, updateAgentSchema, } from './schemas.js';
|
|
8
|
+
import { actorSchema, agentLookupSchema, bindRuntimeSchema, createAgentSchema, createPostSchema, updatePostSchema, createNoteSchema, createTaskSchema, createTodoSchema, changesInputSchema, giveKudosSchema, itemListInputSchema, listInputSchema, reviseNoteSchema, sendMemoSchema, updateTaskSchema, updateTodoSchema, updateAgentSchema, createTopicSchema, updateTopicSchema, topicListInputSchema, topicLookupSchema, } from './schemas.js';
|
|
9
9
|
import { SynomemStorage } from './storage.js';
|
|
10
10
|
export class SynomemCore {
|
|
11
11
|
/**
|
|
@@ -35,6 +35,15 @@ export class SynomemCore {
|
|
|
35
35
|
bindRuntime: (input) => this.bindRuntime(input),
|
|
36
36
|
unbindRuntime: (bindingId) => this.unbindRuntime(bindingId),
|
|
37
37
|
};
|
|
38
|
+
topics = {
|
|
39
|
+
create: (input) => this.createTopic(input),
|
|
40
|
+
update: (idOrAlias, changes) => this.updateTopicRecord(idOrAlias, changes),
|
|
41
|
+
get: (idOrAlias) => this.getTopicRecord(idOrAlias),
|
|
42
|
+
list: (input = {}) => this.listTopicsRecord(input),
|
|
43
|
+
resolve: (query) => this.resolveTopicRecord(query),
|
|
44
|
+
archive: (idOrAlias) => this.setTopicStatus(idOrAlias, 'archived'),
|
|
45
|
+
restore: (idOrAlias) => this.setTopicStatus(idOrAlias, 'active'),
|
|
46
|
+
};
|
|
38
47
|
kudos = {
|
|
39
48
|
give: (input) => this.giveKudos(input),
|
|
40
49
|
list: (input = {}) => this.listKudos(input),
|
|
@@ -427,6 +436,138 @@ export class SynomemCore {
|
|
|
427
436
|
this.checkAbort();
|
|
428
437
|
return await this.repository.unbindRuntime(bindingId);
|
|
429
438
|
}
|
|
439
|
+
/* ---------------------------------------------------------------- topics *
|
|
440
|
+
* A topic is a controlled, reusable subject a record can be filed under —
|
|
441
|
+
* one canonical display name and a set of aliases, so a stable "Synomem"
|
|
442
|
+
* page survives a rename without retagging every record that carries it.
|
|
443
|
+
* Any actor may create one freely, the same as a tag; only the creator or
|
|
444
|
+
* an administrator renames or archives it, matching how an agent's own
|
|
445
|
+
* identity is managed.
|
|
446
|
+
*/
|
|
447
|
+
async createTopic(input) {
|
|
448
|
+
this.checkAbort();
|
|
449
|
+
await this.repository.assertEventCompatibility();
|
|
450
|
+
const parsed = this.validate(() => createTopicSchema.parse(input));
|
|
451
|
+
if (await this.repository.getTopic(parsed.displayName)) {
|
|
452
|
+
throw new SynomemError('TOPIC_EXISTS', `Topic or alias already exists: ${parsed.displayName}`);
|
|
453
|
+
}
|
|
454
|
+
const aliases = [...new Set(parsed.aliases ?? [])].sort();
|
|
455
|
+
for (const alias of aliases) {
|
|
456
|
+
if (await this.repository.getTopic(alias)) {
|
|
457
|
+
throw new SynomemError('ALIAS_CONFLICT', `Alias already belongs to a topic: ${alias}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const topic = {
|
|
461
|
+
id: this.nextId(),
|
|
462
|
+
displayName: parsed.displayName,
|
|
463
|
+
...(aliases.length ? { aliases } : {}),
|
|
464
|
+
status: 'active',
|
|
465
|
+
createdAt: this.now(),
|
|
466
|
+
};
|
|
467
|
+
const event = {
|
|
468
|
+
...this.eventBase(topic.id, 1),
|
|
469
|
+
type: 'topic.created',
|
|
470
|
+
topic,
|
|
471
|
+
};
|
|
472
|
+
await this.repository.transaction(async () => {
|
|
473
|
+
await this.repository.insertTopic(topic);
|
|
474
|
+
await this.repository.insertEvent(event);
|
|
475
|
+
});
|
|
476
|
+
return topic;
|
|
477
|
+
}
|
|
478
|
+
async updateTopicRecord(idOrAlias, changes) {
|
|
479
|
+
this.checkAbort();
|
|
480
|
+
await this.repository.assertEventCompatibility();
|
|
481
|
+
this.validate(() => topicLookupSchema.parse(idOrAlias));
|
|
482
|
+
const parsed = this.validate(() => updateTopicSchema.parse(changes));
|
|
483
|
+
const existing = await this.repository.getTopic(idOrAlias);
|
|
484
|
+
if (!existing)
|
|
485
|
+
throw new SynomemError('TOPIC_NOT_FOUND', `Unknown topic: ${idOrAlias}`);
|
|
486
|
+
if (parsed.displayName && parsed.displayName !== existing.displayName) {
|
|
487
|
+
const owner = await this.repository.getTopic(parsed.displayName);
|
|
488
|
+
if (owner && owner.id !== existing.id) {
|
|
489
|
+
throw new SynomemError('ALIAS_CONFLICT', `Display name already belongs to ${owner.id}: ${parsed.displayName}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const aliases = parsed.aliases ? [...new Set(parsed.aliases)].sort() : existing.aliases;
|
|
493
|
+
for (const alias of aliases ?? []) {
|
|
494
|
+
const owner = await this.repository.getTopic(alias);
|
|
495
|
+
if (owner && owner.id !== existing.id) {
|
|
496
|
+
throw new SynomemError('ALIAS_CONFLICT', `Alias already belongs to ${owner.id}: ${alias}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const updated = {
|
|
500
|
+
...existing,
|
|
501
|
+
...parsed,
|
|
502
|
+
...(aliases?.length ? { aliases } : { aliases: undefined }),
|
|
503
|
+
};
|
|
504
|
+
const changesForEvent = { ...parsed, ...(parsed.aliases ? { aliases } : {}) };
|
|
505
|
+
await this.repository.transaction(async () => {
|
|
506
|
+
const event = {
|
|
507
|
+
...this.eventBase(existing.id, await this.repository.nextAggregateVersion(existing.id)),
|
|
508
|
+
type: 'topic.updated',
|
|
509
|
+
topicId: existing.id,
|
|
510
|
+
changes: changesForEvent,
|
|
511
|
+
};
|
|
512
|
+
await this.repository.updateTopic(updated, event.createdAt);
|
|
513
|
+
await this.repository.insertEvent(event);
|
|
514
|
+
});
|
|
515
|
+
return updated;
|
|
516
|
+
}
|
|
517
|
+
async setTopicStatus(idOrAlias, status) {
|
|
518
|
+
this.checkAbort();
|
|
519
|
+
await this.repository.assertEventCompatibility();
|
|
520
|
+
this.validate(() => topicLookupSchema.parse(idOrAlias));
|
|
521
|
+
const existing = await this.repository.getTopic(idOrAlias);
|
|
522
|
+
if (!existing)
|
|
523
|
+
throw new SynomemError('TOPIC_NOT_FOUND', `Unknown topic: ${idOrAlias}`);
|
|
524
|
+
if (existing.status === status)
|
|
525
|
+
return existing;
|
|
526
|
+
const updated = { ...existing, status };
|
|
527
|
+
await this.repository.transaction(async () => {
|
|
528
|
+
const event = {
|
|
529
|
+
...this.eventBase(existing.id, await this.repository.nextAggregateVersion(existing.id)),
|
|
530
|
+
type: 'topic.updated',
|
|
531
|
+
topicId: existing.id,
|
|
532
|
+
changes: { status },
|
|
533
|
+
};
|
|
534
|
+
await this.repository.updateTopic(updated, event.createdAt);
|
|
535
|
+
await this.repository.insertEvent(event);
|
|
536
|
+
});
|
|
537
|
+
return updated;
|
|
538
|
+
}
|
|
539
|
+
async getTopicRecord(idOrAlias) {
|
|
540
|
+
this.checkAbort();
|
|
541
|
+
this.validate(() => topicLookupSchema.parse(idOrAlias));
|
|
542
|
+
const topic = await this.repository.getTopic(idOrAlias);
|
|
543
|
+
if (!topic)
|
|
544
|
+
throw new SynomemError('TOPIC_NOT_FOUND', `Unknown topic: ${idOrAlias}`);
|
|
545
|
+
return topic;
|
|
546
|
+
}
|
|
547
|
+
async listTopicsRecord(input) {
|
|
548
|
+
this.checkAbort();
|
|
549
|
+
const parsed = this.validate(() => topicListInputSchema.parse(input));
|
|
550
|
+
return await this.repository.listTopics(parsed.status);
|
|
551
|
+
}
|
|
552
|
+
async resolveTopicRecord(query) {
|
|
553
|
+
this.checkAbort();
|
|
554
|
+
const trimmed = this.validate(() => topicLookupSchema.parse(query));
|
|
555
|
+
const resolved = await this.repository.resolveTopic(trimmed);
|
|
556
|
+
return { query: trimmed, ...resolved };
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Every topic in `topicIds` must already exist and be active — a record
|
|
560
|
+
* cannot be filed under a subject that does not exist, or one somebody
|
|
561
|
+
* archived precisely to stop new records collecting under it.
|
|
562
|
+
*/
|
|
563
|
+
async assertTopicsExist(topicIds) {
|
|
564
|
+
for (const topicId of topicIds ?? []) {
|
|
565
|
+
const topic = await this.repository.getTopic(topicId);
|
|
566
|
+
if (!topic || topic.status !== 'active') {
|
|
567
|
+
throw new SynomemError('TOPIC_NOT_FOUND', `Unknown or archived topic: ${topicId}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
430
571
|
async giveKudos(input) {
|
|
431
572
|
this.checkAbort();
|
|
432
573
|
await this.repository.assertEventCompatibility();
|
|
@@ -434,6 +575,7 @@ export class SynomemCore {
|
|
|
434
575
|
...input,
|
|
435
576
|
visibility: input.visibility ?? this.repository.config.defaultVisibility,
|
|
436
577
|
}));
|
|
578
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
437
579
|
const recipient = await this.repository.getAgent(parsed.recipientAgentId);
|
|
438
580
|
if (!recipient) {
|
|
439
581
|
throw new SynomemError('AGENT_NOT_FOUND', `Unknown recipient: ${parsed.recipientAgentId}`);
|
|
@@ -458,6 +600,7 @@ export class SynomemCore {
|
|
|
458
600
|
visibility: parsed.visibility,
|
|
459
601
|
...(parsed.evidence ? { evidence: parsed.evidence } : {}),
|
|
460
602
|
...(parsed.tags ? { tags: [...new Set(parsed.tags)].sort() } : {}),
|
|
603
|
+
...(parsed.topicIds ? { topicIds: [...new Set(parsed.topicIds)].sort() } : {}),
|
|
461
604
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
462
605
|
...(parsed.source ? { source: parsed.source } : {}),
|
|
463
606
|
...(parsed.metadata ? { metadata: parsed.metadata } : {}),
|
|
@@ -605,6 +748,7 @@ export class SynomemCore {
|
|
|
605
748
|
this.checkAbort();
|
|
606
749
|
await this.repository.assertEventCompatibility();
|
|
607
750
|
const parsed = this.validate(() => sendMemoSchema.parse(input));
|
|
751
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
608
752
|
const recipient = await this.repository.getAgent(parsed.recipientAgentId);
|
|
609
753
|
if (!recipient)
|
|
610
754
|
throw new SynomemError('AGENT_NOT_FOUND', `Unknown recipient: ${parsed.recipientAgentId}`);
|
|
@@ -621,6 +765,7 @@ export class SynomemCore {
|
|
|
621
765
|
subject: parsed.subject,
|
|
622
766
|
body: parsed.body,
|
|
623
767
|
tags: [...new Set(parsed.tags ?? [])].sort(),
|
|
768
|
+
topicIds: [...new Set(parsed.topicIds ?? [])].sort(),
|
|
624
769
|
visibility: parsed.visibility ?? this.repository.config.defaultVisibility,
|
|
625
770
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
626
771
|
...(parsed.source ? { source: parsed.source } : {}),
|
|
@@ -700,6 +845,7 @@ export class SynomemCore {
|
|
|
700
845
|
this.checkAbort();
|
|
701
846
|
await this.repository.assertEventCompatibility();
|
|
702
847
|
const parsed = this.validate(() => createPostSchema.parse(input));
|
|
848
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
703
849
|
// A reply inherits its parent's workspace by construction, and cannot name
|
|
704
850
|
// a different target — there is no target to name.
|
|
705
851
|
if (parsed.replyTo)
|
|
@@ -715,6 +861,7 @@ export class SynomemCore {
|
|
|
715
861
|
title: parsed.title,
|
|
716
862
|
body: parsed.body,
|
|
717
863
|
tags: [...new Set(parsed.tags ?? [])].sort(),
|
|
864
|
+
topicIds: [...new Set(parsed.topicIds ?? [])].sort(),
|
|
718
865
|
...(parsed.replyTo ? { replyTo: parsed.replyTo } : {}),
|
|
719
866
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
720
867
|
...(parsed.source ? { source: parsed.source } : {}),
|
|
@@ -740,6 +887,7 @@ export class SynomemCore {
|
|
|
740
887
|
async updatePost(input) {
|
|
741
888
|
this.checkAbort();
|
|
742
889
|
const parsed = this.validate(() => updatePostSchema.parse(input));
|
|
890
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
743
891
|
const record = await this.getPostRecord(parsed.postId);
|
|
744
892
|
this.assertPostAuthor(record);
|
|
745
893
|
if (record.status === 'archived') {
|
|
@@ -756,6 +904,7 @@ export class SynomemCore {
|
|
|
756
904
|
title: parsed.title ?? record.title,
|
|
757
905
|
body: parsed.body ?? record.body,
|
|
758
906
|
tags: [...new Set(parsed.tags ?? record.tags ?? [])].sort(),
|
|
907
|
+
topicIds: [...new Set(parsed.topicIds ?? record.topicIds ?? [])].sort(),
|
|
759
908
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
760
909
|
};
|
|
761
910
|
await this.repository.insertEvent(event);
|
|
@@ -840,6 +989,7 @@ export class SynomemCore {
|
|
|
840
989
|
this.checkAbort();
|
|
841
990
|
await this.repository.assertEventCompatibility();
|
|
842
991
|
const parsed = this.validate(() => createNoteSchema.parse(input));
|
|
992
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
843
993
|
const ownerId = parsed.ownerAgentId ?? (this.actor.kind === 'agent' ? this.actor.id : undefined);
|
|
844
994
|
if (!ownerId)
|
|
845
995
|
throw new SynomemError('INVALID_INPUT', 'A human or system actor must specify ownerAgentId.');
|
|
@@ -862,6 +1012,7 @@ export class SynomemCore {
|
|
|
862
1012
|
title: parsed.title,
|
|
863
1013
|
body: parsed.body,
|
|
864
1014
|
tags: [...new Set(parsed.tags ?? [])].sort(),
|
|
1015
|
+
topicIds: [...new Set(parsed.topicIds ?? [])].sort(),
|
|
865
1016
|
visibility: 'private',
|
|
866
1017
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
867
1018
|
...(parsed.source ? { source: parsed.source } : {}),
|
|
@@ -886,6 +1037,7 @@ export class SynomemCore {
|
|
|
886
1037
|
}
|
|
887
1038
|
async reviseNote(input) {
|
|
888
1039
|
const parsed = this.validate(() => reviseNoteSchema.parse(input));
|
|
1040
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
889
1041
|
const record = await this.getNoteRecord(parsed.noteId);
|
|
890
1042
|
this.assertNoteOwner(record);
|
|
891
1043
|
if (record.status === 'archived')
|
|
@@ -906,6 +1058,7 @@ export class SynomemCore {
|
|
|
906
1058
|
title: parsed.title ?? record.current.title,
|
|
907
1059
|
body: parsed.body ?? record.current.body,
|
|
908
1060
|
tags: parsed.tags ?? record.current.tags,
|
|
1061
|
+
topicIds: parsed.topicIds ?? record.current.topicIds,
|
|
909
1062
|
visibility: 'private',
|
|
910
1063
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
911
1064
|
...(parsed.source ? { source: parsed.source } : {}),
|
|
@@ -951,6 +1104,7 @@ export class SynomemCore {
|
|
|
951
1104
|
this.checkAbort();
|
|
952
1105
|
await this.repository.assertEventCompatibility();
|
|
953
1106
|
const parsed = this.validate(() => createTaskSchema.parse(input));
|
|
1107
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
954
1108
|
const assigneeId = parsed.assigneeAgentId ?? (this.actor.kind === 'agent' ? this.actor.id : undefined);
|
|
955
1109
|
if (!assigneeId)
|
|
956
1110
|
throw new SynomemError('INVALID_INPUT', 'A human or system actor must specify assigneeAgentId.');
|
|
@@ -977,6 +1131,7 @@ export class SynomemCore {
|
|
|
977
1131
|
priority: parsed.priority ?? 3,
|
|
978
1132
|
...(parsed.due ? { due: parsed.due } : {}),
|
|
979
1133
|
tags: [...new Set(parsed.tags ?? [])].sort(),
|
|
1134
|
+
topicIds: [...new Set(parsed.topicIds ?? [])].sort(),
|
|
980
1135
|
visibility: parsed.visibility ?? this.repository.config.defaultVisibility,
|
|
981
1136
|
requiresAcceptance,
|
|
982
1137
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
@@ -1008,6 +1163,7 @@ export class SynomemCore {
|
|
|
1008
1163
|
}
|
|
1009
1164
|
async updateTask(input) {
|
|
1010
1165
|
const parsed = this.validate(() => updateTaskSchema.parse(input));
|
|
1166
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
1011
1167
|
const record = await this.getTaskRecord(parsed.taskId);
|
|
1012
1168
|
this.assertTaskParticipant(record);
|
|
1013
1169
|
if (record.status !== 'open')
|
|
@@ -1035,6 +1191,7 @@ export class SynomemCore {
|
|
|
1035
1191
|
priority: parsed.priority ?? record.current.priority,
|
|
1036
1192
|
...(due ? { due } : {}),
|
|
1037
1193
|
tags: parsed.tags ?? record.current.tags,
|
|
1194
|
+
topicIds: parsed.topicIds ?? record.current.topicIds,
|
|
1038
1195
|
visibility: parsed.visibility ?? record.current.visibility,
|
|
1039
1196
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
1040
1197
|
...(parsed.source ? { source: parsed.source } : {}),
|
|
@@ -1131,6 +1288,7 @@ export class SynomemCore {
|
|
|
1131
1288
|
this.checkAbort();
|
|
1132
1289
|
await this.repository.assertEventCompatibility();
|
|
1133
1290
|
const parsed = this.validate(() => createTodoSchema.parse(input));
|
|
1291
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
1134
1292
|
const outcome = await this.repository.transaction(async () => {
|
|
1135
1293
|
const prior = await this.priorMutation(parsed.idempotencyKey, 'todo.created');
|
|
1136
1294
|
if (prior?.type === 'todo.created')
|
|
@@ -1144,6 +1302,7 @@ export class SynomemCore {
|
|
|
1144
1302
|
priority: parsed.priority ?? 3,
|
|
1145
1303
|
...(parsed.due ? { due: parsed.due } : {}),
|
|
1146
1304
|
tags: [...new Set(parsed.tags ?? [])].sort(),
|
|
1305
|
+
topicIds: [...new Set(parsed.topicIds ?? [])].sort(),
|
|
1147
1306
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
1148
1307
|
...(parsed.source ? { source: parsed.source } : {}),
|
|
1149
1308
|
...(parsed.metadata ? { metadata: parsed.metadata } : {}),
|
|
@@ -1188,6 +1347,7 @@ export class SynomemCore {
|
|
|
1188
1347
|
async updateTodo(input) {
|
|
1189
1348
|
this.checkAbort();
|
|
1190
1349
|
const parsed = this.validate(() => updateTodoSchema.parse(input));
|
|
1350
|
+
await this.assertTopicsExist(parsed.topicIds);
|
|
1191
1351
|
const record = await this.getTodoRecord(parsed.todoId);
|
|
1192
1352
|
if (record.current.version !== parsed.expectedVersion) {
|
|
1193
1353
|
throw new SynomemError('REVISION_CONFLICT', `Todo ${parsed.todoId} is at version ${record.current.version}.`);
|
|
@@ -1208,6 +1368,7 @@ export class SynomemCore {
|
|
|
1208
1368
|
priority: parsed.priority ?? record.current.priority,
|
|
1209
1369
|
...(due ? { due } : {}),
|
|
1210
1370
|
tags: [...new Set(parsed.tags ?? record.current.tags)].sort(),
|
|
1371
|
+
topicIds: [...new Set(parsed.topicIds ?? record.current.topicIds)].sort(),
|
|
1211
1372
|
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
1212
1373
|
};
|
|
1213
1374
|
await this.repository.insertEvent(event);
|
|
@@ -1362,8 +1523,8 @@ export class SynomemCore {
|
|
|
1362
1523
|
* check that silently lags the migration runner reports a healthy database as
|
|
1363
1524
|
* broken.
|
|
1364
1525
|
*/
|
|
1365
|
-
const CURRENT_SCHEMA_VERSION =
|
|
1366
|
-
const EXPECTED_APPLIED_MIGRATIONS = [1, 2, 3, 4, 5, 6, 7];
|
|
1526
|
+
const CURRENT_SCHEMA_VERSION = 8;
|
|
1527
|
+
const EXPECTED_APPLIED_MIGRATIONS = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
1367
1528
|
export class SynomemClient extends SynomemCore {
|
|
1368
1529
|
home;
|
|
1369
1530
|
storage;
|