@wirecat/cli-core 0.419.0 → 0.421.0
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/CHANGELOG.md +48 -0
- package/dist/cli/bot/context.d.ts +3 -2
- package/dist/cli/bot/context.js +15 -0
- package/dist/cli/config-command.js +1 -2
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -1
- package/dist/cli/messenger/context.js +3 -6
- package/dist/cli/permission-keys.d.ts +2 -0
- package/dist/cli/permission-keys.js +10 -3
- package/dist/cli/preview.d.ts +2 -2
- package/dist/cli/preview.js +4 -3
- package/dist/cli/program.js +4 -8
- package/dist/cli/settings.d.ts +0 -2
- package/dist/cli/settings.js +1 -5
- package/dist/domain/providers.d.ts +20 -2
- package/dist/domain/providers.js +23 -3
- package/dist/domain/tag-consolidation.d.ts +30 -1
- package/dist/entities/index.d.ts +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/surface.js +0 -4
- package/dist/mcp/tool.d.ts +1 -3
- package/dist/mcp/tool.js +1 -2
- package/dist/meetings/calendar-text.d.ts +13 -0
- package/dist/meetings/calendar-text.js +57 -0
- package/dist/meetings/event-store.d.ts +202 -0
- package/dist/meetings/event-store.js +1 -0
- package/dist/meetings/events.js +5 -1
- package/dist/meetings/index.d.ts +3 -0
- package/dist/meetings/index.js +2 -0
- package/dist/meetings/join-key.d.ts +7 -0
- package/dist/meetings/join-key.js +38 -0
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/meetings/model.d.ts +8 -2
- package/dist/meetings/read-answers.d.ts +6 -2
- package/dist/meetings/read-answers.js +9 -2
- package/dist/meetings/store.d.ts +2 -0
- package/dist/meetings/testing/event-store-contract.d.ts +10 -0
- package/dist/meetings/testing/event-store-contract.js +161 -0
- package/dist/meetings/testing/index.d.ts +1 -0
- package/dist/meetings/testing/index.js +1 -0
- package/dist/parity/manifest.js +0 -4
- package/dist/search/account-list.js +39 -7
- package/dist/sends/index.d.ts +1 -1
- package/dist/sends/index.js +1 -1
- package/dist/sends/permissions.d.ts +0 -13
- package/dist/sends/permissions.js +15 -142
- package/dist/services/agenda.js +5 -5
- package/dist/services/document-source.d.ts +7 -3
- package/dist/services/document-source.js +24 -12
- package/dist/services/documents/ingestion.d.ts +6 -1
- package/dist/services/documents/ingestion.js +4 -0
- package/dist/services/notes-search.js +3 -2
- package/dist/services/search-all.js +1 -0
- package/dist/services/task-tag-filters.d.ts +3 -0
- package/dist/services/task-tag-filters.js +8 -0
- package/dist/services/tasks.js +5 -9
- package/dist/store/document-transfer/index.js +12 -2
- package/dist/store/index.d.ts +2 -1
- package/dist/store/sqlite/accounts.js +1 -0
- package/dist/store/sqlite/calendar.d.ts +3 -0
- package/dist/store/sqlite/calendar.js +593 -0
- package/dist/store/sqlite/catalog.js +16 -5
- package/dist/store/sqlite/chat-rekey.js +1 -1
- package/dist/store/sqlite/chats.js +2 -2
- package/dist/store/sqlite/context-document-source.js +8 -4
- package/dist/store/sqlite/document-transfer/apply.js +24 -2
- package/dist/store/sqlite/document-transfer/collection.d.ts +19 -0
- package/dist/store/sqlite/document-transfer/collection.js +66 -0
- package/dist/store/sqlite/document-transfer/plan.js +15 -8
- package/dist/store/sqlite/document-transfer/snapshot.js +12 -7
- package/dist/store/sqlite/entity-types.d.ts +5 -1
- package/dist/store/sqlite/entity-types.js +5 -1
- package/dist/store/sqlite/events.d.ts +8 -2
- package/dist/store/sqlite/events.js +36 -12
- package/dist/store/sqlite/files.d.ts +93 -0
- package/dist/store/sqlite/files.js +293 -0
- package/dist/store/sqlite/folded.d.ts +3 -0
- package/dist/store/sqlite/folded.js +1 -0
- package/dist/store/sqlite/imported-documents.js +1 -1
- package/dist/store/sqlite/join-keys.d.ts +8 -0
- package/dist/store/sqlite/join-keys.js +27 -0
- package/dist/store/sqlite/knowledge.js +6 -6
- package/dist/store/sqlite/link-reviews.d.ts +9 -0
- package/dist/store/sqlite/link-reviews.js +12 -0
- package/dist/store/sqlite/lucene-fields.js +8 -6
- package/dist/store/sqlite/manifest.js +11 -0
- package/dist/store/sqlite/meeting-selection.js +1 -1
- package/dist/store/sqlite/meetings.js +6 -1
- package/dist/store/sqlite/migrations.generated.js +143 -0
- package/dist/store/sqlite/note-index.d.ts +8 -1
- package/dist/store/sqlite/note-index.js +14 -0
- package/dist/store/sqlite/note-search.js +6 -5
- package/dist/store/sqlite/notes.d.ts +18 -1
- package/dist/store/sqlite/notes.js +237 -40
- package/dist/store/sqlite/open-store.js +4 -0
- package/dist/store/sqlite/person-links.js +7 -2
- package/dist/store/sqlite/schema/canonical-tags.d.ts +87 -0
- package/dist/store/sqlite/schema/canonical-tags.js +8 -0
- package/dist/store/sqlite/schema/tag-consolidations.d.ts +270 -0
- package/dist/store/sqlite/schema/tag-consolidations.js +34 -0
- package/dist/store/sqlite/schema.d.ts +3589 -1634
- package/dist/store/sqlite/schema.js +191 -13
- package/dist/store/sqlite/tag-consolidation-apply.d.ts +3 -0
- package/dist/store/sqlite/tag-consolidation-apply.js +88 -0
- package/dist/store/sqlite/tag-consolidation-input.d.ts +16 -0
- package/dist/store/sqlite/tag-consolidation-input.js +52 -0
- package/dist/store/sqlite/tag-consolidation.js +51 -66
- package/dist/store/sqlite/tag-creation.js +17 -8
- package/dist/store/sqlite/tag-resolution.d.ts +5 -0
- package/dist/store/sqlite/tag-resolution.js +33 -0
- package/dist/store/sqlite/tags.d.ts +3 -1
- package/dist/store/sqlite/tags.js +70 -50
- package/dist/store/sqlite/tasks.d.ts +2 -0
- package/dist/store/sqlite/tasks.js +6 -1
- package/dist/store/sqlite/things.js +2 -1
- package/dist/store/sqlite/vocabulary.d.ts +1 -0
- package/dist/store/sqlite/vocabulary.js +30 -8
- package/dist/store/store.d.ts +6 -0
- package/dist/store/store.js +10 -0
- package/dist/store/tags.js +2 -2
- package/package.json +2 -1
- package/dist/cli/moved.d.ts +0 -18
- package/dist/cli/moved.js +0 -72
- package/dist/sends/permission-aliases.d.ts +0 -5
- package/dist/sends/permission-aliases.js +0 -50
|
@@ -12,11 +12,11 @@ const folded = (name) => {
|
|
|
12
12
|
/** Includes both label kinds and aliases: a narrower preview must not hide a collision. */
|
|
13
13
|
export const tagCreationReview = ({ database }, name, kind) => {
|
|
14
14
|
const metadata = database
|
|
15
|
-
.prepare("SELECT count(*) AS count,coalesce(sum(length(CAST(name AS BLOB))),0) AS bytes FROM
|
|
15
|
+
.prepare("SELECT count(*) AS count,coalesce(sum(length(CAST(name AS BLOB))),0) AS bytes FROM canonical_tags")
|
|
16
16
|
.get();
|
|
17
17
|
const aliasMetadata = database
|
|
18
18
|
.prepare("SELECT count(*) AS count,coalesce(sum(length(CAST(a.name AS BLOB))),0) AS bytes FROM aliases a " +
|
|
19
|
-
"JOIN
|
|
19
|
+
"JOIN canonical_tags t ON t.id=a.aliasable_id WHERE a.aliasable_type='tag' AND a.account_id IS NULL")
|
|
20
20
|
.get();
|
|
21
21
|
if (Number(metadata?.count ?? 0) > 5000 ||
|
|
22
22
|
Number(aliasMetadata?.count ?? 0) > 100000 ||
|
|
@@ -24,9 +24,11 @@ export const tagCreationReview = ({ database }, name, kind) => {
|
|
|
24
24
|
throw new CliError("validation_error", "label creation review exceeds its metadata budget", {
|
|
25
25
|
reason: "tag-review.over-budget",
|
|
26
26
|
});
|
|
27
|
-
const rows = database
|
|
27
|
+
const rows = database
|
|
28
|
+
.prepare("SELECT t.id,t.name,t.kind,r.canonical_id,r.name AS canonical_name FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id ORDER BY t.id")
|
|
29
|
+
.all();
|
|
28
30
|
const aliases = database
|
|
29
|
-
.prepare("SELECT a.id,a.aliasable_id,a.name FROM aliases a JOIN
|
|
31
|
+
.prepare("SELECT a.id,a.aliasable_id,a.name FROM aliases a JOIN canonical_tags t ON t.id=a.aliasable_id " +
|
|
30
32
|
"WHERE a.aliasable_type='tag' AND a.account_id IS NULL ORDER BY a.id")
|
|
31
33
|
.all();
|
|
32
34
|
const names = new Map();
|
|
@@ -36,14 +38,21 @@ export const tagCreationReview = ({ database }, name, kind) => {
|
|
|
36
38
|
values.push(String(alias.name));
|
|
37
39
|
names.set(id, values);
|
|
38
40
|
}
|
|
39
|
-
const
|
|
41
|
+
const scored = rows
|
|
40
42
|
.map((row) => ({
|
|
41
|
-
id: String(row.
|
|
42
|
-
name: String(row.
|
|
43
|
+
id: String(row.canonical_id),
|
|
44
|
+
name: String(row.canonical_name),
|
|
43
45
|
kind: row.kind,
|
|
44
46
|
similarity: Math.max(...[String(row.name), ...(names.get(Number(row.id)) ?? [])].map((value) => folded(value) === null ? 0 : vocabularySimilarity(name, value))),
|
|
45
47
|
}))
|
|
46
|
-
.filter((entry) => entry.similarity >= 0.65)
|
|
48
|
+
.filter((entry) => entry.similarity >= 0.65);
|
|
49
|
+
const grouped = new Map();
|
|
50
|
+
for (const entry of scored) {
|
|
51
|
+
const held = grouped.get(entry.id);
|
|
52
|
+
if (!held || entry.similarity > held.similarity)
|
|
53
|
+
grouped.set(entry.id, entry);
|
|
54
|
+
}
|
|
55
|
+
const candidates = [...grouped.values()]
|
|
47
56
|
.sort((a, b) => b.similarity - a.similarity || Number(a.id) - Number(b.id))
|
|
48
57
|
.slice(0, 10);
|
|
49
58
|
return {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type TagRedirectSnapshot } from "../../domain/tag-redirects.js";
|
|
2
|
+
import type { CacheDatabase } from "../driver.js";
|
|
3
|
+
export declare const tagCanonicalId: (database: CacheDatabase, id: number) => number;
|
|
4
|
+
export declare const tagMemberIds: (database: CacheDatabase, id: number) => number[];
|
|
5
|
+
export declare const tagRedirectSnapshot: (database: CacheDatabase) => TagRedirectSnapshot;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { createTagRedirectIndex } from "../../domain/tag-redirects.js";
|
|
3
|
+
export const tagCanonicalId = (database, id) => {
|
|
4
|
+
const row = database.prepare("SELECT canonical_id FROM resolved_tags WHERE id=?").get(id);
|
|
5
|
+
if (!row)
|
|
6
|
+
throw new CliError("not_found", "the selected tag is unavailable");
|
|
7
|
+
return Number(row.canonical_id);
|
|
8
|
+
};
|
|
9
|
+
export const tagMemberIds = (database, id) => {
|
|
10
|
+
const canonical = tagCanonicalId(database, id);
|
|
11
|
+
return database
|
|
12
|
+
.prepare("SELECT ? AS id UNION ALL SELECT source_id AS id FROM tag_redirects WHERE survivor_id=? ORDER BY id")
|
|
13
|
+
.all(canonical, canonical)
|
|
14
|
+
.map((row) => Number(row.id));
|
|
15
|
+
};
|
|
16
|
+
export const tagRedirectSnapshot = (database) => {
|
|
17
|
+
const tags = database.prepare("SELECT id FROM canonical_tags WHERE kind='tag' ORDER BY id LIMIT 4097").all();
|
|
18
|
+
const redirects = database
|
|
19
|
+
.prepare("SELECT source_id,survivor_id FROM tag_redirects ORDER BY source_id LIMIT 4097")
|
|
20
|
+
.all();
|
|
21
|
+
if (tags.length > 4096 || redirects.length > 4096)
|
|
22
|
+
throw new CliError("validation_error", "tag consolidation exceeds the complete graph budget of 4096 rows");
|
|
23
|
+
const snapshot = {
|
|
24
|
+
complete: true,
|
|
25
|
+
tags: tags.map((row) => ({ id: String(row.id), kind: "tag" })),
|
|
26
|
+
redirects: redirects.map((row) => ({ sourceId: String(row.source_id), survivorId: String(row.survivor_id) })),
|
|
27
|
+
};
|
|
28
|
+
createTagRedirectIndex(snapshot);
|
|
29
|
+
const sources = new Set(snapshot.redirects.map((row) => row.sourceId));
|
|
30
|
+
if (snapshot.redirects.some((row) => sources.has(row.survivorId)))
|
|
31
|
+
throw new CliError("configuration_error", "tag redirects must point directly to a canonical tag");
|
|
32
|
+
return snapshot;
|
|
33
|
+
};
|
|
@@ -75,7 +75,7 @@ export declare const createTag: (context: StoreContext, name: string, kind: TagK
|
|
|
75
75
|
export declare const ensureTag: (context: StoreContext, name: string) => TagRow;
|
|
76
76
|
/** The tags it did not have before, in the order given; a tag the owner adds becomes the owner's. */
|
|
77
77
|
export declare const addTags: (context: StoreContext, thing: Thing, tags: string[], source?: TaggingSource, author?: Actor) => string[];
|
|
78
|
-
/**
|
|
78
|
+
/** An explicit removal withdraws the effective label, restricted to the requested origin when supplied. */
|
|
79
79
|
export declare const removeTags: ({ database }: StoreContext, thing: Thing, tags: string[], source?: "manual" | "auto") => string[];
|
|
80
80
|
export interface Label {
|
|
81
81
|
tag: string;
|
|
@@ -83,6 +83,8 @@ export interface Label {
|
|
|
83
83
|
origin: "file" | "owner";
|
|
84
84
|
}
|
|
85
85
|
/** A thing's tags by name, with whether only a file states each. */
|
|
86
|
+
/** A tagging of something still there: a deleted document keeps its tags but is not counted or listed by them. */
|
|
87
|
+
export declare const liveTagging: (alias: string) => string;
|
|
86
88
|
export declare const labelsOf: (database: CacheDatabase, thing: Thing, tag?: string) => Label[];
|
|
87
89
|
/**
|
|
88
90
|
* Marks one topic as the thing's main one, or clears it with `null`: the topic is tagged on it if it was
|
|
@@ -2,6 +2,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { formatLocator } from "../../domain/locator.js";
|
|
3
3
|
import { normalizeVocabularyName } from "../../domain/vocabulary.js";
|
|
4
4
|
import { guardTagCreation } from "./tag-creation.js";
|
|
5
|
+
import { tagMemberIds } from "./tag-resolution.js";
|
|
5
6
|
import { inboxKey, taskAccountOf } from "./task-keys.js";
|
|
6
7
|
import { emailOf, emailThreadOf, taskRowOf } from "./things.js";
|
|
7
8
|
import { toIso } from "./values.js";
|
|
@@ -53,7 +54,9 @@ export const targetThing = ({ database }, key, target) => {
|
|
|
53
54
|
return { type: TAGGABLE[target.type], id: Number(found.id) };
|
|
54
55
|
};
|
|
55
56
|
export const tagNamed = (database, name) => {
|
|
56
|
-
const row = database
|
|
57
|
+
const row = database
|
|
58
|
+
.prepare("SELECT r.canonical_id AS id,r.name,r.kind FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id WHERE t.name=?")
|
|
59
|
+
.get(name);
|
|
57
60
|
return row ? { id: Number(row.id), name: String(row.name), kind: row.kind } : undefined;
|
|
58
61
|
};
|
|
59
62
|
/** An explicit alias reuses its target; similarity never chooses a canonical label. */
|
|
@@ -62,17 +65,18 @@ export const canonicalTagNamed = (database, name) => {
|
|
|
62
65
|
// createTag stores the name as given, so "Projects/Acme" must be found again by its folded form.
|
|
63
66
|
const foldedNamed = () => {
|
|
64
67
|
const rows = database
|
|
65
|
-
.prepare("SELECT id,
|
|
68
|
+
.prepare("SELECT r.canonical_id AS id,r.name,r.kind,t.name AS original_name FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id WHERE lower(t.name)=lower(?) LIMIT 4097")
|
|
66
69
|
.all(normalized)
|
|
67
|
-
.filter((row) => normalizeVocabularyName(String(row.
|
|
68
|
-
|
|
70
|
+
.filter((row) => normalizeVocabularyName(String(row.original_name)) === normalized);
|
|
71
|
+
const unique = [...new Map(rows.map((row) => [Number(row.id), row])).values()];
|
|
72
|
+
if (unique.length > 1)
|
|
69
73
|
throw new CliError("configuration_error", "the label name has conflicting canonical targets");
|
|
70
|
-
const row =
|
|
74
|
+
const row = unique[0];
|
|
71
75
|
return row ? { id: Number(row.id), name: String(row.name), kind: row.kind } : undefined;
|
|
72
76
|
};
|
|
73
77
|
const direct = tagNamed(database, normalized) ?? foldedNamed();
|
|
74
78
|
const aliases = database
|
|
75
|
-
.prepare("SELECT DISTINCT t.id,t.name,t.kind FROM aliases a JOIN
|
|
79
|
+
.prepare("SELECT DISTINCT t.canonical_id AS id,t.name,t.kind FROM aliases a JOIN resolved_tags t ON t.id=a.aliasable_id " +
|
|
76
80
|
"WHERE a.aliasable_type='tag' AND a.account_id IS NULL AND a.name_folded=? LIMIT 2")
|
|
77
81
|
.all(normalized);
|
|
78
82
|
if (aliases.length > 1 || (direct && aliases.some((row) => Number(row.id) !== direct.id)))
|
|
@@ -97,7 +101,9 @@ export const createTag = (context, name, kind, by, options = {}) => {
|
|
|
97
101
|
throw new CliError("validation_error", held.kind === kind ? `the ${kind} "${name}" exists` : `"${name}" is a ${held.kind}; one name is never both`, { reason: held.kind === kind ? "tag_exists" : "tag_name_clash" });
|
|
98
102
|
guardTagCreation(context, name, kind, by, options);
|
|
99
103
|
const at = now();
|
|
100
|
-
database
|
|
104
|
+
database
|
|
105
|
+
.prepare("INSERT INTO canonical_tags (name, kind, created_at, updated_at) VALUES (?, ?, ?, ?)")
|
|
106
|
+
.run(name, kind, at, at);
|
|
101
107
|
return tagNamed(database, name);
|
|
102
108
|
};
|
|
103
109
|
/** The tag of that name, made as a plain tag when no tag or topic holds the name yet. */
|
|
@@ -113,51 +119,64 @@ export const addTags = (context, thing, tags, source = "owner", author) => {
|
|
|
113
119
|
const at = now();
|
|
114
120
|
return tags.filter((name) => {
|
|
115
121
|
const tag = ensureTag(context, name);
|
|
122
|
+
const held = database
|
|
123
|
+
.prepare("SELECT source,author_type,author_id FROM taggings WHERE tag_id IN (SELECT ? UNION ALL SELECT source_id FROM tag_redirects WHERE survivor_id=?) AND taggable_type=? AND taggable_id=?")
|
|
124
|
+
.all(tag.id, tag.id, thing.type, thing.id);
|
|
125
|
+
if (held.some((row) => row.source === source && row.author_type === (author?.type ?? null) && row.author_id === (author?.id ?? null)))
|
|
126
|
+
return false;
|
|
116
127
|
const added = insert.run(tag.id, thing.type, thing.id, source, author?.type ?? null, author?.id ?? null, at, at).changes > 0;
|
|
117
128
|
if (!added && source === "owner")
|
|
118
129
|
claim.run(at, tag.id, thing.type, thing.id);
|
|
119
|
-
return added;
|
|
130
|
+
return added && held.length === 0;
|
|
120
131
|
});
|
|
121
132
|
};
|
|
122
|
-
|
|
123
|
-
.prepare("SELECT source FROM taggings WHERE tag_id=? AND taggable_type=? AND taggable_id=?")
|
|
124
|
-
.get(tagId, thing.type, thing.id)?.source;
|
|
125
|
-
/** The tags it had, in the order given. A chat's keyword claim stays apart from the owner's own tag. */
|
|
133
|
+
/** An explicit removal withdraws the effective label, restricted to the requested origin when supplied. */
|
|
126
134
|
export const removeTags = ({ database }, thing, tags, source) => {
|
|
127
135
|
const remove = database.prepare("DELETE FROM taggings WHERE tag_id=? AND taggable_type=? AND taggable_id=?");
|
|
128
136
|
return tags.filter((name) => {
|
|
129
137
|
const tag = canonicalTagNamed(database, name);
|
|
130
138
|
if (!tag)
|
|
131
139
|
return false;
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (automatic)
|
|
140
|
+
let changed = false;
|
|
141
|
+
for (const id of tagMemberIds(database, tag.id)) {
|
|
142
|
+
const held = database
|
|
143
|
+
.prepare("SELECT source FROM taggings WHERE tag_id=? AND taggable_type=? AND taggable_id=?")
|
|
144
|
+
.get(id, thing.type, thing.id);
|
|
145
|
+
const automatic = thing.type === "chat" &&
|
|
139
146
|
database
|
|
140
|
-
.prepare("
|
|
141
|
-
.
|
|
147
|
+
.prepare("SELECT 1 AS held FROM canonical_auto_tag_claims WHERE chat_id=? AND tag_id=?")
|
|
148
|
+
.get(thing.id, id);
|
|
149
|
+
if (source === "manual") {
|
|
150
|
+
if (held?.source !== "owner")
|
|
151
|
+
continue;
|
|
152
|
+
if (automatic)
|
|
153
|
+
database
|
|
154
|
+
.prepare("UPDATE taggings SET source='auto' WHERE tag_id=? AND taggable_type=? AND taggable_id=?")
|
|
155
|
+
.run(id, thing.type, thing.id);
|
|
156
|
+
else
|
|
157
|
+
remove.run(id, thing.type, thing.id);
|
|
158
|
+
changed = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (automatic) {
|
|
162
|
+
database.prepare("DELETE FROM canonical_auto_tag_claims WHERE chat_id=? AND tag_id=?").run(thing.id, id);
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
if (source === "auto") {
|
|
166
|
+
if (held?.source === "auto")
|
|
167
|
+
changed = remove.run(id, thing.type, thing.id).changes > 0 || changed;
|
|
168
|
+
}
|
|
142
169
|
else
|
|
143
|
-
remove.run(
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
if (thing.type === "chat")
|
|
147
|
-
database.prepare("DELETE FROM auto_tag_claims WHERE chat_id=? AND tag_id=?").run(thing.id, tag.id);
|
|
148
|
-
if (source === "auto") {
|
|
149
|
-
if (!automatic)
|
|
150
|
-
return false;
|
|
151
|
-
if (held !== "owner")
|
|
152
|
-
remove.run(tag.id, thing.type, thing.id);
|
|
153
|
-
return true;
|
|
170
|
+
changed = remove.run(id, thing.type, thing.id).changes > 0 || changed;
|
|
154
171
|
}
|
|
155
|
-
return
|
|
172
|
+
return changed;
|
|
156
173
|
});
|
|
157
174
|
};
|
|
158
175
|
/** A thing's tags by name, with whether only a file states each. */
|
|
176
|
+
/** A tagging of something still there: a deleted document keeps its tags but is not counted or listed by them. */
|
|
177
|
+
export const liveTagging = (alias) => `NOT (${alias}.taggable_type = 'document' AND EXISTS (SELECT 1 FROM documents dt WHERE dt.id = ${alias}.taggable_id AND dt.deleted_at IS NOT NULL))`;
|
|
159
178
|
export const labelsOf = (database, thing, tag) => database
|
|
160
|
-
.prepare(`SELECT t.name, g.source FROM taggings g JOIN
|
|
179
|
+
.prepare(`SELECT t.name,CASE WHEN min(g.source)='file' AND max(g.source)='file' THEN 'file' ELSE 'owner' END AS source FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id WHERE g.taggable_type=? AND g.taggable_id=?${tag === undefined ? "" : " AND t.name = ?"} GROUP BY t.canonical_id ORDER BY t.name`)
|
|
161
180
|
.all(thing.type, thing.id, ...(tag === undefined ? [] : [tag]))
|
|
162
181
|
.map((row) => ({ tag: String(row.name), origin: row.source === "file" ? "file" : "owner" }));
|
|
163
182
|
/**
|
|
@@ -181,45 +200,46 @@ export const setMainTopic = (context, thing, topic) => {
|
|
|
181
200
|
.run(at, tag.id, thing.type, thing.id);
|
|
182
201
|
};
|
|
183
202
|
export const tagsOf = ({ database }, key, filter) => {
|
|
184
|
-
const
|
|
203
|
+
const selectedTag = filter.tag === undefined ? undefined : (canonicalTagNamed(database, filter.tag)?.id ?? -1);
|
|
204
|
+
const tagged = selectedTag === undefined ? "" : " AND t.canonical_id=(SELECT canonical_id FROM resolved_tags WHERE id=?)";
|
|
185
205
|
const parts = [
|
|
186
206
|
{
|
|
187
207
|
type: "chat",
|
|
188
208
|
sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, c.external_id AS chat_id, c.title AS chat_title, " +
|
|
189
|
-
"NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.
|
|
190
|
-
"FROM taggings g JOIN
|
|
209
|
+
"NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
|
|
210
|
+
"FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN chats c ON c.id=g.taggable_id " +
|
|
191
211
|
"JOIN accounts a ON a.id=c.account_id WHERE g.taggable_type='chat' AND a.provider=? AND a.external_id=?",
|
|
192
212
|
params: [key.provider, key.account],
|
|
193
213
|
},
|
|
194
214
|
{
|
|
195
215
|
type: "contact",
|
|
196
216
|
sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, NULL AS chat_id, NULL AS chat_title, " +
|
|
197
|
-
"i.external_id AS person_id, i.name AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.
|
|
198
|
-
"FROM taggings g JOIN
|
|
217
|
+
"i.external_id AS person_id, i.name AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
|
|
218
|
+
"FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN identities i ON i.id=g.taggable_id " +
|
|
199
219
|
"WHERE g.taggable_type='identity' AND i.provider=?",
|
|
200
220
|
params: [key.provider],
|
|
201
221
|
},
|
|
202
222
|
{
|
|
203
223
|
type: "message",
|
|
204
224
|
sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, c.external_id AS chat_id, c.title AS chat_title, " +
|
|
205
|
-
"NULL AS person_id, NULL AS name, m.external_id AS message_id, g.source, g.taggable_id AS target_id, t.
|
|
206
|
-
"FROM taggings g JOIN
|
|
225
|
+
"NULL AS person_id, NULL AS name, m.external_id AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
|
|
226
|
+
"FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN messages m ON m.id=g.taggable_id JOIN chats c ON c.id=m.chat_id " +
|
|
207
227
|
"JOIN accounts a ON a.id=c.account_id WHERE g.taggable_type='message' AND a.provider=? AND a.external_id=?",
|
|
208
228
|
params: [key.provider, key.account],
|
|
209
229
|
},
|
|
210
230
|
{
|
|
211
231
|
type: "chat",
|
|
212
232
|
sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, th.external_id AS chat_id, th.subject AS chat_title, " +
|
|
213
|
-
"NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.
|
|
214
|
-
"FROM taggings g JOIN
|
|
233
|
+
"NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
|
|
234
|
+
"FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN email_threads th ON th.id=g.taggable_id " +
|
|
215
235
|
"JOIN accounts a ON a.id=th.account_id WHERE g.taggable_type='email_thread' AND a.provider=? AND a.external_id=?",
|
|
216
236
|
params: [key.provider, key.account],
|
|
217
237
|
},
|
|
218
238
|
{
|
|
219
239
|
type: "message",
|
|
220
240
|
sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, th.external_id AS chat_id, th.subject AS chat_title, " +
|
|
221
|
-
"NULL AS person_id, NULL AS name, e.external_id AS message_id, g.source, g.taggable_id AS target_id, t.
|
|
222
|
-
"FROM taggings g JOIN
|
|
241
|
+
"NULL AS person_id, NULL AS name, e.external_id AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, NULL AS task_id " +
|
|
242
|
+
"FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN emails e ON e.id=g.taggable_id " +
|
|
223
243
|
"JOIN email_threads th ON th.id=e.email_thread_id JOIN accounts a ON a.id=e.account_id " +
|
|
224
244
|
"WHERE g.taggable_type='email' AND e.deleted_at IS NULL AND a.provider=? AND a.external_id=?",
|
|
225
245
|
params: [key.provider, key.account],
|
|
@@ -227,9 +247,9 @@ export const tagsOf = ({ database }, key, filter) => {
|
|
|
227
247
|
{
|
|
228
248
|
type: "task",
|
|
229
249
|
sql: "SELECT t.name AS tag, g.taggable_type AS type, g.created_at, NULL AS chat_id, k.title AS chat_title, " +
|
|
230
|
-
"NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.
|
|
250
|
+
"NULL AS person_id, NULL AS name, NULL AS message_id, g.source, g.taggable_id AS target_id, t.canonical_id AS tag_id, " +
|
|
231
251
|
"COALESCE(k.package_id, k.key) AS task_id " +
|
|
232
|
-
"FROM taggings g JOIN
|
|
252
|
+
"FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id JOIN tasks k ON k.id=g.taggable_id JOIN projects p ON p.id=k.project_id " +
|
|
233
253
|
"WHERE g.taggable_type='task' AND k.deleted_at IS NULL AND p.key=?",
|
|
234
254
|
params: [inboxKey(`${key.provider}:${key.account}`)],
|
|
235
255
|
},
|
|
@@ -243,13 +263,13 @@ export const tagsOf = ({ database }, key, filter) => {
|
|
|
243
263
|
: [{ sql: filter.source === "auto" ? "x.auto = 1" : "(x.auto = 0 OR x.source = 'owner')", params: [] }]),
|
|
244
264
|
];
|
|
245
265
|
const rows = database
|
|
246
|
-
.prepare("SELECT * FROM (SELECT u.*, CASE u.type WHEN 'identity' THEN 'contact' WHEN 'email_thread' THEN 'chat' " +
|
|
266
|
+
.prepare("SELECT * FROM (SELECT tag,type,min(created_at) AS created_at,chat_id,chat_title,person_id,name,message_id,CASE WHEN max(source='owner') THEN 'owner' ELSE min(source) END AS source,target_id,tag_id,task_id,tag_type,max(auto) AS auto FROM (SELECT u.*, CASE u.type WHEN 'identity' THEN 'contact' WHEN 'email_thread' THEN 'chat' " +
|
|
247
267
|
"WHEN 'email' THEN 'message' ELSE u.type END AS tag_type, (u.type = 'chat' AND EXISTS (SELECT 1 FROM " +
|
|
248
|
-
`
|
|
268
|
+
`canonical_auto_tag_claims ac JOIN resolved_tags rt ON rt.id=ac.tag_id WHERE ac.chat_id = u.target_id AND rt.canonical_id = u.tag_id)) AS auto FROM (${union}) u) GROUP BY tag,type,target_id) x ` +
|
|
249
269
|
`WHERE ${where.length === 0 ? "1" : where.map(({ sql }) => `(${sql})`).join(" AND ")} ` +
|
|
250
270
|
"ORDER BY x.tag, x.type, x.created_at, x.chat_id, x.person_id, x.message_id" +
|
|
251
271
|
(filter.window ? " LIMIT ? OFFSET ?" : ""))
|
|
252
|
-
.all(...chosen.flatMap(({ params }) => (
|
|
272
|
+
.all(...chosen.flatMap(({ params }) => (selectedTag === undefined ? params : [...params, selectedTag])), ...where.flatMap(({ params }) => params), ...(filter.window ? [filter.window.limit, filter.window.offset] : []));
|
|
253
273
|
const result = rows.map((row) => {
|
|
254
274
|
const type = TAG_TYPE[String(row.type)];
|
|
255
275
|
const chatId = row.chat_id == null ? undefined : String(row.chat_id);
|
|
@@ -23,6 +23,8 @@ export interface StoreTaskStore extends Omit<Required<TaskStore>, "reviewDuplica
|
|
|
23
23
|
unlistedCategories(): Promise<UnlistedCategory[]>;
|
|
24
24
|
/** What the task package calls a task — its id, or a key like `IN1A2B3C4D-3` — as the row's `tasks.id`. */
|
|
25
25
|
rowOf(id: string): number | undefined;
|
|
26
|
+
/** A canonical/alias-aware label predicate for the task query's `t` row. */
|
|
27
|
+
tagWhere(name: string): Promise<Where>;
|
|
26
28
|
/** One account's tasks a list query keeps, oldest first, each with its title and last change. */
|
|
27
29
|
query(account: string, where: Where | undefined, window: {
|
|
28
30
|
limit: number;
|
|
@@ -4,6 +4,7 @@ import { taskCategoriesReader } from "../shared-config.js";
|
|
|
4
4
|
import { actorOfOrigin, originOfActor } from "./actors.js";
|
|
5
5
|
import { folded } from "./folded.js";
|
|
6
6
|
import { linkKind } from "./link-kinds.js";
|
|
7
|
+
import { canonicalTagNamed } from "./tags.js";
|
|
7
8
|
import { taskSimilarityOver } from "./task-candidates.js";
|
|
8
9
|
import { savedTaskCategories, saveTaskCategories, storeTaskCategories, unlistedTaskCategories, } from "./task-category-settings.js";
|
|
9
10
|
import { CLEAR_SUGGESTION, closeSuggestionOf, SUGGESTION_COLUMNS } from "./task-close.js";
|
|
@@ -11,7 +12,7 @@ import { taskCloseOver } from "./task-close-store.js";
|
|
|
11
12
|
import { inboxKey, typedKey } from "./task-keys.js";
|
|
12
13
|
import { taskPairsOver } from "./task-pairs.js";
|
|
13
14
|
import { taskRowOf, thingOf } from "./things.js";
|
|
14
|
-
import { write } from "./transaction.js";
|
|
15
|
+
import { read, write } from "./transaction.js";
|
|
15
16
|
import { recordTaskEvent, workTasksOver } from "./work-tasks.js";
|
|
16
17
|
export { inboxKey, linkInbox, taskAccountOf } from "./task-keys.js";
|
|
17
18
|
export { taskIdOf, taskRowOf } from "./things.js";
|
|
@@ -95,6 +96,10 @@ export const taskStoreOver = (context) => {
|
|
|
95
96
|
categoriesFile: () => context.sharedConfig,
|
|
96
97
|
saveCategories: async (categories) => write(database, () => saveTaskCategories(database, categories, now())),
|
|
97
98
|
unlistedCategories: async () => unlistedTaskCategories(database, listOf()),
|
|
99
|
+
tagWhere: async (name) => read(database, () => ({
|
|
100
|
+
sql: "EXISTS (SELECT 1 FROM taggings g JOIN resolved_tags r ON r.id=g.tag_id WHERE g.taggable_type='task' AND g.taggable_id=t.id AND r.canonical_id=(SELECT canonical_id FROM resolved_tags WHERE id=?))",
|
|
101
|
+
params: [canonicalTagNamed(database, name)?.id ?? -1],
|
|
102
|
+
})),
|
|
98
103
|
query: async (account, where, { limit, offset }) => {
|
|
99
104
|
const project = inbox(account, false);
|
|
100
105
|
if (project === undefined)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatLocator } from "../../domain/locator.js";
|
|
2
|
+
import { filesAccountSql } from "../../domain/providers.js";
|
|
2
3
|
import { canonicalReference, formatReference, parseReference } from "../../domain/references.js";
|
|
3
4
|
import { canonicalMeetingReference, formatMeetingReference, parseMeetingReference } from "../../meetings/index.js";
|
|
4
5
|
import { ENTITY_TABLES, isEntityType } from "./entity-types.js";
|
|
@@ -72,7 +73,7 @@ export const thingOf = (database, reference) => {
|
|
|
72
73
|
const id = storeId(parsed.id);
|
|
73
74
|
return id === undefined
|
|
74
75
|
? undefined
|
|
75
|
-
: found("account", database.prepare(
|
|
76
|
+
: found("account", database.prepare(`SELECT id FROM accounts WHERE id = ? AND ${filesAccountSql()}`).get(id));
|
|
76
77
|
}
|
|
77
78
|
case "task": {
|
|
78
79
|
const id = taskRowOf(database, parsed.id);
|
|
@@ -4,6 +4,7 @@ import { normalizeTag } from "../../domain/tags.js";
|
|
|
4
4
|
import { normalizeVocabularyName, vocabularySimilarity } from "../../domain/vocabulary.js";
|
|
5
5
|
import { tagConsolidationStoreOver } from "./tag-consolidation.js";
|
|
6
6
|
import { tagCreationReview } from "./tag-creation.js";
|
|
7
|
+
import { liveTagging } from "./tags.js";
|
|
7
8
|
import { read, write } from "./transaction.js";
|
|
8
9
|
const kinds = ["project", "tag", "topic"];
|
|
9
10
|
const cancelled = (signal) => {
|
|
@@ -40,7 +41,7 @@ export const vocabularyStoreOver = (context) => {
|
|
|
40
41
|
const projects = options.kind === undefined || options.kind === "project";
|
|
41
42
|
const tags = options.kind !== "project";
|
|
42
43
|
const whereProjects = "account_id IS NULL AND deleted_at IS NULL AND (? IS NULL OR scope=?)";
|
|
43
|
-
const whereTags = "(? IS NULL OR kind=?)";
|
|
44
|
+
const whereTags = "(? IS NULL OR kind=?) AND id=canonical_id";
|
|
44
45
|
const tagKind = options.kind === "project" ? null : (options.kind ?? null);
|
|
45
46
|
const metadata = [];
|
|
46
47
|
if (projects)
|
|
@@ -49,7 +50,7 @@ export const vocabularyStoreOver = (context) => {
|
|
|
49
50
|
.all(options.scope ?? null, options.scope ?? null, maxEntries + 1));
|
|
50
51
|
if (tags)
|
|
51
52
|
metadata.push(...database
|
|
52
|
-
.prepare(`SELECT kind,id,length(CAST(name AS BLOB)) AS bytes FROM
|
|
53
|
+
.prepare(`SELECT kind,id,length(CAST(name AS BLOB)) AS bytes FROM resolved_tags WHERE ${whereTags} LIMIT ?`)
|
|
53
54
|
.all(tagKind, tagKind, maxEntries + 1));
|
|
54
55
|
if (metadata.length > maxEntries)
|
|
55
56
|
throw new CliError("validation_error", "vocabulary exceeds the selected entry budget", {
|
|
@@ -57,7 +58,7 @@ export const vocabularyStoreOver = (context) => {
|
|
|
57
58
|
});
|
|
58
59
|
const aliasSelection = "a.account_id IS NULL AND ((? AND a.aliasable_type='project' AND EXISTS " +
|
|
59
60
|
"(SELECT 1 FROM projects p WHERE p.id=a.aliasable_id AND p.account_id IS NULL AND p.deleted_at IS NULL AND (? IS NULL OR p.scope=?))) " +
|
|
60
|
-
"OR (? AND a.aliasable_type='tag' AND EXISTS (SELECT 1 FROM
|
|
61
|
+
"OR (? AND a.aliasable_type='tag' AND EXISTS (SELECT 1 FROM resolved_tags t WHERE t.id=a.aliasable_id AND (? IS NULL OR t.kind=?))))";
|
|
61
62
|
const aliasParams = [
|
|
62
63
|
Number(projects),
|
|
63
64
|
options.scope ?? null,
|
|
@@ -71,11 +72,26 @@ export const vocabularyStoreOver = (context) => {
|
|
|
71
72
|
.get(...aliasParams);
|
|
72
73
|
if (Number(aliases?.count ?? 0) > maxEntries * 20)
|
|
73
74
|
throw new CliError("validation_error", "vocabulary aliases exceed the selected entry budget");
|
|
74
|
-
const
|
|
75
|
+
const retainedWhere = "t.id<>r.canonical_id AND (? IS NULL OR t.kind=?)";
|
|
76
|
+
const retainedMetadata = tags
|
|
77
|
+
? database
|
|
78
|
+
.prepare(`SELECT count(*) AS count,coalesce(sum(length(CAST(t.name AS BLOB))),0) AS bytes FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id WHERE ${retainedWhere}`)
|
|
79
|
+
.get(tagKind, tagKind)
|
|
80
|
+
: undefined;
|
|
81
|
+
if (Number(retainedMetadata?.count ?? 0) > maxEntries)
|
|
82
|
+
throw new CliError("validation_error", "retained tag names exceed the selected entry budget");
|
|
83
|
+
const bytes = metadata.reduce((total, row) => total + Number(row.bytes), 0) +
|
|
84
|
+
Number(aliases?.bytes ?? 0) +
|
|
85
|
+
Number(retainedMetadata?.bytes ?? 0);
|
|
75
86
|
if (bytes > maxReadBytes)
|
|
76
87
|
throw new CliError("validation_error", "vocabulary exceeds the selected input byte budget", {
|
|
77
88
|
reason: "vocabulary.over-byte-budget",
|
|
78
89
|
});
|
|
90
|
+
const retained = tags
|
|
91
|
+
? database
|
|
92
|
+
.prepare(`SELECT t.id,t.name,r.canonical_id FROM canonical_tags t JOIN resolved_tags r ON r.id=t.id WHERE ${retainedWhere} ORDER BY t.id LIMIT ?`)
|
|
93
|
+
.all(tagKind, tagKind, maxEntries + 1)
|
|
94
|
+
: [];
|
|
79
95
|
const entries = [];
|
|
80
96
|
if (projects)
|
|
81
97
|
entries.push(...database
|
|
@@ -92,7 +108,7 @@ export const vocabularyStoreOver = (context) => {
|
|
|
92
108
|
})));
|
|
93
109
|
if (tags)
|
|
94
110
|
entries.push(...database
|
|
95
|
-
.prepare(`SELECT t.id,t.name,t.kind,(SELECT count(*) FROM taggings g
|
|
111
|
+
.prepare(`SELECT t.id,t.name,t.kind,(SELECT count(*) FROM (SELECT DISTINCT g.taggable_type,g.taggable_id FROM taggings g JOIN resolved_tags r ON r.id=g.tag_id WHERE r.canonical_id=t.id AND ${liveTagging("g")}))+(SELECT count(*) FROM (SELECT DISTINCT l.from_type,l.from_id,l.anchor FROM links l JOIN resolved_tags r ON r.id=l.to_id WHERE l.to_type='tag' AND r.canonical_id=t.id AND l.kind='labelled')) AS uses FROM resolved_tags t WHERE ${whereTags} ORDER BY id`)
|
|
96
112
|
.all(tagKind, tagKind)
|
|
97
113
|
.map((row) => ({
|
|
98
114
|
kind: row.kind,
|
|
@@ -105,11 +121,15 @@ export const vocabularyStoreOver = (context) => {
|
|
|
105
121
|
})));
|
|
106
122
|
const byTarget = new Map(entries.map((entry) => [`${entry.kind === "project" ? "project" : "tag"}:${entry.id}`, entry]));
|
|
107
123
|
for (const row of database
|
|
108
|
-
.prepare(`SELECT a.id,a.aliasable_type,a.aliasable_id,a.name FROM aliases a WHERE ${aliasSelection} ORDER BY a.id`)
|
|
124
|
+
.prepare(`SELECT a.id,a.aliasable_type,CASE WHEN a.aliasable_type='tag' THEN (SELECT canonical_id FROM resolved_tags WHERE id=a.aliasable_id) ELSE a.aliasable_id END AS aliasable_id,a.name FROM aliases a WHERE ${aliasSelection} ORDER BY a.id`)
|
|
109
125
|
.all(...aliasParams))
|
|
110
126
|
byTarget
|
|
111
127
|
.get(`${row.aliasable_type}:${row.aliasable_id}`)
|
|
112
128
|
?.aliases.push({ id: String(row.id), name: String(row.name) });
|
|
129
|
+
for (const row of retained)
|
|
130
|
+
byTarget
|
|
131
|
+
.get(`tag:${row.canonical_id}`)
|
|
132
|
+
?.aliases.push({ id: `tag:${row.id}`, name: String(row.name), retained: true });
|
|
113
133
|
cancelled(options.signal);
|
|
114
134
|
return {
|
|
115
135
|
entries,
|
|
@@ -253,13 +273,15 @@ export const vocabularyStoreOver = (context) => {
|
|
|
253
273
|
removeVocabularyAlias: async (target, aliasId, options = {}) => write(database, () => {
|
|
254
274
|
const current = writable(options);
|
|
255
275
|
const entry = targetEntry(current, target);
|
|
276
|
+
if (entry.aliases.some((alias) => alias.id === aliasId && alias.retained))
|
|
277
|
+
throw new CliError("validation_error", "aliasId names a retained tag name; its original record cannot be removed as an alias");
|
|
256
278
|
idOf(aliasId);
|
|
257
279
|
const held = entry.aliases.find((alias) => alias.id === aliasId);
|
|
258
280
|
if (!held)
|
|
259
281
|
return { target: { kind: entry.kind, id: entry.id }, alias: null, changed: false };
|
|
260
282
|
database
|
|
261
|
-
.prepare("DELETE FROM aliases WHERE id=? AND aliasable_type=? AND aliasable_id=? AND account_id IS NULL")
|
|
262
|
-
.run(idOf(aliasId), target.kind === "project" ? "project" : "tag", idOf(target.id));
|
|
283
|
+
.prepare("DELETE FROM aliases WHERE id=? AND aliasable_type=? AND (aliasable_id=? OR (aliasable_type='tag' AND aliasable_id IN (SELECT id FROM resolved_tags WHERE canonical_id=?))) AND account_id IS NULL")
|
|
284
|
+
.run(idOf(aliasId), target.kind === "project" ? "project" : "tag", idOf(target.id), idOf(target.id));
|
|
263
285
|
cancelled(options.signal);
|
|
264
286
|
return { target: { kind: entry.kind, id: entry.id }, alias: held, changed: true };
|
|
265
287
|
}),
|
package/dist/store/store.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContextEvidenceReader, ContextIndex, ContextRecordReader } from "../context/evidence.js";
|
|
2
|
+
import type { EventStore } from "../meetings/index.js";
|
|
2
3
|
import { type AccountStore } from "./account-store.js";
|
|
3
4
|
import { type AccountsStore } from "./accounts.js";
|
|
4
5
|
import { type AttachmentsStore } from "./attachments.js";
|
|
@@ -20,6 +21,7 @@ import { type BotUpdateStore } from "./sqlite/bot-updates.js";
|
|
|
20
21
|
import { type CatalogStore } from "./sqlite/catalog.js";
|
|
21
22
|
import { type MailStore } from "./sqlite/emails.js";
|
|
22
23
|
import { type FactsStore } from "./sqlite/facts.js";
|
|
24
|
+
import { type FilesStore } from "./sqlite/files.js";
|
|
23
25
|
import { type InvolvementStore } from "./sqlite/involvements.js";
|
|
24
26
|
import { type KnowledgeStore } from "./sqlite/knowledge.js";
|
|
25
27
|
import { type MeetingVectors } from "./sqlite/meeting-vectors.js";
|
|
@@ -79,6 +81,10 @@ export interface MessageStore {
|
|
|
79
81
|
/** Notes, the links between anything and anything, and the owner's organisations and projects. */
|
|
80
82
|
readonly notes: NotesStore;
|
|
81
83
|
readonly catalog: CatalogStore;
|
|
84
|
+
/** Where each document is kept: its files, their folders and paths. */
|
|
85
|
+
readonly files: FilesStore;
|
|
86
|
+
/** Calendars and their entries, the source records of events: the `EventStore` a calendar CLI writes through. */
|
|
87
|
+
readonly calendar: EventStore;
|
|
82
88
|
/** Choices that hold until replaced, each with its evidence. */
|
|
83
89
|
readonly meetingFindings: MeetingFindingsStore;
|
|
84
90
|
/** Cited facts and follow-ups about one project from stored messages, mail and documents, for review. */
|
package/dist/store/store.js
CHANGED
|
@@ -15,11 +15,13 @@ import * as accounts from "./sqlite/accounts.js";
|
|
|
15
15
|
import { agentActionsStoreOver } from "./sqlite/agent-actions.js";
|
|
16
16
|
import * as attachmentTexts from "./sqlite/attachment-texts.js";
|
|
17
17
|
import { botUpdateStoreOver } from "./sqlite/bot-updates.js";
|
|
18
|
+
import { eventStoreOver } from "./sqlite/calendar.js";
|
|
18
19
|
import { catalogStoreOver } from "./sqlite/catalog.js";
|
|
19
20
|
import { contextEvidenceOver, contextIndexOver, contextRecordsOver } from "./sqlite/context-evidence.js";
|
|
20
21
|
import { mailStoreOver } from "./sqlite/emails.js";
|
|
21
22
|
import { evidenceFindingsStoreOver } from "./sqlite/evidence-findings.js";
|
|
22
23
|
import { factsStoreOver } from "./sqlite/facts.js";
|
|
24
|
+
import { filesStoreOver } from "./sqlite/files.js";
|
|
23
25
|
import { involvementStoreOver } from "./sqlite/involvements.js";
|
|
24
26
|
import { knowledgeStoreOver } from "./sqlite/knowledge.js";
|
|
25
27
|
import { logPruner } from "./sqlite/log-pruning.js";
|
|
@@ -96,6 +98,8 @@ const storeOver = (context) => {
|
|
|
96
98
|
const knowledge = once(() => knowledgeStoreOver(context));
|
|
97
99
|
const notes = once(() => ({ ...notesStoreOver(context), ...noteSearchOver(context) }));
|
|
98
100
|
const catalog = once(() => catalogStoreOver(context));
|
|
101
|
+
const files = once(() => filesStoreOver(context));
|
|
102
|
+
const calendar = once(() => eventStoreOver(context));
|
|
99
103
|
const meetingFindings = once(() => meetingFindingsStoreOver(context));
|
|
100
104
|
const evidenceFindings = once(() => evidenceFindingsStoreOver(context));
|
|
101
105
|
const facts = once(() => factsStoreOver(context));
|
|
@@ -183,6 +187,12 @@ const storeOver = (context) => {
|
|
|
183
187
|
get catalog() {
|
|
184
188
|
return catalog();
|
|
185
189
|
},
|
|
190
|
+
get files() {
|
|
191
|
+
return files();
|
|
192
|
+
},
|
|
193
|
+
get calendar() {
|
|
194
|
+
return calendar();
|
|
195
|
+
},
|
|
186
196
|
get meetingFindings() {
|
|
187
197
|
return meetingFindings();
|
|
188
198
|
},
|
package/dist/store/tags.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as tagQueries from "./sqlite/tags.js";
|
|
2
|
-
import { write } from "./sqlite/transaction.js";
|
|
2
|
+
import { read, write } from "./sqlite/transaction.js";
|
|
3
3
|
export const tagsStoreOver = (context) => {
|
|
4
4
|
const { database } = context;
|
|
5
5
|
return {
|
|
@@ -17,6 +17,6 @@ export const tagsStoreOver = (context) => {
|
|
|
17
17
|
});
|
|
18
18
|
return removed;
|
|
19
19
|
},
|
|
20
|
-
tags: async (key, filter = {}) => tagQueries.tagsOf(context, key, filter),
|
|
20
|
+
tags: async (key, filter = {}) => read(database, () => tagQueries.tagsOf(context, key, filter)),
|
|
21
21
|
};
|
|
22
22
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.421.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -214,6 +214,7 @@
|
|
|
214
214
|
"@types/pngjs": "6.0.5",
|
|
215
215
|
"@vitest/coverage-v8": "^5.0.2",
|
|
216
216
|
"@wirecat/cli-base": "0.53.0",
|
|
217
|
+
"@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
|
|
217
218
|
"commander": "15.0.0",
|
|
218
219
|
"cspell": "10.3.6",
|
|
219
220
|
"drizzle-kit": "1.0.0-rc.4",
|