@wirecat/cli-core 0.489.0 → 0.490.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 +16 -0
- package/dist/store/sqlite/identities.d.ts +1 -1
- package/dist/store/sqlite/identities.js +4 -1
- package/dist/store/sqlite/knowledge.js +4 -2
- package/dist/store/sqlite/manifest.js +1 -0
- package/dist/store/sqlite/migrations.generated.js +10 -0
- package/dist/store/sqlite/note-index.d.ts +16 -1
- package/dist/store/sqlite/note-index.js +68 -3
- package/dist/store/sqlite/notes.js +7 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.490.0 — 31.10.2026
|
|
11
|
+
|
|
12
|
+
### Changed — may break callers
|
|
13
|
+
|
|
14
|
+
- **Notes, documents, knowledge annotations and contacts `--notes` are found by any part of a word, in any letter
|
|
15
|
+
case (store version 40).** `notes` with a search, `knowledge.annotations` with a search and `contacts` with
|
|
16
|
+
`notes` used `instr(lower(...))`, which folds only Latin letters and reads every row: a mixed-case Cyrillic word
|
|
17
|
+
was missed, and a search took 1.5–4 s on a store five times today's size. They now match `note_trigrams` and
|
|
18
|
+
`document_trigrams`, trigram indexes over the normalized title and text (`ё` as `е`, accents dropped) that the
|
|
19
|
+
notes drain keeps, and answer in milliseconds. A query of one or two characters is refused with "search needs at
|
|
20
|
+
least three characters", as message search is; an empty one still filters nothing. The title is searched too,
|
|
21
|
+
not only the text. The notes and documents stored before version 40 get their trigrams in batches from the
|
|
22
|
+
notes drain (`search notes`, `store migrate`); until then they are matched as before.
|
|
23
|
+
|
|
8
24
|
## 0.489.0 — 31.10.2026
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -28,7 +28,7 @@ export declare const people: ({ orm, database }: StoreContext, provider: Provide
|
|
|
28
28
|
export interface ContactFilter {
|
|
29
29
|
/** Three letters or more of the name, the @username or the local alias. */
|
|
30
30
|
query?: string;
|
|
31
|
-
/**
|
|
31
|
+
/** Three characters or more of one of the owner's private notes about them, anywhere, in any letter case. */
|
|
32
32
|
notes?: string;
|
|
33
33
|
}
|
|
34
34
|
export declare const contacts: ({ orm }: StoreContext, accountKey: number, key: AccountKey, { order, limit, offset, ...filter }: {
|
|
@@ -2,7 +2,9 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { fold } from "../normalize.js";
|
|
3
3
|
import { and, asc, desc, eq, inArray, isNotNull, isNull, ne, sql } from "./drizzle/core.js";
|
|
4
4
|
import { folded } from "./folded.js";
|
|
5
|
+
import { holdingText } from "./note-index.js";
|
|
5
6
|
import { resolvePersonLinks } from "./person-resolution.js";
|
|
7
|
+
import { bound } from "./query-filter.js";
|
|
6
8
|
import { accountIdentities, accounts, aliases, chatMembers, chats, identities, identityLinkEvents, identityLinks, identityRevisions, messages, persons, } from "./schema.js";
|
|
7
9
|
import { toIso } from "./values.js";
|
|
8
10
|
const flag = (value) => (value === undefined || value === null ? null : Number(value));
|
|
@@ -209,13 +211,14 @@ const contactsWhere = (accountKey, key, { query, notes }) => {
|
|
|
209
211
|
if (query !== undefined && query.trim().length < 3) {
|
|
210
212
|
throw new CliError("validation_error", `a contact search takes at least 3 characters, got "${query}"`);
|
|
211
213
|
}
|
|
214
|
+
const noted = notes === undefined ? undefined : holdingText("note", "n.id", notes);
|
|
212
215
|
return and(eq(accountIdentities.accountId, accountKey), ne(identities.externalId, key.account), sql `EXISTS (SELECT 1 FROM ${chatMembers} JOIN ${chats} ON ${chats.id} = ${chatMembers.chatId}
|
|
213
216
|
WHERE ${chatMembers.identityId} = ${identities.id} AND ${chats.accountId} = ${accountIdentities.accountId}
|
|
214
217
|
AND ${chats.kind} = 'dialog')`, query === undefined
|
|
215
218
|
? undefined
|
|
216
219
|
: sql `(${identities.id} IN (SELECT rowid FROM identities_fts WHERE identities_fts MATCH ${`"${query.trim().replaceAll('"', '""')}"`}) OR EXISTS (SELECT 1 FROM aliases ca WHERE ca.aliasable_type='identity' AND ca.display=1 AND ca.account_id=${accountKey} AND ca.aliasable_id=${identities.id} AND instr(ca.name_folded, ${fold(query.trim())}) > 0))`, notes === undefined
|
|
217
220
|
? undefined
|
|
218
|
-
: sql `EXISTS (SELECT 1 FROM notes n WHERE n.notable_type='identity' AND n.notable_id=${identities.id} AND n.deleted_at IS NULL AND
|
|
221
|
+
: sql `EXISTS (SELECT 1 FROM notes n WHERE n.notable_type='identity' AND n.notable_id=${identities.id} AND n.deleted_at IS NULL${noted ? sql ` AND ${bound(noted)}` : sql ``})`);
|
|
219
222
|
};
|
|
220
223
|
export const contacts = ({ orm }, accountKey, key, { order, limit, offset = 0, ...filter }) => {
|
|
221
224
|
const byName = [
|
|
@@ -10,6 +10,7 @@ import { ulid } from "../ulid.js";
|
|
|
10
10
|
import { ownerPerson } from "./actors.js";
|
|
11
11
|
import { linkKind, RELATION_KINDS } from "./link-kinds.js";
|
|
12
12
|
import { meetingProjectsStoreOver } from "./meeting-projects.js";
|
|
13
|
+
import { holdingText } from "./note-index.js";
|
|
13
14
|
import { insertLink, linkOf } from "./notes.js";
|
|
14
15
|
import { queryFilter } from "./query-filter.js";
|
|
15
16
|
import { addTags as addThingTags, canonicalTagNamed, createTag, labelsOf, liveTagging, removeTags as removeThingTags, setMainTopic, } from "./tags.js";
|
|
@@ -314,10 +315,11 @@ export const knowledgeStoreOver = (context) => {
|
|
|
314
315
|
const thing = options.target ? resolve(key, options.target).thing : undefined;
|
|
315
316
|
if (options.target && !thing)
|
|
316
317
|
return { items: [], hasMore: false };
|
|
318
|
+
const text = options.search === undefined ? undefined : holdingText("note", "id", options.search);
|
|
317
319
|
const rows = database
|
|
318
320
|
.prepare("SELECT * FROM notes WHERE deleted_at IS NULL AND (? IS NULL OR (notable_type = ? AND notable_id = ?)) " +
|
|
319
|
-
|
|
320
|
-
.all(thing?.type ?? null, thing?.type ?? null, thing?.id ?? null,
|
|
321
|
+
`${text ? `AND ${text.sql} ` : ""}ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?`)
|
|
322
|
+
.all(thing?.type ?? null, thing?.type ?? null, thing?.id ?? null, ...(text?.params ?? []), limit + 1, offset);
|
|
321
323
|
const items = rows.slice(0, limit).flatMap((row) => annotationOf(row) ?? []);
|
|
322
324
|
return { items, hasMore: rows.length > limit };
|
|
323
325
|
},
|
|
@@ -53,6 +53,7 @@ export const MANIFEST = [
|
|
|
53
53
|
{ name: "20261029224106_version-38-folded-names", version: 38, minCompatible: 34 },
|
|
54
54
|
{ name: "20261029224107_version-38-folded-triggers", version: 38, minCompatible: 34 },
|
|
55
55
|
{ name: "20261030114008_version-39-email-locations", version: 39, minCompatible: 34 },
|
|
56
|
+
{ name: "20261030225520_version-40-note-trigrams", version: 40, minCompatible: 34 },
|
|
56
57
|
];
|
|
57
58
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
58
59
|
const migrations = [];
|
|
@@ -837,5 +837,15 @@ export const GENERATED = [
|
|
|
837
837
|
"CREATE TABLE `email_locations` (\n\t`mailbox_id` integer NOT NULL,\n\t`uidvalidity` text NOT NULL,\n\t`uid` integer NOT NULL,\n\t`email_id` integer NOT NULL,\n\tCONSTRAINT `email_locations_pk` PRIMARY KEY(`mailbox_id`, `uidvalidity`, `uid`),\n\tCONSTRAINT `fk_email_locations_mailbox_id_mailboxes_id_fk` FOREIGN KEY (`mailbox_id`) REFERENCES `mailboxes`(`id`),\n\tCONSTRAINT `fk_email_locations_email_id_emails_id_fk` FOREIGN KEY (`email_id`) REFERENCES `emails`(`id`)\n);",
|
|
838
838
|
"CREATE INDEX `email_locations_by_email_id` ON `email_locations` (`email_id`);"
|
|
839
839
|
]
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
"name": "20261030225520_version-40-note-trigrams",
|
|
843
|
+
"statements": [
|
|
844
|
+
"CREATE VIRTUAL TABLE note_trigrams USING fts5(text, content = '', contentless_delete = 1, tokenize = 'trigram');",
|
|
845
|
+
"CREATE VIRTUAL TABLE document_trigrams USING fts5(text, content = '', contentless_delete = 1, tokenize = 'trigram');",
|
|
846
|
+
"CREATE TRIGGER note_trigrams_bd BEFORE DELETE ON notes BEGIN\n DELETE FROM note_trigrams WHERE rowid = old.id;\nEND;",
|
|
847
|
+
"CREATE TRIGGER document_trigrams_bd BEFORE DELETE ON documents BEGIN\n DELETE FROM document_trigrams WHERE rowid = old.id;\nEND;",
|
|
848
|
+
"-- The trigrams hold JS-normalized text, which SQL cannot produce: fillTrigrams fills the rows stored before this\n-- version, up to `watermark`, and a search reads the rest of them by their text meanwhile.\nINSERT INTO search_index_state (name, watermark, filled_through, terms_through, normalizer_version)\n SELECT 'note_trigrams', coalesce(max(id), 0), 0, 0, 1 FROM notes\n UNION ALL SELECT 'document_trigrams', coalesce(max(id), 0), 0, 0, 1 FROM documents;"
|
|
849
|
+
]
|
|
840
850
|
}
|
|
841
851
|
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Stemmer, type Stemmers } from "../../search/stem.js";
|
|
2
2
|
import type { CacheDatabase } from "../driver.js";
|
|
3
|
+
import type { Where } from "./query-filter.js";
|
|
3
4
|
export interface NoteIndexState {
|
|
4
5
|
/** Rows written since they were indexed. */
|
|
5
6
|
pending: number;
|
|
@@ -102,6 +103,20 @@ export declare const CORPORA: {
|
|
|
102
103
|
};
|
|
103
104
|
/** What a row is indexed as: its title, then its text. Chunk offsets point into this. */
|
|
104
105
|
export declare const noteIndexText: (title: string | null, text: string) => string;
|
|
106
|
+
/**
|
|
107
|
+
* `column` (a note's or document's id) is a row whose title or text holds `search` anywhere, in any letter case;
|
|
108
|
+
* no condition for an empty `search`. A row the trigrams do not hold yet — waiting in its queue, or stored before
|
|
109
|
+
* version 40 and not yet reached by `fillTrigrams` — is matched by its text as SQLite's `lower()` reads it.
|
|
110
|
+
*/
|
|
111
|
+
export declare const holdingText: (type: "note" | "document", column: string, search: string) => Where | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Writes the trigrams of the notes and documents stored before version 40, in short write transactions, up to the
|
|
114
|
+
* highest id the migration found; later rows get theirs from the drain. Stopping between batches loses nothing.
|
|
115
|
+
*/
|
|
116
|
+
export declare const fillTrigrams: (database: CacheDatabase, { batch, until }?: {
|
|
117
|
+
batch?: number;
|
|
118
|
+
until?: () => boolean;
|
|
119
|
+
}) => number;
|
|
105
120
|
export declare const corpusIndexState: (database: CacheDatabase, corpus: Corpus) => NoteIndexState;
|
|
106
121
|
/** Documents and notes together: what a notes search reads. */
|
|
107
122
|
export declare const noteIndexState: (database: CacheDatabase) => NoteIndexState;
|
|
@@ -113,7 +128,7 @@ export declare const drainCorpus: (database: CacheDatabase, corpus: Corpus, stem
|
|
|
113
128
|
batch?: number;
|
|
114
129
|
until?: () => boolean;
|
|
115
130
|
}) => number;
|
|
116
|
-
/** Documents, then notes: what a notes search reads. */
|
|
131
|
+
/** Documents, then notes: what a notes search reads; then the trigrams still to fill. */
|
|
117
132
|
export declare const drainNoteIndex: (database: CacheDatabase, stemmerFor: (stemmers: Stemmers) => Stemmer, options?: {
|
|
118
133
|
batch?: number;
|
|
119
134
|
until?: () => boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
1
2
|
import { CHUNK_CHARS, chunkHash, splitSections, splitText } from "../../conversations/chunks.js";
|
|
2
3
|
import { analyzerIdentity, DEFAULT_STEMMERS } from "../../search/stem.js";
|
|
3
4
|
import { taskStemsOf, taskWordsOf } from "../../search/task-similarity.js";
|
|
@@ -103,6 +104,62 @@ export const CORPORA = {
|
|
|
103
104
|
/** What a row is indexed as: its title, then its text. Chunk offsets point into this. */
|
|
104
105
|
export const noteIndexText = (title, text) => (title ? `${title}\n\n${text}` : text);
|
|
105
106
|
const queuedAs = (corpus) => corpus.queued ?? corpus.type;
|
|
107
|
+
const hasTrigrams = (corpus) => corpus.type === "note" || corpus.type === "document";
|
|
108
|
+
/**
|
|
109
|
+
* `column` (a note's or document's id) is a row whose title or text holds `search` anywhere, in any letter case;
|
|
110
|
+
* no condition for an empty `search`. A row the trigrams do not hold yet — waiting in its queue, or stored before
|
|
111
|
+
* version 40 and not yet reached by `fillTrigrams` — is matched by its text as SQLite's `lower()` reads it.
|
|
112
|
+
*/
|
|
113
|
+
export const holdingText = (type, column, search) => {
|
|
114
|
+
const text = normalize(search);
|
|
115
|
+
if (text === "")
|
|
116
|
+
return undefined;
|
|
117
|
+
// A trigram index answers a shorter query with nothing, silently.
|
|
118
|
+
if ([...text].length < 3)
|
|
119
|
+
throw new CliError("validation_error", "search needs at least three characters");
|
|
120
|
+
const queued = `SELECT id FROM ${type}_index_pending WHERE indexable_type = '${type}'`;
|
|
121
|
+
const state = (field) => `(SELECT ${field} FROM search_index_state WHERE name = '${type}_trigrams')`;
|
|
122
|
+
return {
|
|
123
|
+
sql: `${column} IN (SELECT rowid FROM ${type}_trigrams WHERE ${type}_trigrams MATCH ? AND rowid NOT IN (${queued}) ` +
|
|
124
|
+
`UNION ALL SELECT r.id FROM ${type}s r WHERE (r.id IN (${queued}) OR (r.id > ${state("filled_through")} ` +
|
|
125
|
+
`AND r.id <= ${state("watermark")})) AND instr(lower(coalesce(r.body, '')), lower(?)) > 0)`,
|
|
126
|
+
params: [`"${text.replaceAll('"', '""')}"`, search],
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Writes the trigrams of the notes and documents stored before version 40, in short write transactions, up to the
|
|
131
|
+
* highest id the migration found; later rows get theirs from the drain. Stopping between batches loses nothing.
|
|
132
|
+
*/
|
|
133
|
+
export const fillTrigrams = (database, { batch = 200, until = () => false } = {}) => {
|
|
134
|
+
let filled = 0;
|
|
135
|
+
for (const type of ["document", "note"]) {
|
|
136
|
+
const name = `${type}_trigrams`;
|
|
137
|
+
const state = database.prepare("SELECT watermark, filled_through FROM search_index_state WHERE name = ?");
|
|
138
|
+
const rows = database.prepare(`SELECT id, title, coalesce(body, '') AS body FROM ${type}s WHERE id > ? AND id <= ? AND deleted_at IS NULL ` +
|
|
139
|
+
"ORDER BY id LIMIT ?");
|
|
140
|
+
const drop = database.prepare(`DELETE FROM ${name} WHERE rowid = ?`);
|
|
141
|
+
const add = database.prepare(`INSERT INTO ${name} (rowid, text) VALUES (?, ?)`);
|
|
142
|
+
const reached = database.prepare("UPDATE search_index_state SET filled_through = ? WHERE name = ?");
|
|
143
|
+
for (;;) {
|
|
144
|
+
const at = state.get(name);
|
|
145
|
+
const mark = Number(at?.watermark ?? 0);
|
|
146
|
+
if (Number(at?.filled_through ?? mark) >= mark || until())
|
|
147
|
+
break;
|
|
148
|
+
filled += write(database, () => {
|
|
149
|
+
const page = rows.all(Number(at?.filled_through), mark, batch);
|
|
150
|
+
for (const row of page) {
|
|
151
|
+
const text = normalize(noteIndexText(row.title == null ? null : String(row.title), String(row.body)));
|
|
152
|
+
drop.run(Number(row.id));
|
|
153
|
+
if (text !== "")
|
|
154
|
+
add.run(Number(row.id), text);
|
|
155
|
+
}
|
|
156
|
+
reached.run(page.length < batch ? mark : Number(page.at(-1)?.id), name);
|
|
157
|
+
return page.length;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return filled;
|
|
162
|
+
};
|
|
106
163
|
export const corpusIndexState = (database, corpus) => {
|
|
107
164
|
const row = indexRow(database, corpus.index);
|
|
108
165
|
const pending = Number(database
|
|
@@ -139,9 +196,13 @@ const writer = (database, corpus, stemmer) => {
|
|
|
139
196
|
const statements = {
|
|
140
197
|
dropWords: database.prepare(`DELETE FROM ${corpus.type}_words WHERE rowid = ?`),
|
|
141
198
|
dropStems: database.prepare(`DELETE FROM ${corpus.type}_stems WHERE rowid = ?`),
|
|
199
|
+
dropTrigrams: hasTrigrams(corpus) ? database.prepare(`DELETE FROM ${corpus.type}_trigrams WHERE rowid = ?`) : null,
|
|
142
200
|
dropChunks: database.prepare("DELETE FROM chunks WHERE chunkable_type = ? AND chunkable_id = ?"),
|
|
143
201
|
words: database.prepare(`INSERT INTO ${corpus.type}_words (rowid, normalized_text, scope) VALUES (?, ?, ?)`),
|
|
144
202
|
stems: database.prepare(`INSERT INTO ${corpus.type}_stems (rowid, stems, scope) VALUES (?, ?, ?)`),
|
|
203
|
+
trigrams: hasTrigrams(corpus)
|
|
204
|
+
? database.prepare(`INSERT INTO ${corpus.type}_trigrams (rowid, text) VALUES (?, ?)`)
|
|
205
|
+
: null,
|
|
145
206
|
chunk: database.prepare("INSERT INTO chunks (chunkable_type, chunkable_id, position, start_offset, end_offset, content_hash, scope, account_id, created_at, updated_at) " +
|
|
146
207
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
147
208
|
dequeue: database.prepare(`DELETE FROM ${corpus.type}_index_pending WHERE indexable_type = ? AND id = ?`),
|
|
@@ -151,14 +212,17 @@ const writer = (database, corpus, stemmer) => {
|
|
|
151
212
|
const before = oldHashes.all(corpus.type, id).map((hash) => String(hash.hash));
|
|
152
213
|
statements.dropWords.run(id);
|
|
153
214
|
statements.dropStems.run(id);
|
|
215
|
+
statements.dropTrigrams?.run(id);
|
|
154
216
|
statements.dropChunks.run(corpus.type, id);
|
|
155
217
|
const live = row !== undefined && Number(row.live) === 1;
|
|
156
218
|
const text = live ? noteIndexText(row.title == null ? null : String(row.title), String(row.body)) : "";
|
|
157
219
|
const scope = live ? String(row.scope ?? "personal") : "";
|
|
158
220
|
const terms = corpus.terms && text ? corpus.terms(text, stemmer) : undefined;
|
|
159
221
|
const words = terms ? terms.words : normalize(text);
|
|
160
|
-
if (words !== "")
|
|
222
|
+
if (words !== "") {
|
|
161
223
|
statements.words.run(id, words, scope);
|
|
224
|
+
statements.trigrams?.run(id, words);
|
|
225
|
+
}
|
|
162
226
|
const stems = terms ? terms.stems : text ? stemmer.indexText(text) : "";
|
|
163
227
|
if (stems !== "")
|
|
164
228
|
statements.stems.run(id, stems, scope);
|
|
@@ -228,9 +292,10 @@ export const drainCorpus = (database, corpus, stemmerFor, { batch = 200, until =
|
|
|
228
292
|
}
|
|
229
293
|
return done;
|
|
230
294
|
};
|
|
231
|
-
/** Documents, then notes: what a notes search reads. */
|
|
295
|
+
/** Documents, then notes: what a notes search reads; then the trigrams still to fill. */
|
|
232
296
|
export const drainNoteIndex = (database, stemmerFor, options = {}) => drainCorpus(database, CORPORA.document, stemmerFor, options) +
|
|
233
|
-
drainCorpus(database, CORPORA.note, stemmerFor, options)
|
|
297
|
+
drainCorpus(database, CORPORA.note, stemmerFor, options) +
|
|
298
|
+
fillTrigrams(database, options);
|
|
234
299
|
/** Queues every document, note and memory again — for `store rebuild`. */
|
|
235
300
|
export const resetNoteIndex = (database) => {
|
|
236
301
|
write(database, () => {
|
|
@@ -12,6 +12,7 @@ import { PREFERRED_FILE, syncFile } from "./files.js";
|
|
|
12
12
|
import { importedDocumentWhere } from "./imported-documents.js";
|
|
13
13
|
import { LINK_KINDS, LINK_ORIGINS, linkKind } from "./link-kinds.js";
|
|
14
14
|
import { reviewLink } from "./link-reviews.js";
|
|
15
|
+
import { holdingText } from "./note-index.js";
|
|
15
16
|
import { addTags, ensureTag, labelsOf } from "./tags.js";
|
|
16
17
|
import { canonicalThingReference, referenceOfThing, storedThing, thingOf } from "./things.js";
|
|
17
18
|
import { write } from "./transaction.js";
|
|
@@ -525,7 +526,8 @@ export const notesStoreOver = (context) => {
|
|
|
525
526
|
if (options.about !== undefined && !about)
|
|
526
527
|
return { items: [], hasMore: false };
|
|
527
528
|
const folder = options.folderId === undefined ? null : Number(folderRow(options.folderId).id);
|
|
528
|
-
const
|
|
529
|
+
const documentText = options.search === undefined ? undefined : holdingText("document", "d.id", options.search);
|
|
530
|
+
const noteText = options.search === undefined ? undefined : holdingText("note", "n.id", options.search);
|
|
529
531
|
const aboutType = about?.type ?? null;
|
|
530
532
|
const aboutId = about?.id ?? null;
|
|
531
533
|
const files = options.source === "internal"
|
|
@@ -541,16 +543,16 @@ export const notesStoreOver = (context) => {
|
|
|
541
543
|
"WHERE f.deleted_at IS NULL AND d.deleted_at IS NULL AND f.account_id = ? AND ? IS NOT NULL ") +
|
|
542
544
|
"AND coalesce(json_extract(d.metadata, '$.catalog.active'), 1)=1 " +
|
|
543
545
|
`AND (? IS NULL OR EXISTS (SELECT 1 FROM links l WHERE l.from_type = 'document' AND l.from_id = d.id AND l.kind = ${linkKind("about")} AND l.to_type = ? AND l.to_id = ?)) ` +
|
|
544
|
-
|
|
545
|
-
.all(folder, folder, aboutType, aboutType, aboutId,
|
|
546
|
+
`${documentText ? `AND ${documentText.sql} ` : ""}ORDER BY d.created_at DESC, d.id DESC LIMIT ?`)
|
|
547
|
+
.all(folder, folder, aboutType, aboutType, aboutId, ...(documentText?.params ?? []), limit + offset + 1)
|
|
546
548
|
.map(documentOf);
|
|
547
549
|
const written = options.source === "file" || folder !== null
|
|
548
550
|
? []
|
|
549
551
|
: database
|
|
550
552
|
.prepare("SELECT n.* FROM notes n WHERE n.deleted_at IS NULL " +
|
|
551
553
|
`AND (? IS NULL OR (n.notable_type = ? AND n.notable_id = ?) OR EXISTS (SELECT 1 FROM links l WHERE l.from_type = 'note' AND l.from_id = n.id AND l.kind = ${linkKind("about")} AND l.to_type = ? AND l.to_id = ?)) ` +
|
|
552
|
-
|
|
553
|
-
.all(aboutType, aboutType, aboutId, aboutType, aboutId,
|
|
554
|
+
`${noteText ? `AND ${noteText.sql} ` : ""}ORDER BY n.created_at DESC, n.id DESC LIMIT ?`)
|
|
555
|
+
.all(aboutType, aboutType, aboutId, aboutType, aboutId, ...(noteText?.params ?? []), limit + offset + 1)
|
|
554
556
|
.map(noteOf);
|
|
555
557
|
const all = [...files, ...written].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.ref.localeCompare(a.ref));
|
|
556
558
|
return { items: all.slice(offset, offset + limit), hasMore: all.length > offset + limit };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.490.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",
|