@musnows/scriverse 0.9.0 → 0.9.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/app.js +22 -0
- package/dist/app.js.map +1 -1
- package/dist/database.js +77 -2
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +206 -16
- package/dist/public/index.html +5 -2
- package/dist/public/styles.css +38 -4
- package/dist/store.js +220 -50
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -209,6 +209,13 @@ export const versionedEntityTypes = [
|
|
|
209
209
|
"chapter-outline",
|
|
210
210
|
"foreshadow"
|
|
211
211
|
];
|
|
212
|
+
const bookEntityTypes = ["character", "draft", "setting", "organization"];
|
|
213
|
+
const legacyFavoriteTables = {
|
|
214
|
+
character: "characters",
|
|
215
|
+
draft: "drafts",
|
|
216
|
+
setting: "settings",
|
|
217
|
+
organization: "organizations"
|
|
218
|
+
};
|
|
212
219
|
export const AI_CONVERSATION_STREAM_REQUEST_LEASE_MS = 3 * 60_000;
|
|
213
220
|
export const aiConversationTaskTypes = ["chat", "roleplay", "continue", "polish"];
|
|
214
221
|
export function defaultAiConversationTitle(prompt) {
|
|
@@ -329,6 +336,83 @@ export class Store {
|
|
|
329
336
|
this.migrateEntityVersionBaselines();
|
|
330
337
|
this.purgeExpiredRecycleBin();
|
|
331
338
|
}
|
|
339
|
+
entityPreferenceProjection(entityType, alias) {
|
|
340
|
+
const actor = currentRequestActor();
|
|
341
|
+
if (!actor) {
|
|
342
|
+
return {
|
|
343
|
+
columns: `${alias}.is_favorite AS user_is_favorite,
|
|
344
|
+
EXISTS (
|
|
345
|
+
SELECT 1 FROM work_entity_pins pin
|
|
346
|
+
WHERE pin.work_id = ${alias}.work_id
|
|
347
|
+
AND pin.entity_type = '${entityType}'
|
|
348
|
+
AND pin.entity_id = ${alias}.id
|
|
349
|
+
AND pin.is_pinned = 1
|
|
350
|
+
) AS is_pinned`,
|
|
351
|
+
orderBy: "is_pinned DESC, user_is_favorite DESC",
|
|
352
|
+
params: []
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
columns: `
|
|
357
|
+
EXISTS (
|
|
358
|
+
SELECT 1 FROM work_entity_favorites favorite
|
|
359
|
+
WHERE favorite.work_id = ${alias}.work_id
|
|
360
|
+
AND favorite.entity_type = '${entityType}'
|
|
361
|
+
AND favorite.entity_id = ${alias}.id
|
|
362
|
+
AND favorite.user_id = ?
|
|
363
|
+
AND favorite.is_favorite = 1
|
|
364
|
+
) AS user_is_favorite,
|
|
365
|
+
EXISTS (
|
|
366
|
+
SELECT 1 FROM work_entity_pins pin
|
|
367
|
+
WHERE pin.work_id = ${alias}.work_id
|
|
368
|
+
AND pin.entity_type = '${entityType}'
|
|
369
|
+
AND pin.entity_id = ${alias}.id
|
|
370
|
+
AND pin.is_pinned = 1
|
|
371
|
+
) AS is_pinned`,
|
|
372
|
+
orderBy: "is_pinned DESC, user_is_favorite DESC",
|
|
373
|
+
params: [actor.userId]
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
mapEntityFavorite(row) {
|
|
377
|
+
return Object.hasOwn(row, "user_is_favorite")
|
|
378
|
+
? booleanValue(row, "user_is_favorite")
|
|
379
|
+
: booleanValue(row, "is_favorite");
|
|
380
|
+
}
|
|
381
|
+
mapEntityPin(row) {
|
|
382
|
+
return booleanValue(row, "is_pinned");
|
|
383
|
+
}
|
|
384
|
+
setEntityFavorite(workId, entityType, entityId, isFavorite, legacyFavorite) {
|
|
385
|
+
const actor = currentRequestActor();
|
|
386
|
+
const previousFavorite = actor
|
|
387
|
+
? this.db.get("SELECT is_favorite FROM work_entity_favorites WHERE work_id = ? AND entity_type = ? AND entity_id = ? AND user_id = ?", workId, entityType, entityId, actor.userId)
|
|
388
|
+
: undefined;
|
|
389
|
+
const previous = actor ? booleanValue(previousFavorite ?? {}, "is_favorite") : legacyFavorite;
|
|
390
|
+
if (previous === isFavorite)
|
|
391
|
+
return previous;
|
|
392
|
+
const timestamp = now();
|
|
393
|
+
if (actor) {
|
|
394
|
+
this.db.run(`INSERT INTO work_entity_favorites (work_id, entity_type, entity_id, user_id, is_favorite, created_at, updated_at)
|
|
395
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
396
|
+
ON CONFLICT(work_id, entity_type, entity_id, user_id)
|
|
397
|
+
DO UPDATE SET is_favorite = excluded.is_favorite, updated_at = excluded.updated_at`, workId, entityType, entityId, actor.userId, isFavorite ? 1 : 0, timestamp, timestamp);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
this.db.run(`UPDATE ${legacyFavoriteTables[entityType]} SET is_favorite = ? WHERE id = ?`, isFavorite ? 1 : 0, entityId);
|
|
401
|
+
}
|
|
402
|
+
return previous;
|
|
403
|
+
}
|
|
404
|
+
setEntityPin(workId, entityType, entityId, isPinned) {
|
|
405
|
+
const previousPin = this.db.get("SELECT is_pinned FROM work_entity_pins WHERE work_id = ? AND entity_type = ? AND entity_id = ?", workId, entityType, entityId);
|
|
406
|
+
const previous = booleanValue(previousPin ?? {}, "is_pinned");
|
|
407
|
+
if (previous === isPinned)
|
|
408
|
+
return previous;
|
|
409
|
+
const timestamp = now();
|
|
410
|
+
this.db.run(`INSERT INTO work_entity_pins (work_id, entity_type, entity_id, is_pinned, pinned_by_user_id, created_at, updated_at)
|
|
411
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
412
|
+
ON CONFLICT(work_id, entity_type, entity_id)
|
|
413
|
+
DO UPDATE SET is_pinned = excluded.is_pinned, pinned_by_user_id = excluded.pinned_by_user_id, updated_at = excluded.updated_at`, workId, entityType, entityId, isPinned ? 1 : 0, currentRequestActor()?.userId ?? null, timestamp, timestamp);
|
|
414
|
+
return previous;
|
|
415
|
+
}
|
|
332
416
|
purgeExpiredRecycleBin(referenceTime = new Date()) {
|
|
333
417
|
const cutoff = new Date(referenceTime.getTime() - RECYCLE_BIN_RETENTION_DAYS * 24 * 60 * 60_000).toISOString();
|
|
334
418
|
return this.db.transaction(() => {
|
|
@@ -3748,18 +3832,20 @@ export class Store {
|
|
|
3748
3832
|
}
|
|
3749
3833
|
listDrafts(workId, draftType, includeContent = false) {
|
|
3750
3834
|
this.getWork(workId);
|
|
3751
|
-
|
|
3835
|
+
const preferences = this.entityPreferenceProjection("draft", "draft");
|
|
3836
|
+
return this.db.all(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3752
3837
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3753
3838
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3754
|
-
ORDER BY
|
|
3839
|
+
ORDER BY ${preferences.orderBy}, draft.updated_at DESC, draft.title`, ...preferences.params, workId, draftType ?? null, draftType ?? null).map((row) => this.mapDraft(row, includeContent));
|
|
3755
3840
|
}
|
|
3756
3841
|
listDraftsPage(workId, pagination, draftType, includeContent = false) {
|
|
3757
3842
|
this.getWork(workId);
|
|
3843
|
+
const preferences = this.entityPreferenceProjection("draft", "draft");
|
|
3758
3844
|
const page = paginationSql(pagination);
|
|
3759
|
-
const rows = this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
|
|
3845
|
+
const rows = this.db.all(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3760
3846
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3761
3847
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3762
|
-
ORDER BY
|
|
3848
|
+
ORDER BY ${preferences.orderBy}, draft.updated_at DESC, draft.title${page.sql}`, ...preferences.params, workId, draftType ?? null, draftType ?? null, ...page.params);
|
|
3763
3849
|
return paginated(rows.map((row) => this.mapDraft(row, includeContent)), pagination);
|
|
3764
3850
|
}
|
|
3765
3851
|
searchDrafts(workId, query, draftType, limit = 20) {
|
|
@@ -3768,22 +3854,24 @@ export class Store {
|
|
|
3768
3854
|
const normalizedQuery = query.normalize("NFKC").trim();
|
|
3769
3855
|
const escapedQuery = escapeSqlLikePattern(normalizedQuery);
|
|
3770
3856
|
const pattern = `%${escapedQuery}%`;
|
|
3857
|
+
const preferences = this.entityPreferenceProjection("draft", "draft");
|
|
3771
3858
|
const rows = normalizedQuery
|
|
3772
|
-
? this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
|
|
3859
|
+
? this.db.all(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3773
3860
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3774
3861
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3775
3862
|
AND (draft.title LIKE ? ESCAPE '\\' COLLATE NOCASE OR draft.content LIKE ? ESCAPE '\\' COLLATE NOCASE)
|
|
3776
|
-
ORDER BY CASE WHEN draft.title LIKE ? ESCAPE '\\' COLLATE NOCASE THEN 0 ELSE 1 END, draft.updated_at DESC
|
|
3777
|
-
LIMIT ?`, workId, draftType ?? null, draftType ?? null, pattern, pattern, pattern, safeLimit)
|
|
3778
|
-
: this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
|
|
3863
|
+
ORDER BY ${preferences.orderBy}, CASE WHEN draft.title LIKE ? ESCAPE '\\' COLLATE NOCASE THEN 0 ELSE 1 END, draft.updated_at DESC
|
|
3864
|
+
LIMIT ?`, ...preferences.params, workId, draftType ?? null, draftType ?? null, pattern, pattern, pattern, safeLimit)
|
|
3865
|
+
: this.db.all(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3779
3866
|
LEFT JOIN volumes volume ON volume.id = draft.volume_id
|
|
3780
3867
|
WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
|
|
3781
|
-
ORDER BY draft.updated_at DESC, draft.title LIMIT ?`, workId, draftType ?? null, draftType ?? null, safeLimit);
|
|
3868
|
+
ORDER BY ${preferences.orderBy}, draft.updated_at DESC, draft.title LIMIT ?`, ...preferences.params, workId, draftType ?? null, draftType ?? null, safeLimit);
|
|
3782
3869
|
return rows.map((row) => this.mapDraft(row, true));
|
|
3783
3870
|
}
|
|
3784
3871
|
getDraft(draftId) {
|
|
3785
|
-
const
|
|
3786
|
-
|
|
3872
|
+
const preferences = this.entityPreferenceProjection("draft", "draft");
|
|
3873
|
+
const row = this.db.get(`SELECT draft.*, volume.title AS volume_title, ${preferences.columns} FROM drafts draft
|
|
3874
|
+
LEFT JOIN volumes volume ON volume.id = draft.volume_id WHERE draft.id = ?`, ...preferences.params, draftId);
|
|
3787
3875
|
if (!row)
|
|
3788
3876
|
throw notFound("想法");
|
|
3789
3877
|
return this.mapDraft(row, true);
|
|
@@ -3793,13 +3881,30 @@ export class Store {
|
|
|
3793
3881
|
if (!draft)
|
|
3794
3882
|
throw notFound("想法");
|
|
3795
3883
|
const workId = requiredString(draft, "work_id");
|
|
3796
|
-
const previousFavorite = booleanValue(draft, "is_favorite");
|
|
3797
3884
|
this.db.transaction(() => {
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3885
|
+
const previousFavorite = this.setEntityFavorite(workId, "draft", draftId, isFavorite, booleanValue(draft, "is_favorite"));
|
|
3886
|
+
if (previousFavorite !== isFavorite) {
|
|
3887
|
+
this.audit(workId, "draft.favorite-updated", "draft", draftId, {
|
|
3888
|
+
previousFavorite,
|
|
3889
|
+
isFavorite
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
});
|
|
3893
|
+
return this.getDraft(draftId);
|
|
3894
|
+
}
|
|
3895
|
+
setDraftPin(draftId, isPinned) {
|
|
3896
|
+
const draft = this.db.get("SELECT id, work_id FROM drafts WHERE id = ?", draftId);
|
|
3897
|
+
if (!draft)
|
|
3898
|
+
throw notFound("想法");
|
|
3899
|
+
const workId = requiredString(draft, "work_id");
|
|
3900
|
+
this.db.transaction(() => {
|
|
3901
|
+
const previousPin = this.setEntityPin(workId, "draft", draftId, isPinned);
|
|
3902
|
+
if (previousPin !== isPinned) {
|
|
3903
|
+
this.audit(workId, "draft.pin-updated", "draft", draftId, {
|
|
3904
|
+
previousPin,
|
|
3905
|
+
isPinned
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3803
3908
|
});
|
|
3804
3909
|
return this.getDraft(draftId);
|
|
3805
3910
|
}
|
|
@@ -3841,7 +3946,8 @@ export class Store {
|
|
|
3841
3946
|
volumeTitle: optionalString(row, "volume_title"),
|
|
3842
3947
|
settingModule: optionalString(row, "setting_module"),
|
|
3843
3948
|
title: requiredString(row, "title"),
|
|
3844
|
-
isFavorite:
|
|
3949
|
+
isFavorite: this.mapEntityFavorite(row),
|
|
3950
|
+
isPinned: this.mapEntityPin(row),
|
|
3845
3951
|
...(includeContent ? { content } : { contentPreview: content.replace(/\s+/gu, " ").trim().slice(0, 320) }),
|
|
3846
3952
|
versionNo: this.currentEntityVersionNo("draft", requiredString(row, "id")),
|
|
3847
3953
|
createdAt: requiredString(row, "created_at"),
|
|
@@ -3905,16 +4011,19 @@ export class Store {
|
|
|
3905
4011
|
}
|
|
3906
4012
|
listSettings(workId, includeContent = true) {
|
|
3907
4013
|
this.getWork(workId);
|
|
3908
|
-
|
|
4014
|
+
const preferences = this.entityPreferenceProjection("setting", "setting");
|
|
4015
|
+
return this.db.all(`SELECT setting.*, ${preferences.columns} FROM settings setting WHERE setting.work_id = ? ORDER BY ${preferences.orderBy}, setting.locked DESC, setting.category, setting.title`, ...preferences.params, workId).map((row) => this.mapSetting(row, includeContent));
|
|
3909
4016
|
}
|
|
3910
4017
|
listSettingsPage(workId, pagination, includeContent = true) {
|
|
3911
4018
|
this.getWork(workId);
|
|
4019
|
+
const preferences = this.entityPreferenceProjection("setting", "setting");
|
|
3912
4020
|
const page = paginationSql(pagination);
|
|
3913
|
-
const rows = this.db.all(`SELECT
|
|
4021
|
+
const rows = this.db.all(`SELECT setting.*, ${preferences.columns} FROM settings setting WHERE setting.work_id = ? ORDER BY ${preferences.orderBy}, setting.locked DESC, setting.category, setting.title${page.sql}`, ...preferences.params, workId, ...page.params);
|
|
3914
4022
|
return paginated(rows.map((row) => this.mapSetting(row, includeContent)), pagination);
|
|
3915
4023
|
}
|
|
3916
4024
|
getSetting(settingId) {
|
|
3917
|
-
const
|
|
4025
|
+
const preferences = this.entityPreferenceProjection("setting", "setting");
|
|
4026
|
+
const row = this.db.get(`SELECT setting.*, ${preferences.columns} FROM settings setting WHERE setting.id = ?`, ...preferences.params, settingId);
|
|
3918
4027
|
if (!row)
|
|
3919
4028
|
throw notFound("设定");
|
|
3920
4029
|
return this.mapSetting(row);
|
|
@@ -3924,13 +4033,30 @@ export class Store {
|
|
|
3924
4033
|
if (!setting)
|
|
3925
4034
|
throw notFound("设定");
|
|
3926
4035
|
const workId = requiredString(setting, "work_id");
|
|
3927
|
-
const previousFavorite = booleanValue(setting, "is_favorite");
|
|
3928
4036
|
this.db.transaction(() => {
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
4037
|
+
const previousFavorite = this.setEntityFavorite(workId, "setting", settingId, isFavorite, booleanValue(setting, "is_favorite"));
|
|
4038
|
+
if (previousFavorite !== isFavorite) {
|
|
4039
|
+
this.audit(workId, "setting.favorite-updated", "setting", settingId, {
|
|
4040
|
+
previousFavorite,
|
|
4041
|
+
isFavorite
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
});
|
|
4045
|
+
return this.getSetting(settingId);
|
|
4046
|
+
}
|
|
4047
|
+
setSettingPin(settingId, isPinned) {
|
|
4048
|
+
const setting = this.db.get("SELECT id, work_id FROM settings WHERE id = ?", settingId);
|
|
4049
|
+
if (!setting)
|
|
4050
|
+
throw notFound("设定");
|
|
4051
|
+
const workId = requiredString(setting, "work_id");
|
|
4052
|
+
this.db.transaction(() => {
|
|
4053
|
+
const previousPin = this.setEntityPin(workId, "setting", settingId, isPinned);
|
|
4054
|
+
if (previousPin !== isPinned) {
|
|
4055
|
+
this.audit(workId, "setting.pin-updated", "setting", settingId, {
|
|
4056
|
+
previousPin,
|
|
4057
|
+
isPinned
|
|
4058
|
+
});
|
|
4059
|
+
}
|
|
3934
4060
|
});
|
|
3935
4061
|
return this.getSetting(settingId);
|
|
3936
4062
|
}
|
|
@@ -3968,7 +4094,8 @@ export class Store {
|
|
|
3968
4094
|
tags: json(requiredString(row, "tags_json"), []),
|
|
3969
4095
|
status: requiredString(row, "status"),
|
|
3970
4096
|
locked: booleanValue(row, "locked"),
|
|
3971
|
-
isFavorite:
|
|
4097
|
+
isFavorite: this.mapEntityFavorite(row),
|
|
4098
|
+
isPinned: this.mapEntityPin(row),
|
|
3972
4099
|
evidence: json(requiredString(row, "evidence_json"), []),
|
|
3973
4100
|
scope: json(requiredString(row, "scope_json"), {}),
|
|
3974
4101
|
authorNote: requiredString(row, "author_note"),
|
|
@@ -4282,19 +4409,22 @@ export class Store {
|
|
|
4282
4409
|
}
|
|
4283
4410
|
listOrganizations(workId, includeMarkdown = true) {
|
|
4284
4411
|
this.getWork(workId);
|
|
4285
|
-
const
|
|
4412
|
+
const preferences = this.entityPreferenceProjection("organization", "organization");
|
|
4413
|
+
const rows = this.db.all(`SELECT organization.*, ${preferences.columns} FROM organizations organization WHERE organization.work_id = ? ORDER BY ${preferences.orderBy}, organization.name`, ...preferences.params, workId);
|
|
4286
4414
|
const batch = this.organizationListBatch(rows);
|
|
4287
4415
|
return rows.map((row) => this.mapOrganization(row, includeMarkdown, batch));
|
|
4288
4416
|
}
|
|
4289
4417
|
listOrganizationsPage(workId, pagination, includeMarkdown = true) {
|
|
4290
4418
|
this.getWork(workId);
|
|
4419
|
+
const preferences = this.entityPreferenceProjection("organization", "organization");
|
|
4291
4420
|
const page = paginationSql(pagination);
|
|
4292
|
-
const rows = this.db.all(`SELECT
|
|
4421
|
+
const rows = this.db.all(`SELECT organization.*, ${preferences.columns} FROM organizations organization WHERE organization.work_id = ? ORDER BY ${preferences.orderBy}, organization.name${page.sql}`, ...preferences.params, workId, ...page.params);
|
|
4293
4422
|
const batch = this.organizationListBatch(rows);
|
|
4294
4423
|
return paginated(rows.map((row) => this.mapOrganization(row, includeMarkdown, batch)), pagination);
|
|
4295
4424
|
}
|
|
4296
4425
|
getOrganization(organizationId) {
|
|
4297
|
-
const
|
|
4426
|
+
const preferences = this.entityPreferenceProjection("organization", "organization");
|
|
4427
|
+
const row = this.db.get(`SELECT organization.*, ${preferences.columns} FROM organizations organization WHERE organization.id = ?`, ...preferences.params, organizationId);
|
|
4298
4428
|
if (!row)
|
|
4299
4429
|
throw notFound("组织");
|
|
4300
4430
|
return this.mapOrganization(row);
|
|
@@ -4304,13 +4434,30 @@ export class Store {
|
|
|
4304
4434
|
if (!organization)
|
|
4305
4435
|
throw notFound("组织");
|
|
4306
4436
|
const workId = requiredString(organization, "work_id");
|
|
4307
|
-
const previousFavorite = booleanValue(organization, "is_favorite");
|
|
4308
4437
|
this.db.transaction(() => {
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4438
|
+
const previousFavorite = this.setEntityFavorite(workId, "organization", organizationId, isFavorite, booleanValue(organization, "is_favorite"));
|
|
4439
|
+
if (previousFavorite !== isFavorite) {
|
|
4440
|
+
this.audit(workId, "organization.favorite-updated", "organization", organizationId, {
|
|
4441
|
+
previousFavorite,
|
|
4442
|
+
isFavorite
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4445
|
+
});
|
|
4446
|
+
return this.getOrganization(organizationId);
|
|
4447
|
+
}
|
|
4448
|
+
setOrganizationPin(organizationId, isPinned) {
|
|
4449
|
+
const organization = this.db.get("SELECT id, work_id FROM organizations WHERE id = ?", organizationId);
|
|
4450
|
+
if (!organization)
|
|
4451
|
+
throw notFound("组织");
|
|
4452
|
+
const workId = requiredString(organization, "work_id");
|
|
4453
|
+
this.db.transaction(() => {
|
|
4454
|
+
const previousPin = this.setEntityPin(workId, "organization", organizationId, isPinned);
|
|
4455
|
+
if (previousPin !== isPinned) {
|
|
4456
|
+
this.audit(workId, "organization.pin-updated", "organization", organizationId, {
|
|
4457
|
+
previousPin,
|
|
4458
|
+
isPinned
|
|
4459
|
+
});
|
|
4460
|
+
}
|
|
4314
4461
|
});
|
|
4315
4462
|
return this.getOrganization(organizationId);
|
|
4316
4463
|
}
|
|
@@ -4441,7 +4588,8 @@ export class Store {
|
|
|
4441
4588
|
name: requiredString(row, "name"),
|
|
4442
4589
|
description: requiredString(row, "description"),
|
|
4443
4590
|
isDissolved: booleanValue(row, "is_dissolved"),
|
|
4444
|
-
isFavorite:
|
|
4591
|
+
isFavorite: this.mapEntityFavorite(row),
|
|
4592
|
+
isPinned: this.mapEntityPin(row),
|
|
4445
4593
|
...(includeMarkdown
|
|
4446
4594
|
? { settings, settingsMarkdown: settingsMarkdownFromList(settings), settingsSections }
|
|
4447
4595
|
: { settings: [], settingsCount: settingsSections.length }),
|
|
@@ -4560,14 +4708,16 @@ export class Store {
|
|
|
4560
4708
|
}
|
|
4561
4709
|
listCharacters(workId, includeProfileSections = false, includeMerged = false, includeRaceMarkdown = true) {
|
|
4562
4710
|
this.getWork(workId);
|
|
4563
|
-
|
|
4711
|
+
const preferences = this.entityPreferenceProjection("character", "character");
|
|
4712
|
+
return this.db.all(`SELECT character.*, ${preferences.columns} FROM characters character WHERE character.work_id = ?${includeMerged ? "" : " AND character.merged_into_character_id IS NULL"} ORDER BY ${preferences.orderBy}, character.name`, ...preferences.params, workId)
|
|
4564
4713
|
.map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown));
|
|
4565
4714
|
}
|
|
4566
4715
|
listCharactersPage(workId, pagination, includeProfileSections = false, includeMerged = false, includeRaceMarkdown = true) {
|
|
4567
4716
|
this.getWork(workId);
|
|
4717
|
+
const preferences = this.entityPreferenceProjection("character", "character");
|
|
4568
4718
|
const page = paginationSql(pagination);
|
|
4569
4719
|
const count = this.db.get(`SELECT COUNT(*) AS count FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"}`, workId);
|
|
4570
|
-
const rows = this.db.all(`SELECT
|
|
4720
|
+
const rows = this.db.all(`SELECT character.*, ${preferences.columns} FROM characters character WHERE character.work_id = ?${includeMerged ? "" : " AND character.merged_into_character_id IS NULL"} ORDER BY ${preferences.orderBy}, character.name${page.sql}`, ...preferences.params, workId, ...page.params);
|
|
4571
4721
|
return paginated(rows.map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown)), pagination, Number(count?.count ?? 0));
|
|
4572
4722
|
}
|
|
4573
4723
|
mapCharacterProfileSection(row) {
|
|
@@ -5043,7 +5193,8 @@ export class Store {
|
|
|
5043
5193
|
return { storageKey, cleanupQueued: !this.attachmentStorageKeyInUse(storageKey) };
|
|
5044
5194
|
}
|
|
5045
5195
|
getCharacter(characterId) {
|
|
5046
|
-
const
|
|
5196
|
+
const preferences = this.entityPreferenceProjection("character", "character");
|
|
5197
|
+
const row = this.db.get(`SELECT character.*, ${preferences.columns} FROM characters character WHERE character.id = ?`, ...preferences.params, characterId);
|
|
5047
5198
|
if (!row)
|
|
5048
5199
|
throw notFound("角色");
|
|
5049
5200
|
return this.mapCharacter(row);
|
|
@@ -5056,15 +5207,33 @@ export class Store {
|
|
|
5056
5207
|
throw new AppError(409, "CHARACTER_ALREADY_MERGED", "已合并角色不能收藏");
|
|
5057
5208
|
}
|
|
5058
5209
|
const workId = requiredString(character, "work_id");
|
|
5059
|
-
const previousFavorite = booleanValue(character, "is_favorite");
|
|
5060
|
-
if (previousFavorite === isFavorite)
|
|
5061
|
-
return this.getCharacter(characterId);
|
|
5062
5210
|
this.db.transaction(() => {
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5211
|
+
const previousFavorite = this.setEntityFavorite(workId, "character", characterId, isFavorite, booleanValue(character, "is_favorite"));
|
|
5212
|
+
if (previousFavorite !== isFavorite) {
|
|
5213
|
+
this.audit(workId, "character.favorite-updated", "character", characterId, {
|
|
5214
|
+
previousFavorite,
|
|
5215
|
+
isFavorite
|
|
5216
|
+
});
|
|
5217
|
+
}
|
|
5218
|
+
});
|
|
5219
|
+
return this.getCharacter(characterId);
|
|
5220
|
+
}
|
|
5221
|
+
setCharacterPin(characterId, isPinned) {
|
|
5222
|
+
const character = this.db.get("SELECT id, work_id, merged_into_character_id FROM characters WHERE id = ?", characterId);
|
|
5223
|
+
if (!character)
|
|
5224
|
+
throw notFound("角色");
|
|
5225
|
+
if (optionalString(character, "merged_into_character_id")) {
|
|
5226
|
+
throw new AppError(409, "CHARACTER_ALREADY_MERGED", "已合并角色不能置顶");
|
|
5227
|
+
}
|
|
5228
|
+
const workId = requiredString(character, "work_id");
|
|
5229
|
+
this.db.transaction(() => {
|
|
5230
|
+
const previousPin = this.setEntityPin(workId, "character", characterId, isPinned);
|
|
5231
|
+
if (previousPin !== isPinned) {
|
|
5232
|
+
this.audit(workId, "character.pin-updated", "character", characterId, {
|
|
5233
|
+
previousPin,
|
|
5234
|
+
isPinned
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5068
5237
|
});
|
|
5069
5238
|
return this.getCharacter(characterId);
|
|
5070
5239
|
}
|
|
@@ -5342,7 +5511,8 @@ export class Store {
|
|
|
5342
5511
|
profileSectionCount,
|
|
5343
5512
|
currentState: json(requiredString(row, "current_state_json"), {}),
|
|
5344
5513
|
isDead: booleanValue(row, "is_dead"),
|
|
5345
|
-
isFavorite:
|
|
5514
|
+
isFavorite: this.mapEntityFavorite(row),
|
|
5515
|
+
isPinned: this.mapEntityPin(row),
|
|
5346
5516
|
avatarUrl: avatarSha256
|
|
5347
5517
|
? `/api/characters/${encodeURIComponent(characterId)}/avatar?v=${encodeURIComponent(avatarSha256)}`
|
|
5348
5518
|
: null,
|