@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
|
@@ -425,7 +425,7 @@ export const replaceAutoTags = (context, key, chatId, algorithm, matches) => {
|
|
|
425
425
|
const pk = metadataChatPk(context, key, chatId);
|
|
426
426
|
const db = context.database;
|
|
427
427
|
db.prepare("DELETE FROM taggings WHERE taggable_type='chat' AND taggable_id=? AND source='auto'").run(pk);
|
|
428
|
-
db.prepare("DELETE FROM
|
|
428
|
+
db.prepare("DELETE FROM canonical_auto_tag_claims WHERE chat_id=?").run(pk);
|
|
429
429
|
const claims = new Map();
|
|
430
430
|
for (const match of matches) {
|
|
431
431
|
const tagId = ensureTag(context, match.tag).id;
|
|
@@ -436,7 +436,7 @@ export const replaceAutoTags = (context, key, chatId, algorithm, matches) => {
|
|
|
436
436
|
});
|
|
437
437
|
}
|
|
438
438
|
for (const [tagId, match] of claims) {
|
|
439
|
-
db.prepare("INSERT INTO
|
|
439
|
+
db.prepare("INSERT INTO canonical_auto_tag_claims (chat_id, tag_id, algorithm, score, fields, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)").run(pk, tagId, algorithm, match.score, JSON.stringify(match.fields), context.now(), context.now());
|
|
440
440
|
db.prepare("INSERT INTO taggings (tag_id, taggable_type, taggable_id, source, created_at, updated_at) VALUES (?, 'chat', ?, 'auto', ?, ?) ON CONFLICT DO NOTHING").run(tagId, pk, context.now(), context.now());
|
|
441
441
|
}
|
|
442
442
|
};
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { PREFERRED_FILE } from "./files.js";
|
|
2
|
+
// A document's preferred file holds its link and edit time; rows without one keep them in front matter.
|
|
2
3
|
const drive = "CASE WHEN json_valid(n.front_matter) THEN json_extract(n.front_matter,'$.source')='google-drive' ELSE 0 END";
|
|
3
4
|
const field = (name) => `CASE WHEN (${drive}) THEN
|
|
4
5
|
CASE WHEN json_type(n.front_matter,'$.${name}')='text' THEN json_extract(n.front_matter,'$.${name}') END END`;
|
|
6
|
+
const preferred = (column) => `(SELECT f.${column} FROM files f JOIN documents d ON d.id = f.document_id WHERE f.document_id = n.id
|
|
7
|
+
ORDER BY ${PREFERRED_FILE} LIMIT 1)`;
|
|
5
8
|
const modified = `round(unixepoch(${field("modifiedAt")},'subsec')*1000)`;
|
|
6
|
-
|
|
7
|
-
export const
|
|
9
|
+
const remoteTime = `coalesce(${preferred("external_updated_at")},n.external_updated_at,${modified})`;
|
|
10
|
+
export const contextDocumentTime = `coalesce(${remoteTime},n.updated_at)`;
|
|
11
|
+
export const contextDocumentTimeKind = `CASE WHEN (${remoteTime}) IS NOT NULL
|
|
8
12
|
THEN 'source-modified' ELSE 'stored-update' END`;
|
|
9
|
-
export const contextDocumentUrl = `coalesce(n.url,${field("sourceUrl")})`;
|
|
13
|
+
export const contextDocumentUrl = `coalesce(${preferred("url")},n.url,${field("sourceUrl")})`;
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { folderProviderOf } from "../../../domain/providers.js";
|
|
2
3
|
import { botNamed, ownerPerson } from "../actors.js";
|
|
4
|
+
import { syncFile } from "../files.js";
|
|
3
5
|
import { canonicalTagNamed } from "../tags.js";
|
|
4
6
|
import { referenceOfThing } from "../things.js";
|
|
5
7
|
import { write } from "../transaction.js";
|
|
6
8
|
import { copyPeople } from "./people.js";
|
|
7
9
|
import { planTransfer } from "./plan.js";
|
|
8
10
|
import { TRANSFER_TABLES } from "./snapshot.js";
|
|
11
|
+
/** A file exported before store version 32 names its folder `folder`; here it gets the provider its format says. */
|
|
12
|
+
const providerHere = ({ provider, name, settings }) => {
|
|
13
|
+
if (provider !== "folder" || String(name ?? "").startsWith("google-drive:"))
|
|
14
|
+
return provider ?? null;
|
|
15
|
+
return folderProviderOf(formatOf(settings) === "markdown" ? "markdown" : "obsidian");
|
|
16
|
+
};
|
|
17
|
+
const formatOf = (settings) => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(String(settings))?.format;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
9
25
|
const COLUMNS = {
|
|
10
26
|
document: "account_id external_id kind title location file_name extension url storage local_path mime size content_hash front_matter body normalized_text extraction extraction_error language revision export_path external_created_at external_updated_at metadata created_at updated_at deleted_at",
|
|
11
27
|
note: "notable_type notable_id title body author_type author_id revision created_at updated_at deleted_at",
|
|
@@ -54,7 +70,11 @@ export const applyTransfer = (database, snapshot, sourceKey, bindings, expectedF
|
|
|
54
70
|
const organization = snapshot.collection.organization_id == null
|
|
55
71
|
? undefined
|
|
56
72
|
: mapped(mappings, "organization", snapshot.collection.organization_id);
|
|
57
|
-
collectionId = insert(database, "accounts", "provider external_id name created_at settings updated_at scope organization_id", {
|
|
73
|
+
collectionId = insert(database, "accounts", "provider external_id name created_at settings updated_at scope organization_id", {
|
|
74
|
+
...snapshot.collection,
|
|
75
|
+
provider: providerHere(snapshot.collection),
|
|
76
|
+
organization_id: organization?.id ?? null,
|
|
77
|
+
});
|
|
58
78
|
}
|
|
59
79
|
mappings.set(`account:${String(snapshot.collection.id)}`, { type: "account", id: collectionId });
|
|
60
80
|
const inserted = new Set();
|
|
@@ -79,6 +99,8 @@ export const applyTransfer = (database, snapshot, sourceKey, bindings, expectedF
|
|
|
79
99
|
values.project_id = null;
|
|
80
100
|
}
|
|
81
101
|
const id = insert(database, TRANSFER_TABLES[entity.kind], COLUMNS[entity.kind], values);
|
|
102
|
+
if (entity.kind === "document")
|
|
103
|
+
syncFile(database, now(), id, { seen: false });
|
|
82
104
|
mappings.set(entity.key, { type: entity.kind, id });
|
|
83
105
|
inserted.add(entity.key);
|
|
84
106
|
}
|
|
@@ -105,7 +127,7 @@ export const applyTransfer = (database, snapshot, sourceKey, bindings, expectedF
|
|
|
105
127
|
const tag = canonicalTagNamed(database, String(tagging.tag_name));
|
|
106
128
|
const tagId = tag
|
|
107
129
|
? Number(tag.id)
|
|
108
|
-
: insert(database, "
|
|
130
|
+
: insert(database, "canonical_tags", "name kind created_at updated_at", {
|
|
109
131
|
name: tagging.tag_name ?? null,
|
|
110
132
|
kind: tagging.tag_kind ?? null,
|
|
111
133
|
created_at: tagging.created_at ?? null,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../../driver.js";
|
|
2
|
+
type Row = Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* The name a transfer gives a collection. A Drive collection is `google-drive:<principal>` before version 31 (the
|
|
5
|
+
* stand-in folder's name) and after it (the account's key), so transfers between stores on either side still match.
|
|
6
|
+
*/
|
|
7
|
+
export declare const collectionNameOf: (row: Row) => string;
|
|
8
|
+
/** At most two rows: the account a collection name stands for, and a second one only when that is ambiguous. */
|
|
9
|
+
export declare const collectionAccounts: (database: CacheDatabase, collection: string) => Row[];
|
|
10
|
+
/** The stand-in folder version 31 moved an account's documents from, as its `settings.movedFrom` records it. */
|
|
11
|
+
export declare const movedFromOf: (row: Row | undefined) => {
|
|
12
|
+
external_id?: unknown;
|
|
13
|
+
} | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* A source store from before version 31 holds Drive in a `folder` named `google-drive:<principal>`; the copy gets the
|
|
16
|
+
* shape version 31 gives it, so it lands in (or creates) the `google-drive` account, never a new stand-in folder.
|
|
17
|
+
*/
|
|
18
|
+
export declare const currentCollection: (row: Row) => Row;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { folderProviderSql, isDocumentAccountProvider } from "../../../domain/providers.js";
|
|
2
|
+
/** `google-drive:<principal>` names a documents account; anything else is a notes folder's name. */
|
|
3
|
+
const documentsKeyOf = (collection) => {
|
|
4
|
+
const colon = collection.indexOf(":");
|
|
5
|
+
const provider = collection.slice(0, colon);
|
|
6
|
+
return colon > 0 && isDocumentAccountProvider(provider)
|
|
7
|
+
? { provider, account: collection.slice(colon + 1) }
|
|
8
|
+
: undefined;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The name a transfer gives a collection. A Drive collection is `google-drive:<principal>` before version 31 (the
|
|
12
|
+
* stand-in folder's name) and after it (the account's key), so transfers between stores on either side still match.
|
|
13
|
+
*/
|
|
14
|
+
export const collectionNameOf = (row) => isDocumentAccountProvider(String(row.provider))
|
|
15
|
+
? `${String(row.provider)}:${String(row.external_id)}`
|
|
16
|
+
: String(row.name);
|
|
17
|
+
/** At most two rows: the account a collection name stands for, and a second one only when that is ambiguous. */
|
|
18
|
+
export const collectionAccounts = (database, collection) => {
|
|
19
|
+
const key = documentsKeyOf(collection);
|
|
20
|
+
return key
|
|
21
|
+
? database
|
|
22
|
+
.prepare(`SELECT * FROM accounts WHERE (provider=? AND external_id=?) OR (${folderProviderSql()} AND name=?) ORDER BY id LIMIT 2`)
|
|
23
|
+
.all(key.provider, key.account, collection)
|
|
24
|
+
: database
|
|
25
|
+
.prepare(`SELECT * FROM accounts WHERE ${folderProviderSql()} AND name=? ORDER BY id LIMIT 2`)
|
|
26
|
+
.all(collection);
|
|
27
|
+
};
|
|
28
|
+
/** The stand-in folder version 31 moved an account's documents from, as its `settings.movedFrom` records it. */
|
|
29
|
+
export const movedFromOf = (row) => {
|
|
30
|
+
if (typeof row?.settings !== "string")
|
|
31
|
+
return undefined;
|
|
32
|
+
try {
|
|
33
|
+
const moved = JSON.parse(row.settings).movedFrom;
|
|
34
|
+
return typeof moved === "object" && moved !== null ? moved : undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* A source store from before version 31 holds Drive in a `folder` named `google-drive:<principal>`; the copy gets the
|
|
42
|
+
* shape version 31 gives it, so it lands in (or creates) the `google-drive` account, never a new stand-in folder.
|
|
43
|
+
*/
|
|
44
|
+
export const currentCollection = (row) => {
|
|
45
|
+
const key = isDocumentAccountProvider(String(row.provider)) ? undefined : documentsKeyOf(String(row.name));
|
|
46
|
+
if (!key)
|
|
47
|
+
return row;
|
|
48
|
+
const settings = (() => {
|
|
49
|
+
try {
|
|
50
|
+
return typeof row.settings === "string" ? JSON.parse(row.settings) : {};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
return {
|
|
57
|
+
...row,
|
|
58
|
+
provider: key.provider,
|
|
59
|
+
external_id: key.account,
|
|
60
|
+
name: "Google Drive",
|
|
61
|
+
settings: JSON.stringify({
|
|
62
|
+
...settings,
|
|
63
|
+
movedFrom: { id: row.id, external_id: row.external_id, name: row.name },
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { folderProviderSql } from "../../../domain/providers.js";
|
|
2
3
|
import { digest } from "../../document-transfer/digest.js";
|
|
3
4
|
import { ownerPerson } from "../actors.js";
|
|
5
|
+
import { tagMemberIds } from "../tag-resolution.js";
|
|
4
6
|
import { canonicalTagNamed } from "../tags.js";
|
|
5
7
|
import { stateOfThing, thingOf } from "../things.js";
|
|
8
|
+
import { collectionAccounts, collectionNameOf, movedFromOf } from "./collection.js";
|
|
6
9
|
import { planPeople } from "./people.js";
|
|
7
10
|
export const planTransfer = (database, snapshot, sourceKey, bindings) => {
|
|
8
11
|
const mappings = new Map();
|
|
@@ -10,21 +13,20 @@ export const planTransfer = (database, snapshot, sourceKey, bindings) => {
|
|
|
10
13
|
const snapshotHash = digest(snapshot);
|
|
11
14
|
const bindingsHash = digest(Object.entries(bindings).sort(([a], [b]) => a.localeCompare(b)));
|
|
12
15
|
const collection = String(snapshot.collection.external_id);
|
|
13
|
-
const targets = database
|
|
14
|
-
.prepare("SELECT * FROM accounts WHERE provider='folder' AND name=? ORDER BY id LIMIT 2")
|
|
15
|
-
.all(String(snapshot.collection.name));
|
|
16
|
+
const targets = collectionAccounts(database, collectionNameOf(snapshot.collection));
|
|
16
17
|
if (targets.length > 1)
|
|
17
18
|
conflicts.add("the destination has duplicate document collection identities");
|
|
18
19
|
const target = targets[0];
|
|
19
|
-
if (!target &&
|
|
20
|
+
if (!target &&
|
|
21
|
+
database.prepare(`SELECT id FROM accounts WHERE ${folderProviderSql()} AND external_id=?`).get(collection))
|
|
20
22
|
conflicts.add("source collection identity is held by another destination collection");
|
|
21
23
|
const hasReceipts = database
|
|
22
24
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='document_annotation_transfers'")
|
|
23
25
|
.get();
|
|
24
26
|
const receipt = hasReceipts &&
|
|
25
27
|
database
|
|
26
|
-
.prepare("SELECT * FROM document_annotation_transfers WHERE source_key=? AND collection
|
|
27
|
-
.get(sourceKey, collection);
|
|
28
|
+
.prepare("SELECT * FROM document_annotation_transfers WHERE source_key=? AND collection IN (?,?,?) LIMIT 1")
|
|
29
|
+
.get(sourceKey, collection, String(movedFromOf(snapshot.collection)?.external_id ?? collection), String(movedFromOf(target)?.external_id ?? collection));
|
|
28
30
|
if (receipt && (receipt.snapshot_hash !== snapshotHash || receipt.bindings_hash !== bindingsHash))
|
|
29
31
|
conflicts.add("the source or binding decisions changed after a completed transfer; keep the original store selected");
|
|
30
32
|
if (target && target.scope !== snapshot.collection.scope)
|
|
@@ -82,9 +84,10 @@ export const planTransfer = (database, snapshot, sourceKey, bindings) => {
|
|
|
82
84
|
continue;
|
|
83
85
|
}
|
|
84
86
|
else if (external.collection && external.external_id) {
|
|
87
|
+
const holders = JSON.stringify(collectionAccounts(database, external.collection).map(({ id }) => Number(id)));
|
|
85
88
|
const row = database
|
|
86
|
-
.prepare("SELECT
|
|
87
|
-
.get(
|
|
89
|
+
.prepare("SELECT id FROM documents WHERE account_id IN (SELECT value FROM json_each(?)) AND external_id=? AND deleted_at IS NULL ORDER BY id LIMIT 1")
|
|
90
|
+
.get(holders, external.external_id);
|
|
88
91
|
if (row)
|
|
89
92
|
found = { type: "document", id: Number(row.id) };
|
|
90
93
|
}
|
|
@@ -106,7 +109,11 @@ export const planTransfer = (database, snapshot, sourceKey, bindings) => {
|
|
|
106
109
|
if (target && target.organization_id !== organization)
|
|
107
110
|
conflicts.add("the destination collection has a different organization binding");
|
|
108
111
|
for (const tagging of snapshot.taggings) {
|
|
112
|
+
if (Number(tagging.tag_group_size ?? 1) > 1)
|
|
113
|
+
conflicts.add(`source tag ${String(tagging.tag_id)} belongs to a consolidated group; annotation transfer cannot preserve that group yet`);
|
|
109
114
|
const tag = canonicalTagNamed(database, String(tagging.tag_name));
|
|
115
|
+
if (tag && tagMemberIds(database, tag.id).length > 1)
|
|
116
|
+
conflicts.add(`destination tag ${String(tag.id)} belongs to a consolidated group; annotation transfer cannot preserve independent contributions yet`);
|
|
110
117
|
if (tag && tag.name !== tagging.tag_name)
|
|
111
118
|
conflicts.add(`destination vocabulary resolves source tag ${String(tagging.tag_id)} to another canonical label`);
|
|
112
119
|
if (tag && tag.kind !== tagging.tag_kind)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { filesAccountSql } from "../../../domain/providers.js";
|
|
2
3
|
import { digest } from "../../document-transfer/digest.js";
|
|
3
4
|
import { referenceOfThing, storedThing } from "../things.js";
|
|
4
5
|
import { read } from "../transaction.js";
|
|
6
|
+
import { collectionAccounts, collectionNameOf, currentCollection } from "./collection.js";
|
|
5
7
|
export const TRANSFER_TABLES = { document: "documents", note: "notes", memory: "memories", fact: "facts" };
|
|
6
8
|
const MAX_ENTITIES = 1000;
|
|
7
9
|
const MAX_AUXILIARY = 10000;
|
|
@@ -14,9 +16,7 @@ const valuesOf = (row) => Object.fromEntries(Object.entries(row).map(([key, valu
|
|
|
14
16
|
return [key, value];
|
|
15
17
|
}));
|
|
16
18
|
export const transferSnapshot = (database, collection) => read(database, () => {
|
|
17
|
-
const accounts = database
|
|
18
|
-
.prepare("SELECT * FROM accounts WHERE provider='folder' AND name=? ORDER BY id LIMIT 2")
|
|
19
|
-
.all(collection);
|
|
19
|
+
const accounts = collectionAccounts(database, collection);
|
|
20
20
|
if (accounts.length > 1)
|
|
21
21
|
throw new CliError("configuration_error", "the source has duplicate document collection identities; select a repaired copy before transferring");
|
|
22
22
|
const account = accounts[0];
|
|
@@ -35,6 +35,11 @@ export const transferSnapshot = (database, collection) => read(database, () => {
|
|
|
35
35
|
const entities = new Map();
|
|
36
36
|
const links = new Map();
|
|
37
37
|
const taggings = new Map();
|
|
38
|
+
const canonicalTags = Boolean(database.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='canonical_tags'").get());
|
|
39
|
+
const tagTable = canonicalTags ? "canonical_tags" : "tags";
|
|
40
|
+
const resolution = canonicalTags
|
|
41
|
+
? "r.canonical_id AS tag_canonical_id,(SELECT count(*) FROM resolved_tags members WHERE members.canonical_id=r.canonical_id) AS tag_group_size"
|
|
42
|
+
: "t.id AS tag_canonical_id,1 AS tag_group_size";
|
|
38
43
|
const dependencies = new Map();
|
|
39
44
|
const depend = (type, id) => {
|
|
40
45
|
if (type != null && id != null)
|
|
@@ -84,7 +89,7 @@ export const transferSnapshot = (database, collection) => read(database, () => {
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
for (const row of database
|
|
87
|
-
.prepare(
|
|
92
|
+
.prepare(`SELECT g.*,t.name AS tag_name,t.kind AS tag_kind,${resolution} FROM taggings g JOIN ${tagTable} t ON t.id=g.tag_id ${canonicalTags ? "JOIN resolved_tags r ON r.id=t.id" : ""} WHERE g.taggable_type=? AND g.taggable_id=? ORDER BY g.id LIMIT 10001`)
|
|
88
93
|
.all(kind, Number(values.id))) {
|
|
89
94
|
if (taggings.size >= MAX_AUXILIARY)
|
|
90
95
|
throw new CliError("validation_error", "annotation transfer exceeds 10000 tag assignments");
|
|
@@ -160,10 +165,10 @@ export const transferSnapshot = (database, collection) => read(database, () => {
|
|
|
160
165
|
}
|
|
161
166
|
if (thing.type === "document") {
|
|
162
167
|
const doc = database
|
|
163
|
-
.prepare(
|
|
168
|
+
.prepare(`SELECT a.provider,a.external_id AS account,a.name,d.external_id,d.body,d.content_hash,d.deleted_at FROM documents d JOIN accounts a ON a.id=d.account_id WHERE d.id=? AND ${filesAccountSql("a.provider")}`)
|
|
164
169
|
.get(thing.id);
|
|
165
170
|
if (doc) {
|
|
166
|
-
item.collection =
|
|
171
|
+
item.collection = collectionNameOf({ provider: doc.provider, external_id: doc.account, name: doc.name });
|
|
167
172
|
item.external_id = String(doc.external_id);
|
|
168
173
|
const content = bounded({ body: doc.body, content_hash: doc.content_hash, deleted_at: doc.deleted_at });
|
|
169
174
|
item.document_fingerprint = digest(content);
|
|
@@ -172,7 +177,7 @@ export const transferSnapshot = (database, collection) => read(database, () => {
|
|
|
172
177
|
external.push(item);
|
|
173
178
|
}
|
|
174
179
|
return {
|
|
175
|
-
collection: bounded(account),
|
|
180
|
+
collection: bounded(currentCollection(account)),
|
|
176
181
|
entities: [...entities.values()].sort((a, b) => a.key.localeCompare(b.key)),
|
|
177
182
|
links: [...links.values()].sort((a, b) => Number(a.id) - Number(b.id)),
|
|
178
183
|
taggings: [...taggings.values()].sort((a, b) => Number(a.id) - Number(b.id)),
|
|
@@ -7,13 +7,17 @@ export declare const ENTITY_TABLES: {
|
|
|
7
7
|
readonly account: "accounts";
|
|
8
8
|
readonly attachment: "attachments";
|
|
9
9
|
readonly bot: "bots";
|
|
10
|
+
readonly calendar_entry: "calendar_entries";
|
|
10
11
|
readonly chat: "chats";
|
|
11
12
|
readonly conversation: "conversations";
|
|
12
13
|
readonly document: "documents";
|
|
13
14
|
readonly document_picture: "documents";
|
|
14
15
|
readonly email: "emails";
|
|
15
16
|
readonly email_thread: "email_threads";
|
|
17
|
+
readonly event: "events";
|
|
18
|
+
readonly event_series: "event_series";
|
|
16
19
|
readonly fact: "facts";
|
|
20
|
+
readonly file: "files";
|
|
17
21
|
readonly identity: "identities";
|
|
18
22
|
readonly image: "attachments";
|
|
19
23
|
readonly meeting: "meetings";
|
|
@@ -26,7 +30,7 @@ export declare const ENTITY_TABLES: {
|
|
|
26
30
|
readonly person: "persons";
|
|
27
31
|
readonly place: "places";
|
|
28
32
|
readonly project: "projects";
|
|
29
|
-
readonly tag: "
|
|
33
|
+
readonly tag: "canonical_tags";
|
|
30
34
|
readonly task: "tasks";
|
|
31
35
|
};
|
|
32
36
|
export type EntityType = keyof typeof ENTITY_TABLES;
|
|
@@ -6,13 +6,17 @@ export const ENTITY_TABLES = {
|
|
|
6
6
|
account: "accounts",
|
|
7
7
|
attachment: "attachments",
|
|
8
8
|
bot: "bots",
|
|
9
|
+
calendar_entry: "calendar_entries",
|
|
9
10
|
chat: "chats",
|
|
10
11
|
conversation: "conversations",
|
|
11
12
|
document: "documents",
|
|
12
13
|
document_picture: "documents",
|
|
13
14
|
email: "emails",
|
|
14
15
|
email_thread: "email_threads",
|
|
16
|
+
event: "events",
|
|
17
|
+
event_series: "event_series",
|
|
15
18
|
fact: "facts",
|
|
19
|
+
file: "files",
|
|
16
20
|
identity: "identities",
|
|
17
21
|
image: "attachments",
|
|
18
22
|
meeting: "meetings",
|
|
@@ -25,7 +29,7 @@ export const ENTITY_TABLES = {
|
|
|
25
29
|
person: "persons",
|
|
26
30
|
place: "places",
|
|
27
31
|
project: "projects",
|
|
28
|
-
tag: "
|
|
32
|
+
tag: "canonical_tags",
|
|
29
33
|
task: "tasks",
|
|
30
34
|
};
|
|
31
35
|
export const isEntityType = (type) => Object.hasOwn(ENTITY_TABLES, type);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { QueryAst } from "@wirecat/cli-base/query";
|
|
2
|
-
import type
|
|
2
|
+
import { type Event, type EventCandidateFilter, type EventSeries, type NewRecord } from "../../meetings/index.js";
|
|
3
3
|
import type { CacheDatabase } from "../driver.js";
|
|
4
4
|
/** JSON columns keep a missing value as SQL NULL, never the text `null`, so a read gives back what was saved. */
|
|
5
5
|
export declare const toJson: (value: unknown) => string | null;
|
|
@@ -13,8 +13,14 @@ export declare const page: (filter: {
|
|
|
13
13
|
limit?: number;
|
|
14
14
|
offset?: number;
|
|
15
15
|
}) => [number, number];
|
|
16
|
+
export declare const eventOf: (row: Record<string, unknown>) => Event;
|
|
16
17
|
export declare const events: (database: CacheDatabase, query?: QueryAst) => Event[];
|
|
17
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* The events a meeting may belong to: overlapping in time, with exclusive ends so back-to-back meetings do not both
|
|
20
|
+
* match, and sharing its series or its room (`join_key`, never the link with its passcode). The room is matched on
|
|
21
|
+
* other meetings and on calendar entries, so an event with no meeting yet is found. An event without an end, and a
|
|
22
|
+
* meeting with none, occupy their start instant.
|
|
23
|
+
*/
|
|
18
24
|
export declare const eventCandidates: (database: CacheDatabase, filter: EventCandidateFilter) => Event[];
|
|
19
25
|
export declare const createEvent: (database: CacheDatabase, input: NewRecord<Event>, now: number) => Event;
|
|
20
26
|
export declare const createEventSeries: (database: CacheDatabase, input: NewRecord<EventSeries>, now: number) => EventSeries;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { joinKeyOf, } from "../../meetings/index.js";
|
|
1
2
|
import { queryFilter } from "./query-filter.js";
|
|
3
|
+
import { write } from "./transaction.js";
|
|
2
4
|
/** JSON columns keep a missing value as SQL NULL, never the text `null`, so a read gives back what was saved. */
|
|
3
5
|
export const toJson = (value) => value === null || value === undefined ? null : JSON.stringify(value);
|
|
4
6
|
export const fromJson = (value) => (value === null || value === undefined ? null : JSON.parse(String(value)));
|
|
@@ -14,7 +16,7 @@ export const page = (filter) => {
|
|
|
14
16
|
throw new Error("Invalid offset");
|
|
15
17
|
return [filter.limit ?? -1, filter.offset ?? 0];
|
|
16
18
|
};
|
|
17
|
-
const eventOf = (row) => ({
|
|
19
|
+
export const eventOf = (row) => ({
|
|
18
20
|
id: Number(row.id),
|
|
19
21
|
eventSeriesId: int(row.event_series_id),
|
|
20
22
|
title: str(row.title),
|
|
@@ -25,6 +27,9 @@ const eventOf = (row) => ({
|
|
|
25
27
|
timezone: str(row.timezone),
|
|
26
28
|
origin: String(row.origin),
|
|
27
29
|
deletedAt: int(row.deleted_at),
|
|
30
|
+
status: str(row.status),
|
|
31
|
+
allDay: bool(row.all_day),
|
|
32
|
+
sourceDeletedAt: int(row.source_deleted_at),
|
|
28
33
|
createdAt: Number(row.created_at),
|
|
29
34
|
updatedAt: Number(row.updated_at),
|
|
30
35
|
});
|
|
@@ -52,17 +57,28 @@ export const events = (database, query) => {
|
|
|
52
57
|
.all(...(where?.params ?? []))
|
|
53
58
|
.map(eventOf);
|
|
54
59
|
};
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The events a meeting may belong to: overlapping in time, with exclusive ends so back-to-back meetings do not both
|
|
62
|
+
* match, and sharing its series or its room (`join_key`, never the link with its passcode). The room is matched on
|
|
63
|
+
* other meetings and on calendar entries, so an event with no meeting yet is found. An event without an end, and a
|
|
64
|
+
* meeting with none, occupy their start instant.
|
|
65
|
+
*/
|
|
66
|
+
export const eventCandidates = (database, filter) => {
|
|
67
|
+
const key = joinKeyOf(filter.joinUrl) ?? "";
|
|
68
|
+
const end = Math.max(filter.endsAt, filter.startsAt + 1);
|
|
69
|
+
return database
|
|
70
|
+
.prepare(`SELECT e.* FROM events e
|
|
58
71
|
WHERE e.deleted_at IS NULL AND e.starts_at IS NOT NULL
|
|
59
|
-
AND e.starts_at
|
|
72
|
+
AND e.starts_at < ? AND coalesce(e.ends_at, e.starts_at + 1) > ?
|
|
60
73
|
AND (e.event_series_id IN (SELECT event_series_id FROM meeting_series WHERE id = ?)
|
|
61
|
-
OR (?
|
|
62
|
-
|
|
74
|
+
OR (? <> '' AND (EXISTS (SELECT 1 FROM meetings m
|
|
75
|
+
WHERE m.event_id = e.id AND m.deleted_at IS NULL AND m.join_key = ?)
|
|
76
|
+
OR EXISTS (SELECT 1 FROM calendar_entries ce WHERE ce.event_id = e.id AND ce.deleted_at IS NULL
|
|
77
|
+
AND ce.status <> 'cancelled' AND ce.join_key = ?))))
|
|
63
78
|
ORDER BY e.id`)
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
.all(end, filter.startsAt, filter.meetingSeriesId, key, key, key)
|
|
80
|
+
.map(eventOf);
|
|
81
|
+
};
|
|
66
82
|
export const createEvent = (database, input, now) => {
|
|
67
83
|
if (input.eventSeriesId !== null && !exists(database, "event_series", input.eventSeriesId))
|
|
68
84
|
throw new Error("Event series not found");
|
|
@@ -88,7 +104,15 @@ export const linkMeeting = (database, id, eventId, now, mode = "auto") => {
|
|
|
88
104
|
throw new Error("Meeting not found");
|
|
89
105
|
if (!exists(database, "events", eventId))
|
|
90
106
|
throw new Error("Event not found");
|
|
91
|
-
database
|
|
92
|
-
.prepare(
|
|
93
|
-
|
|
107
|
+
write(database, () => {
|
|
108
|
+
const from = database.prepare("SELECT event_id FROM meetings WHERE id = ?").get(id)?.event_id;
|
|
109
|
+
const changed = database
|
|
110
|
+
.prepare(`UPDATE meetings SET event_id = ?, updated_at = ? WHERE id = ?${mode === "owner" ? "" : " AND event_id IS NULL"}`)
|
|
111
|
+
.run(eventId, now, id).changes;
|
|
112
|
+
if (changed && int(from) !== eventId)
|
|
113
|
+
database
|
|
114
|
+
.prepare("INSERT INTO entity_link_events (linkable_type, linkable_id, entity_type, from_entity_id, to_entity_id, method, " +
|
|
115
|
+
"confidence, author, created_at) VALUES ('meeting', ?, 'event', ?, ?, ?, NULL, ?, ?)")
|
|
116
|
+
.run(id, int(from), eventId, mode === "owner" ? "owner" : "rules", mode === "owner" ? "owner" : "rules", now);
|
|
117
|
+
});
|
|
94
118
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
type Row = Record<string, unknown>;
|
|
3
|
+
/** A local file of a document, where it is and its own facts. */
|
|
4
|
+
export interface LocalPlace {
|
|
5
|
+
accountId: number;
|
|
6
|
+
path: string;
|
|
7
|
+
contentHash: string | null;
|
|
8
|
+
frontMatter: unknown;
|
|
9
|
+
}
|
|
10
|
+
interface Placed {
|
|
11
|
+
id: number;
|
|
12
|
+
path: string;
|
|
13
|
+
}
|
|
14
|
+
/** A file's document is the one its writer saved it as (`import`); a change of it is logged as every link is. */
|
|
15
|
+
export declare const logFileLink: (database: CacheDatabase, now: number, fileId: number, from: unknown, to: number | null, method?: string, author?: string) => {
|
|
16
|
+
changes: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Writes the `files` row of one document from the document as it is now: the document's source record. A Drive
|
|
20
|
+
* document (catalog metadata or Drive front matter) is keyed by its Drive file id and placed under its first parent;
|
|
21
|
+
* a local one by its path, under folder rows made from that path. `seen` marks a sync or import that returned it.
|
|
22
|
+
* `place` names a local file of the document other than the one its own columns name: a copy in another folder.
|
|
23
|
+
*/
|
|
24
|
+
export declare const syncFile: (database: CacheDatabase, now: number, documentId: number, { seen, depth, place, method, }?: {
|
|
25
|
+
seen?: boolean;
|
|
26
|
+
depth?: number;
|
|
27
|
+
place?: LocalPlace;
|
|
28
|
+
method?: string;
|
|
29
|
+
}) => Placed | null;
|
|
30
|
+
export declare const FILES_BACKFILL = "files_backfill_after";
|
|
31
|
+
/**
|
|
32
|
+
* Writes the `files` row of every document stored before version 32, from the document after the cursor the
|
|
33
|
+
* migration left, in short writes: another process waits for one batch at most, and a stop resumes where it was.
|
|
34
|
+
*/
|
|
35
|
+
export declare const fillFiles: (database: CacheDatabase, now: () => number, { batch }?: {
|
|
36
|
+
batch?: number | undefined;
|
|
37
|
+
}) => number;
|
|
38
|
+
/** A source record of a document: one item in one place (`files` row). Times are ISO 8601. */
|
|
39
|
+
export interface StoredFile {
|
|
40
|
+
id: string;
|
|
41
|
+
accountId: string;
|
|
42
|
+
externalId: string;
|
|
43
|
+
kind: "file" | "folder" | "page";
|
|
44
|
+
name: string;
|
|
45
|
+
path: string;
|
|
46
|
+
parentId: string | null;
|
|
47
|
+
/** `document:<id>`, or `null` for a local folder. */
|
|
48
|
+
document: string | null;
|
|
49
|
+
mime: string | null;
|
|
50
|
+
size: number | null;
|
|
51
|
+
contentHash: string | null;
|
|
52
|
+
url: string | null;
|
|
53
|
+
externalCreatedAt: string | null;
|
|
54
|
+
externalUpdatedAt: string | null;
|
|
55
|
+
metadata: Record<string, unknown> | null;
|
|
56
|
+
seenAt: string | null;
|
|
57
|
+
deletedAt: string | null;
|
|
58
|
+
}
|
|
59
|
+
export interface FileListQuery {
|
|
60
|
+
/** Everything at and below this folder path, from the account root. */
|
|
61
|
+
under?: string;
|
|
62
|
+
/** Paths holding this text, in any case and accent. */
|
|
63
|
+
pathContains?: string;
|
|
64
|
+
kind?: StoredFile["kind"];
|
|
65
|
+
includeDeleted?: boolean;
|
|
66
|
+
limit?: number;
|
|
67
|
+
offset?: number;
|
|
68
|
+
}
|
|
69
|
+
export interface FilesStore {
|
|
70
|
+
/** The files of one account, by path. The account is always named: there is no default place. */
|
|
71
|
+
list(accountId: string, query?: FileListQuery): Promise<{
|
|
72
|
+
items: StoredFile[];
|
|
73
|
+
hasMore: boolean;
|
|
74
|
+
}>;
|
|
75
|
+
byPath(accountId: string, path: string): Promise<StoredFile | undefined>;
|
|
76
|
+
/** Every place a document is kept, the one it shows first: the owner's pin, else the most recently changed. */
|
|
77
|
+
ofDocument(reference: string): Promise<StoredFile[]>;
|
|
78
|
+
/** The owner's choice of the file a document shows (`document:<id>`, a file id of that document). */
|
|
79
|
+
pin(document: string, fileId: string): Promise<void>;
|
|
80
|
+
/** The document shows its most recently changed file again. */
|
|
81
|
+
unpin(document: string): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
export declare const fileOf: (row: Row) => StoredFile;
|
|
84
|
+
/**
|
|
85
|
+
* The file a document shows (requirements 3.3): live ones first, then the owner's pin, then the most recently changed.
|
|
86
|
+
* Read with the document's row joined as `d`.
|
|
87
|
+
*/
|
|
88
|
+
export declare const PREFERRED_FILE = "f.deleted_at IS NOT NULL, f.id IS NOT d.pinned_file_id, coalesce(f.external_updated_at, f.updated_at) DESC, f.id DESC";
|
|
89
|
+
export declare const filesStoreOver: ({ database, now }: {
|
|
90
|
+
database: CacheDatabase;
|
|
91
|
+
now: () => number;
|
|
92
|
+
}) => FilesStore;
|
|
93
|
+
export {};
|