@musnows/scriverse 0.3.8 → 0.3.9

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/app.js CHANGED
@@ -55,11 +55,12 @@ const loginSchema = z.object({
55
55
  ...captchaFields
56
56
  }).strict();
57
57
  const userUpdateSchema = z.object({ role: z.enum(["admin", "user"]).optional(), status: z.enum(["active", "disabled"]).optional() }).strict();
58
- const memberSchema = z.object({ userId: identifier, role: z.enum(["editor", "viewer"]) }).strict();
59
- const memberRoleSchema = z.object({ role: z.enum(["editor", "viewer"]) }).strict();
58
+ const memberSchema = z.object({ userId: identifier, role: z.enum(["editor", "settings-editor", "viewer"]) }).strict();
59
+ const memberRoleSchema = z.object({ role: z.enum(["editor", "settings-editor", "viewer"]) }).strict();
60
60
  const profileSchema = z.object({ displayName: z.string().trim().min(1).max(80) }).strict();
61
61
  const passwordChangeSchema = z.object({ currentPassword: z.string().max(200), newPassword: passwordSchema }).strict();
62
62
  const changeNoteSchema = z.string().trim().max(500).optional();
63
+ const expectedVersionNoSchema = z.coerce.number().int().positive().optional();
63
64
  function validateImportedText(text) {
64
65
  if (text.length > maximumImportedTextLength)
65
66
  throw new AppError(413, "IMPORT_TEXT_TOO_LARGE", "导入文件解压后的文本超过 2000 万字符限制");
@@ -174,7 +175,7 @@ const organizationSchema = z.object({
174
175
  description: z.string().max(100_000).optional(),
175
176
  settings: z.array(z.string().trim().min(1).max(20_000)).max(200).optional(),
176
177
  memberIds: z.array(identifier).max(1000).optional()
177
- });
178
+ }).strict();
178
179
  const raceSchema = z.object({
179
180
  name: nonEmpty.max(200),
180
181
  parentRaceId: identifier.nullable().optional(),
@@ -550,9 +551,13 @@ export function createRuntime(options) {
550
551
  store.audit(request.params.workId, "work.member-removed", "user", request.params.userId);
551
552
  data(response, members);
552
553
  });
553
- app.patch("/api/works/:workId", (request, response) => data(response, store.updateWork(request.params.workId, parse(workSchema.partial(), request.body))));
554
+ app.patch("/api/works/:workId", (request, response) => {
555
+ const { expectedVersionNo, changeNote, ...input } = parse(workSchema.partial().extend({ expectedVersionNo: expectedVersionNoSchema, changeNote: changeNoteSchema }).strict(), request.body);
556
+ data(response, store.updateWork(request.params.workId, input, expectedVersionNo, "manual", null, changeNote));
557
+ });
554
558
  app.delete("/api/works/:workId", async (request, response) => {
555
- const removableStorageKeys = store.deleteWork(request.params.workId);
559
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
560
+ const removableStorageKeys = store.deleteWork(request.params.workId, input.expectedVersionNo);
556
561
  await Promise.all(removableStorageKeys.map((storageKey) => attachmentStorage.remove(storageKey)));
557
562
  noContent(response);
558
563
  });
@@ -574,10 +579,12 @@ export function createRuntime(options) {
574
579
  const mimeType = isPng ? "image/png" : isJpeg ? "image/jpeg" : isWebp ? "image/webp" : null;
575
580
  if (!mimeType)
576
581
  throw new AppError(415, "INVALID_COVER", "封面文件内容不是有效的 PNG、JPEG 或 WebP 图片");
577
- data(response, store.setWorkCover(String(request.params.workId), mimeType, bytes));
582
+ const expectedVersionNo = parse(expectedVersionNoSchema, request.body.expectedVersionNo);
583
+ data(response, store.setWorkCover(String(request.params.workId), mimeType, bytes, expectedVersionNo));
578
584
  });
579
585
  app.delete("/api/works/:workId/cover", (request, response) => {
580
- store.deleteWorkCover(request.params.workId);
586
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
587
+ store.deleteWorkCover(request.params.workId, input.expectedVersionNo);
581
588
  noContent(response);
582
589
  });
583
590
  app.get("/api/works/:workId/file-versions", (request, response) => {
@@ -585,7 +592,8 @@ export function createRuntime(options) {
585
592
  data(response, pagination ? store.listFileVersionsPage(request.params.workId, pagination) : store.listFileVersions(request.params.workId));
586
593
  });
587
594
  app.post("/api/works/:workId/file-versions/:fileVersionId/restore", (request, response) => {
588
- data(response, store.restoreFileVersion(request.params.workId, request.params.fileVersionId));
595
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
596
+ data(response, store.restoreFileVersion(request.params.workId, request.params.fileVersionId, input.expectedVersionNo));
589
597
  });
590
598
  app.post("/api/works/:workId/import", upload.single("file"), async (request, response) => {
591
599
  if (!request.file)
@@ -599,19 +607,23 @@ export function createRuntime(options) {
599
607
  ? await extractDocxText(request.file.buffer)
600
608
  : decodeUtf8ImportedText(request.file.buffer));
601
609
  const parsed = applyImportFileHints(parseNovelText(text), originalFileName);
602
- data(response, store.importNovel(String(request.params.workId), originalFileName, extension.slice(1), parsed), 201);
610
+ const mode = parse(z.enum(["append", "overwrite"]), request.body.mode ?? "overwrite");
611
+ const expectedVersionNo = parse(expectedVersionNoSchema, request.body.expectedVersionNo);
612
+ data(response, store.importNovel(String(request.params.workId), originalFileName, extension.slice(1), parsed, mode, expectedVersionNo), 201);
603
613
  });
604
614
  app.post("/api/works/:workId/volumes", (request, response) => {
605
615
  const input = parse(z.object({ title: nonEmpty.max(200), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional() }), request.body);
606
616
  data(response, store.createVolume(request.params.workId, input), 201);
607
617
  });
608
618
  app.patch("/api/volumes/:volumeId", (request, response) => {
609
- const input = parse(z.object({ title: nonEmpty.max(200).optional(), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional(), sortOrder: z.number().int().min(0).optional() }), request.body);
610
- data(response, store.updateVolume(request.params.volumeId, input));
619
+ const input = parse(z.object({ title: nonEmpty.max(200).optional(), kind: z.enum(["main", "prequel", "extra", "epilogue", "appendix"]).optional(), description: z.string().max(5_000).optional(), keywords: z.array(nonEmpty.max(100)).max(100).optional(), sortOrder: z.number().int().min(0).optional(), expectedVersionNo: expectedVersionNoSchema, changeNote: changeNoteSchema }).strict(), request.body);
620
+ const { expectedVersionNo, changeNote, ...volumeInput } = input;
621
+ data(response, store.updateVolume(request.params.volumeId, volumeInput, expectedVersionNo, "manual", null, changeNote));
611
622
  });
612
623
  app.get("/api/volumes/:volumeId", (request, response) => data(response, store.getVolume(request.params.volumeId)));
613
624
  app.delete("/api/volumes/:volumeId", (request, response) => {
614
- store.deleteVolume(request.params.volumeId);
625
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
626
+ store.deleteVolume(request.params.volumeId, input.expectedVersionNo);
615
627
  noContent(response);
616
628
  });
617
629
  app.post("/api/works/:workId/chapters", (request, response) => {
@@ -620,12 +632,13 @@ export function createRuntime(options) {
620
632
  });
621
633
  app.get("/api/chapters/:chapterId", (request, response) => data(response, store.getChapter(request.params.chapterId)));
622
634
  app.patch("/api/chapters/:chapterId", (request, response) => {
623
- const input = parse(z.object({ title: nonEmpty.max(300).optional(), content: z.string().max(2_000_000).optional(), excludedFromAnalysis: z.boolean().optional(), chapterType: chapterTypeSchema.optional(), source: z.enum(["manual", "auto"]).optional(), changeNote: changeNoteSchema }).strict(), request.body);
624
- const { source, changeNote, ...chapterInput } = input;
625
- data(response, store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote));
635
+ const input = parse(z.object({ title: nonEmpty.max(300).optional(), content: z.string().max(2_000_000).optional(), excludedFromAnalysis: z.boolean().optional(), chapterType: chapterTypeSchema.optional(), source: z.enum(["manual", "auto"]).optional(), changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
636
+ const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
637
+ data(response, store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo));
626
638
  });
627
639
  app.delete("/api/chapters/:chapterId", (request, response) => {
628
- store.deleteChapter(request.params.chapterId);
640
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
641
+ store.deleteChapter(request.params.chapterId, input.expectedVersionNo);
629
642
  noContent(response);
630
643
  });
631
644
  app.get("/api/chapters/:chapterId/versions", (request, response) => {
@@ -637,12 +650,13 @@ export function createRuntime(options) {
637
650
  data(response, pagination ? store.listChapterInsightsPage(request.params.chapterId, pagination) : store.listChapterInsights(request.params.chapterId));
638
651
  });
639
652
  app.post("/api/chapters/:chapterId/restore", (request, response) => {
640
- const input = parse(z.object({ versionNo: z.number().int().positive() }), request.body);
641
- data(response, store.restoreChapter(request.params.chapterId, input.versionNo));
653
+ const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
654
+ data(response, store.restoreChapter(request.params.chapterId, input.versionNo, input.expectedVersionNo));
642
655
  });
643
656
  app.post("/api/chapters/:chapterId/move", (request, response) => {
644
- const input = parse(z.object({ volumeId: identifier, sortOrder: z.number().int().min(0) }), request.body);
645
- data(response, store.moveChapter(request.params.chapterId, input));
657
+ const input = parse(z.object({ volumeId: identifier, sortOrder: z.number().int().min(0), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
658
+ const { expectedVersionNo, ...moveInput } = input;
659
+ data(response, store.moveChapter(request.params.chapterId, moveInput, expectedVersionNo));
646
660
  });
647
661
  app.get("/api/works/:workId/outlines", (request, response) => {
648
662
  const pagination = parsePagination(request.query);
@@ -650,11 +664,12 @@ export function createRuntime(options) {
650
664
  });
651
665
  app.get("/api/chapters/:chapterId/outline", (request, response) => data(response, store.getChapterOutline(request.params.chapterId)));
652
666
  app.put("/api/chapters/:chapterId/outline", (request, response) => {
653
- const { changeNote, ...input } = parse(chapterOutlineSchema.extend({ changeNote: changeNoteSchema }), request.body);
654
- data(response, store.upsertChapterOutline(request.params.chapterId, input, "manual", null, changeNote));
667
+ const { changeNote, expectedVersionNo, ...input } = parse(chapterOutlineSchema.extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
668
+ data(response, store.upsertChapterOutline(request.params.chapterId, input, "manual", null, changeNote, expectedVersionNo));
655
669
  });
656
670
  app.delete("/api/chapters/:chapterId/outline", (request, response) => {
657
- store.deleteChapterOutline(request.params.chapterId);
671
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
672
+ store.deleteChapterOutline(request.params.chapterId, input.expectedVersionNo);
658
673
  noContent(response);
659
674
  });
660
675
  app.get("/api/works/:workId/foreshadows", (request, response) => {
@@ -672,21 +687,27 @@ export function createRuntime(options) {
672
687
  });
673
688
  app.get("/api/foreshadows/:foreshadowId", (request, response) => data(response, store.getForeshadow(request.params.foreshadowId)));
674
689
  app.patch("/api/foreshadows/:foreshadowId", (request, response) => {
675
- const { changeNote, ...input } = parse(foreshadowSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
676
- data(response, store.updateForeshadow(request.params.foreshadowId, input, "manual", null, changeNote));
690
+ const { changeNote, expectedVersionNo, ...input } = parse(foreshadowSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
691
+ data(response, store.updateForeshadow(request.params.foreshadowId, input, "manual", null, changeNote, expectedVersionNo));
677
692
  });
678
693
  app.delete("/api/foreshadows/:foreshadowId", (request, response) => {
679
- store.deleteForeshadow(request.params.foreshadowId);
694
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
695
+ store.deleteForeshadow(request.params.foreshadowId, input.expectedVersionNo);
680
696
  noContent(response);
681
697
  });
682
698
  app.post("/api/foreshadows/:foreshadowId/occurrences", (request, response) => {
683
- data(response, store.createForeshadowOccurrence(request.params.foreshadowId, parse(foreshadowOccurrenceSchema, request.body)), 201);
699
+ const input = parse(foreshadowOccurrenceSchema.extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
700
+ const { expectedVersionNo, ...occurrenceInput } = input;
701
+ data(response, store.createForeshadowOccurrence(request.params.foreshadowId, occurrenceInput, expectedVersionNo), 201);
684
702
  });
685
703
  app.patch("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
686
- data(response, store.updateForeshadowOccurrence(request.params.occurrenceId, parse(foreshadowOccurrenceSchema.partial(), request.body)));
704
+ const input = parse(foreshadowOccurrenceSchema.partial().extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
705
+ const { expectedVersionNo, ...occurrenceInput } = input;
706
+ data(response, store.updateForeshadowOccurrence(request.params.occurrenceId, occurrenceInput, expectedVersionNo));
687
707
  });
688
708
  app.delete("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
689
- store.deleteForeshadowOccurrence(request.params.occurrenceId);
709
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
710
+ store.deleteForeshadowOccurrence(request.params.occurrenceId, input.expectedVersionNo);
690
711
  noContent(response);
691
712
  });
692
713
  app.get("/api/works/:workId/settings", (request, response) => {
@@ -696,11 +717,12 @@ export function createRuntime(options) {
696
717
  app.post("/api/works/:workId/settings", (request, response) => data(response, store.createSetting(request.params.workId, parse(settingSchema, request.body)), 201));
697
718
  app.get("/api/settings/:settingId", (request, response) => data(response, store.getSetting(request.params.settingId)));
698
719
  app.patch("/api/settings/:settingId", (request, response) => {
699
- const { changeNote, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
700
- data(response, store.updateSetting(request.params.settingId, input, "manual", null, changeNote));
720
+ const { changeNote, expectedVersionNo, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
721
+ data(response, store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo));
701
722
  });
702
723
  app.delete("/api/settings/:settingId", (request, response) => {
703
- store.deleteSetting(request.params.settingId);
724
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
725
+ store.deleteSetting(request.params.settingId, input.expectedVersionNo);
704
726
  noContent(response);
705
727
  });
706
728
  app.get("/api/works/:workId/characters", (request, response) => {
@@ -716,21 +738,34 @@ export function createRuntime(options) {
716
738
  app.post("/api/works/:workId/characters", (request, response) => data(response, store.createCharacter(request.params.workId, parse(characterSchema, request.body)), 201));
717
739
  app.get("/api/characters/:characterId", (request, response) => data(response, store.getCharacter(request.params.characterId)));
718
740
  app.patch("/api/characters/:characterId", (request, response) => {
719
- const { changeNote, ...input } = parse(characterUpdateSchema, request.body);
720
- data(response, store.updateCharacter(request.params.characterId, input, "manual", null, changeNote));
741
+ const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
742
+ data(response, store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo));
721
743
  });
722
744
  app.get("/api/characters/:characterId/versions", (request, response) => {
723
745
  const pagination = parsePagination(request.query);
724
746
  data(response, pagination ? store.listCharacterVersionsPage(request.params.characterId, pagination) : store.listCharacterVersions(request.params.characterId));
725
747
  });
726
748
  app.post("/api/characters/:characterId/restore", (request, response) => {
727
- const input = parse(z.object({ versionNo: z.number().int().positive() }), request.body);
728
- data(response, store.restoreCharacter(request.params.characterId, input.versionNo));
749
+ const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
750
+ data(response, store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo));
729
751
  });
730
752
  app.delete("/api/characters/:characterId", (request, response) => {
731
- store.deleteCharacter(request.params.characterId);
753
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
754
+ store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
732
755
  noContent(response);
733
756
  });
757
+ app.post("/api/characters/:characterId/merge", (request, response) => {
758
+ const input = parse(z.object({
759
+ targetCharacterId: identifier,
760
+ expectedTargetVersionNo: z.number().int().positive(),
761
+ expectedSourceVersionNo: z.number().int().positive()
762
+ }).strict(), request.body);
763
+ data(response, store.mergeCharacters({
764
+ reviewId: null,
765
+ sourceCharacterId: request.params.characterId,
766
+ ...input
767
+ }));
768
+ });
734
769
  app.get("/api/characters/:characterId/sections", (request, response) => {
735
770
  const pagination = parsePagination(request.query);
736
771
  data(response, pagination
@@ -744,11 +779,12 @@ export function createRuntime(options) {
744
779
  data(response, store.getCharacterProfileSection(request.params.sectionId));
745
780
  });
746
781
  app.patch("/api/character-sections/:sectionId", (request, response) => {
747
- const { changeNote, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema }).strict(), request.body);
748
- data(response, store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote));
782
+ const { changeNote, expectedVersionNo, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
783
+ data(response, store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote, expectedVersionNo));
749
784
  });
750
785
  app.delete("/api/character-sections/:sectionId", (request, response) => {
751
- store.deleteCharacterProfileSection(request.params.sectionId);
786
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
787
+ store.deleteCharacterProfileSection(request.params.sectionId, input.expectedVersionNo);
752
788
  noContent(response);
753
789
  });
754
790
  app.get("/api/character-sections/:sectionId/versions", (request, response) => {
@@ -758,8 +794,8 @@ export function createRuntime(options) {
758
794
  : store.listCharacterProfileSectionVersions(request.params.sectionId));
759
795
  });
760
796
  app.post("/api/character-sections/:sectionId/restore", (request, response) => {
761
- const input = parse(z.object({ versionNo: z.number().int().positive() }).strict(), request.body);
762
- data(response, store.restoreCharacterProfileSection(request.params.sectionId, input.versionNo));
797
+ const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
798
+ data(response, store.restoreCharacterProfileSection(request.params.sectionId, input.versionNo, input.expectedVersionNo));
763
799
  });
764
800
  app.get("/api/works/:workId/attachments", (request, response) => {
765
801
  const pagination = parsePagination(request.query);
@@ -815,13 +851,18 @@ export function createRuntime(options) {
815
851
  });
816
852
  app.get("/api/races/:raceId", (request, response) => data(response, store.getRace(request.params.raceId)));
817
853
  app.patch("/api/races/:raceId", (request, response) => {
818
- const { changeNote, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
819
- data(response, store.updateRace(request.params.raceId, input, "manual", null, changeNote));
854
+ const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
855
+ data(response, store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo));
820
856
  });
821
857
  app.delete("/api/races/:raceId", (request, response) => {
822
- store.deleteRace(request.params.raceId);
858
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
859
+ store.deleteRace(request.params.raceId, input.expectedVersionNo);
823
860
  noContent(response);
824
861
  });
862
+ app.post("/api/races/:raceId/merge", (request, response) => {
863
+ const input = parse(z.object({ targetRaceId: identifier }).strict(), request.body);
864
+ data(response, store.mergeRaces(request.params.raceId, input.targetRaceId));
865
+ });
825
866
  app.get("/api/works/:workId/organizations", (request, response) => {
826
867
  const pagination = parsePagination(request.query);
827
868
  data(response, pagination ? store.listOrganizationsPage(request.params.workId, pagination) : store.listOrganizations(request.params.workId));
@@ -831,13 +872,18 @@ export function createRuntime(options) {
831
872
  });
832
873
  app.get("/api/organizations/:organizationId", (request, response) => data(response, store.getOrganization(request.params.organizationId)));
833
874
  app.patch("/api/organizations/:organizationId", (request, response) => {
834
- const { changeNote, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
835
- data(response, store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote));
875
+ const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
876
+ data(response, store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo));
836
877
  });
837
878
  app.delete("/api/organizations/:organizationId", (request, response) => {
838
- store.deleteOrganization(request.params.organizationId);
879
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
880
+ store.deleteOrganization(request.params.organizationId, input.expectedVersionNo);
839
881
  noContent(response);
840
882
  });
883
+ app.post("/api/organizations/:organizationId/merge", (request, response) => {
884
+ const input = parse(z.object({ targetOrganizationId: identifier }).strict(), request.body);
885
+ data(response, store.mergeOrganizations(request.params.organizationId, input.targetOrganizationId));
886
+ });
841
887
  app.get("/api/works/:workId/timeline-tracks", (request, response) => {
842
888
  const pagination = parsePagination(request.query);
843
889
  data(response, pagination ? store.listTimelineTracksPage(request.params.workId, pagination) : store.listTimelineTracks(request.params.workId));
@@ -845,11 +891,12 @@ export function createRuntime(options) {
845
891
  app.post("/api/works/:workId/timeline-tracks", (request, response) => data(response, store.createTimelineTrack(request.params.workId, parse(timelineTrackSchema, request.body)), 201));
846
892
  app.get("/api/timeline-tracks/:trackId", (request, response) => data(response, store.getTimelineTrack(request.params.trackId)));
847
893
  app.patch("/api/timeline-tracks/:trackId", (request, response) => {
848
- const { changeNote, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
849
- data(response, store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote));
894
+ const { changeNote, expectedVersionNo, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
895
+ data(response, store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote, expectedVersionNo));
850
896
  });
851
897
  app.delete("/api/timeline-tracks/:trackId", (request, response) => {
852
- store.deleteTimelineTrack(request.params.trackId);
898
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
899
+ store.deleteTimelineTrack(request.params.trackId, input.expectedVersionNo);
853
900
  noContent(response);
854
901
  });
855
902
  app.get("/api/works/:workId/timeline", (request, response) => {
@@ -863,14 +910,16 @@ export function createRuntime(options) {
863
910
  name: nonEmpty.max(300),
864
911
  description: z.string().max(100_000).optional(),
865
912
  timeLabel: z.string().max(300).optional(),
866
- timeSort: z.number().finite().nullable().optional()
867
- }), request.body);
868
- data(response, store.mergeTimelineEvents(request.params.workId, input.eventIds, input), 201);
913
+ timeSort: z.number().finite().nullable().optional(),
914
+ expectedVersionNos: z.record(identifier, z.number().int().positive()).optional()
915
+ }).strict(), request.body);
916
+ const { expectedVersionNos, ...mergeInput } = input;
917
+ data(response, store.mergeTimelineEvents(request.params.workId, input.eventIds, mergeInput, expectedVersionNos), 201);
869
918
  });
870
919
  app.get("/api/timeline/:eventId", (request, response) => data(response, store.getTimelineEvent(request.params.eventId)));
871
920
  app.patch("/api/timeline/:eventId", (request, response) => {
872
- const { changeNote, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
873
- data(response, store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote));
921
+ const { changeNote, expectedVersionNo, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
922
+ data(response, store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote, expectedVersionNo));
874
923
  });
875
924
  app.post("/api/timeline/:eventId/split", (request, response) => {
876
925
  const input = parse(z.object({
@@ -879,12 +928,14 @@ export function createRuntime(options) {
879
928
  description: z.string().max(100_000).optional(),
880
929
  timeLabel: z.string().max(300).optional(),
881
930
  timeSort: z.number().finite().nullable().optional()
882
- })).min(2)
883
- }), request.body);
884
- data(response, store.splitTimelineEvent(request.params.eventId, input.parts), 201);
931
+ })).min(2),
932
+ expectedVersionNo: expectedVersionNoSchema
933
+ }).strict(), request.body);
934
+ data(response, store.splitTimelineEvent(request.params.eventId, input.parts, input.expectedVersionNo), 201);
885
935
  });
886
936
  app.delete("/api/timeline/:eventId", (request, response) => {
887
- store.deleteTimelineEvent(request.params.eventId);
937
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
938
+ store.deleteTimelineEvent(request.params.eventId, input.expectedVersionNo);
888
939
  noContent(response);
889
940
  });
890
941
  app.get("/api/works/:workId/relationships", (request, response) => {
@@ -899,11 +950,12 @@ export function createRuntime(options) {
899
950
  app.post("/api/works/:workId/relationships", (request, response) => data(response, store.createRelationship(request.params.workId, parse(relationshipSchema, request.body)), 201));
900
951
  app.get("/api/relationships/:relationshipId", (request, response) => data(response, store.getRelationship(request.params.relationshipId)));
901
952
  app.patch("/api/relationships/:relationshipId", (request, response) => {
902
- const { changeNote, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema }), request.body);
903
- data(response, store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote));
953
+ const { changeNote, expectedVersionNo, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
954
+ data(response, store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote, expectedVersionNo));
904
955
  });
905
956
  app.delete("/api/relationships/:relationshipId", (request, response) => {
906
- store.deleteRelationship(request.params.relationshipId);
957
+ const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
958
+ store.deleteRelationship(request.params.relationshipId, input.expectedVersionNo);
907
959
  noContent(response);
908
960
  });
909
961
  app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
@@ -915,8 +967,8 @@ export function createRuntime(options) {
915
967
  });
916
968
  app.post("/api/entity-versions/:entityType/:entityId/restore", (request, response) => {
917
969
  const params = parse(z.object({ entityType: versionedEntityTypeSchema, entityId: identifier }), request.params);
918
- const input = parse(z.object({ versionNo: z.number().int().positive() }), request.body);
919
- data(response, store.restoreEntityVersion(params.entityType, params.entityId, input.versionNo));
970
+ const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
971
+ data(response, store.restoreEntityVersion(params.entityType, params.entityId, input.versionNo, input.expectedVersionNo));
920
972
  });
921
973
  app.get("/api/works/:workId/reviews", (request, response) => {
922
974
  const status = typeof request.query.status === "string" ? request.query.status : undefined;