@mastra/mongodb 1.25.0 → 1.26.0-alpha.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/dist/_types/@internal_core/dist/knowledge-compat/index.d.ts +338 -0
- package/dist/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/index.cjs +1837 -728
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1838 -729
- package/dist/index.js.map +1 -1
- package/dist/storage/domains/knowledge/index.d.ts +40 -77
- package/dist/storage/domains/knowledge/index.d.ts.map +1 -1
- package/dist/storage/domains/knowledge/pages.d.ts +172 -0
- package/dist/storage/domains/knowledge/pages.d.ts.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { v4 } from "@lukeed/uuid";
|
|
2
2
|
import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
|
|
3
|
-
import { AgentsStorage, BackgroundTasksStorage, BlobStore, DatasetsStorage, ExperimentsStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MastraCompositeStore, MemoryStorage, NotificationsStorage, ObservabilityStorage, PromptBlocksStorage, RunFenceConflictError, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS,
|
|
3
|
+
import { AgentsStorage, BackgroundTasksStorage, BlobStore, DatasetsStorage, ExperimentsStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MastraCompositeStore, MemoryStorage, NotificationsStorage, ObservabilityStorage, PromptBlocksStorage, RunFenceConflictError, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, hasErrorCode, isRunFenceConflictError, listTracesArgsSchema, normalizePerPage, normalizeScheduleTarget, parseDuration, resolveListOrderBy, resolveRunFence, safelyParseJSON, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
|
|
4
4
|
import { MastraVector, validateUpsertInput, validateVectorValues } from "@mastra/core/vector";
|
|
5
5
|
import { MongoBulkWriteError, MongoClient, ObjectId } from "mongodb";
|
|
6
6
|
import { BaseFilterTranslator } from "@mastra/core/vector/filter";
|
|
7
|
+
import { coreFeatures } from "@mastra/core/features";
|
|
7
8
|
import { createHash } from "crypto";
|
|
8
9
|
import { MessageList } from "@mastra/core/agent";
|
|
9
10
|
import { saveScorePayloadSchema } from "@mastra/core/evals";
|
|
10
11
|
import { skillSnapshotFieldValuesEqual } from "@mastra/core/storage/domains/skills";
|
|
11
12
|
// package.json
|
|
12
|
-
var version = "1.
|
|
13
|
+
var version = "1.26.0-alpha.0";
|
|
13
14
|
// src/vector/filter.ts
|
|
14
15
|
/**
|
|
15
16
|
* Translator for MongoDB filter queries.
|
|
@@ -4768,845 +4769,1953 @@ var MongoDBExperimentsStorage = class MongoDBExperimentsStorage extends Experime
|
|
|
4768
4769
|
} catch {}
|
|
4769
4770
|
}
|
|
4770
4771
|
};
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4772
|
+
const KNOWLEDGE_TABLE_NAMES = [
|
|
4773
|
+
"mastra_knowledge_nodes",
|
|
4774
|
+
"mastra_knowledge_records",
|
|
4775
|
+
"mastra_knowledge_mentions",
|
|
4776
|
+
"mastra_knowledge_activity",
|
|
4777
|
+
"mastra_knowledge_semantic_outbox",
|
|
4778
|
+
"mastra_knowledge_node_scopes",
|
|
4779
|
+
"mastra_knowledge_record_scopes",
|
|
4780
|
+
"mastra_knowledge_scope_grants",
|
|
4781
|
+
"mastra_knowledge_access_state",
|
|
4782
|
+
"mastra_knowledge_scope_addresses",
|
|
4783
|
+
"mastra_knowledge_node_addresses",
|
|
4784
|
+
"mastra_knowledge_import_state",
|
|
4785
|
+
"mastra_knowledge_import_runs",
|
|
4786
|
+
"mastra_knowledge_proposals"
|
|
4787
|
+
];
|
|
4788
|
+
/** Knowledge v1 tables with no v2 equivalent; an explicit schema reset drops them. */
|
|
4789
|
+
const RETIRED_KNOWLEDGE_TABLE_NAMES = ["mastra_knowledge_cursors"];
|
|
4790
|
+
const KNOWLEDGE_PAGES_CORE_FEATURE = "knowledge-pages";
|
|
4791
|
+
const KNOWLEDGE_PAGES_TABLE_NAMES = [
|
|
4792
|
+
"mastra_knowledge_pages",
|
|
4793
|
+
"mastra_knowledge_notes",
|
|
4794
|
+
"mastra_knowledge_note_stamps",
|
|
4795
|
+
"mastra_knowledge_mentions",
|
|
4796
|
+
"mastra_knowledge_pins",
|
|
4797
|
+
"mastra_knowledge_activity",
|
|
4798
|
+
"mastra_knowledge_access_state"
|
|
4799
|
+
];
|
|
4800
|
+
/** Every Knowledge table any schema version created. Reset drops these and nothing else. */
|
|
4801
|
+
const KNOWLEDGE_RESETTABLE_TABLE_NAMES = [.../* @__PURE__ */ new Set([
|
|
4802
|
+
...KNOWLEDGE_PAGES_TABLE_NAMES,
|
|
4803
|
+
...KNOWLEDGE_TABLE_NAMES,
|
|
4804
|
+
...RETIRED_KNOWLEDGE_TABLE_NAMES
|
|
4805
|
+
])];
|
|
4806
|
+
/** Schema-1/2 tables whose presence means the store needs a reset before schema 4 can run. */
|
|
4807
|
+
const LEGACY_KNOWLEDGE_TABLE_NAMES = new Set([...KNOWLEDGE_TABLE_NAMES, ...RETIRED_KNOWLEDGE_TABLE_NAMES].filter((name) => !KNOWLEDGE_PAGES_TABLE_NAMES.includes(name)));
|
|
4808
|
+
/**
|
|
4809
|
+
* Classifies what an adapter found before creating schema-4 tables. `tableNames` is every
|
|
4810
|
+
* `mastra_knowledge_*` table present; `schemaVersion` is the state row's version, if any.
|
|
4811
|
+
*/
|
|
4812
|
+
function inspectKnowledgePagesSchema(snapshot) {
|
|
4813
|
+
const legacy = snapshot.tableNames.filter((name) => LEGACY_KNOWLEDGE_TABLE_NAMES.has(name));
|
|
4814
|
+
if (legacy.length > 0) return {
|
|
4815
|
+
status: "incompatible-reset-required",
|
|
4816
|
+
schemaVersion: snapshot.schemaVersion ?? null,
|
|
4817
|
+
reason: `found Knowledge tables from an earlier schema (${legacy.join(", ")})`
|
|
4818
|
+
};
|
|
4819
|
+
if (snapshot.schemaVersion === 4) return {
|
|
4820
|
+
status: "compatible",
|
|
4821
|
+
schemaVersion: 4
|
|
4822
|
+
};
|
|
4823
|
+
if (snapshot.schemaVersion === void 0) {
|
|
4824
|
+
const present = snapshot.tableNames.filter((name) => KNOWLEDGE_PAGES_TABLE_NAMES.includes(name));
|
|
4825
|
+
if (present.length === 0) return {
|
|
4826
|
+
status: "uninitialized",
|
|
4827
|
+
schemaVersion: null
|
|
4828
|
+
};
|
|
4829
|
+
return {
|
|
4830
|
+
status: "incompatible-reset-required",
|
|
4831
|
+
schemaVersion: null,
|
|
4832
|
+
reason: `found Knowledge tables (${present.join(", ")}) without a schema version`
|
|
4833
|
+
};
|
|
4834
|
+
}
|
|
4835
|
+
return {
|
|
4836
|
+
status: "incompatible-reset-required",
|
|
4837
|
+
schemaVersion: snapshot.schemaVersion,
|
|
4838
|
+
reason: `Knowledge schema version ${snapshot.schemaVersion} is not 4`
|
|
4839
|
+
};
|
|
4840
|
+
}
|
|
4841
|
+
/** Schema-2 table names schema 4 reuses with a different shape. */
|
|
4842
|
+
const REUSED_KNOWLEDGE_TABLE_NAMES = KNOWLEDGE_TABLE_NAMES.filter((name) => KNOWLEDGE_PAGES_TABLE_NAMES.includes(name));
|
|
4843
|
+
/**
|
|
4844
|
+
* The old Knowledge tables an adapter drops on init, rows included: Knowledge before schema 4 was an
|
|
4845
|
+
* experimental store with no migration. Every schema-1/2-only table present is dropped. The table
|
|
4846
|
+
* names schema 4 reuses (mentions, activity, the state table) are dropped too, but only when an old
|
|
4847
|
+
* table is present and no schema-4 state row exists, so they are old-shaped. Schema-4-only tables
|
|
4848
|
+
* are never listed. On a schema-4 store this lists only tables an older process recreated during a
|
|
4849
|
+
* rolling deploy. Drop order is safe for adapters that enforce references.
|
|
4850
|
+
*/
|
|
4851
|
+
function legacyKnowledgeTablesToDrop(snapshot) {
|
|
4852
|
+
const present = new Set(snapshot.tableNames);
|
|
4853
|
+
const legacy = [...LEGACY_KNOWLEDGE_TABLE_NAMES].filter((name) => present.has(name));
|
|
4854
|
+
if (legacy.length === 0) return [];
|
|
4855
|
+
const reused = snapshot.schemaVersion === 4 ? [] : REUSED_KNOWLEDGE_TABLE_NAMES.filter((name) => present.has(name));
|
|
4856
|
+
const drop = /* @__PURE__ */ new Set([...legacy, ...reused]);
|
|
4857
|
+
return [...RETIRED_KNOWLEDGE_TABLE_NAMES, ...[...KNOWLEDGE_TABLE_NAMES].reverse()].filter((name) => drop.has(name));
|
|
4858
|
+
}
|
|
4859
|
+
/** Message for the error an adapter throws on `incompatible-reset-required`. */
|
|
4860
|
+
function knowledgePagesResetMessage(reason) {
|
|
4861
|
+
return `Knowledge storage needs a reset before this version can use it: ${reason}. Knowledge data has no migration; run \`await storage.stores?.knowledge?.dangerouslyReset()\` to drop the Knowledge tables (Memory tables are untouched).`;
|
|
4862
|
+
}
|
|
4863
|
+
const KNOWLEDGE_PAGES_CORE_FUNCTIONS = [
|
|
4864
|
+
"assertKnowledgePageDescriptionWithinBound",
|
|
4865
|
+
"assertKnowledgeNoteTextWithinBound",
|
|
4866
|
+
"canonicalKnowledgeName",
|
|
4867
|
+
"knowledgePositionBetween",
|
|
4868
|
+
"KnowledgeMoveTooLargeError",
|
|
4869
|
+
"KnowledgePageDepthError",
|
|
4870
|
+
"KnowledgePageHasChildrenError",
|
|
4871
|
+
"KnowledgePageKeyConflictError"
|
|
4872
|
+
];
|
|
4873
|
+
const KNOWLEDGE_PAGES_CORE_NUMBERS = [
|
|
4874
|
+
"MAX_KNOWLEDGE_MOVE_PAGES",
|
|
4875
|
+
"MAX_KNOWLEDGE_PAGE_DEPTH",
|
|
4876
|
+
"MAX_KNOWLEDGE_PAGE_LIST_LIMIT"
|
|
4877
|
+
];
|
|
4878
|
+
function resolveKnowledgePagesCore(storageModule) {
|
|
4879
|
+
const module = typeof storageModule === "object" && storageModule !== null ? storageModule : {};
|
|
4880
|
+
const missing = [...KNOWLEDGE_PAGES_CORE_FUNCTIONS.filter((name) => typeof module[name] !== "function"), ...KNOWLEDGE_PAGES_CORE_NUMBERS.filter((name) => typeof module[name] !== "number")];
|
|
4881
|
+
if (missing.length > 0) throw new Error(`@mastra/core advertises Knowledge pages without the required storage API (missing ${missing.join(", ")})`);
|
|
4882
|
+
return module;
|
|
4883
|
+
}
|
|
4884
|
+
/** Loads core's page-store runtime once, after checking core advertises `knowledge-pages`. */
|
|
4885
|
+
function createKnowledgePagesCoreLoader(features, loadStorageModule) {
|
|
4886
|
+
let core;
|
|
4887
|
+
return async () => {
|
|
4888
|
+
if (!features.has("knowledge-pages")) throw new Error(`Knowledge requires a @mastra/core release with the "${KNOWLEDGE_PAGES_CORE_FEATURE}" feature. Upgrade @mastra/core to use Knowledge; other storage domains keep working on this version.`);
|
|
4889
|
+
core ??= loadStorageModule().then(resolveKnowledgePagesCore).catch((error) => {
|
|
4890
|
+
core = void 0;
|
|
4891
|
+
throw error;
|
|
4778
4892
|
});
|
|
4779
|
-
|
|
4780
|
-
|
|
4893
|
+
return core;
|
|
4894
|
+
};
|
|
4895
|
+
}
|
|
4896
|
+
/**
|
|
4897
|
+
* Keyword-search words: the query split on whitespace, lowercased, deduplicated. Every word must
|
|
4898
|
+
* appear (as a case-insensitive substring) in a hit; an empty list means the search returns nothing.
|
|
4899
|
+
*/
|
|
4900
|
+
function knowledgeSearchWords(query) {
|
|
4901
|
+
return [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
|
|
4902
|
+
}
|
|
4903
|
+
/**
|
|
4904
|
+
* Why `key` can't be a Knowledge page key, or undefined when it can. Keys are one path segment:
|
|
4905
|
+
* non-empty, with no `/` (path separator), `#` (scope/path separator) or `%` (escape character).
|
|
4906
|
+
*/
|
|
4907
|
+
function knowledgePageKeyProblem(key) {
|
|
4908
|
+
if (!key) return "Knowledge page key must not be empty";
|
|
4909
|
+
if (/[/#%]/.test(key)) return `Knowledge page key "${key}" must not contain "/", "#" or "%"`;
|
|
4910
|
+
}
|
|
4911
|
+
// src/storage/domains/knowledge/pages.ts
|
|
4912
|
+
const loadPagesCore = createKnowledgePagesCoreLoader(coreFeatures, () => import("@mastra/core/storage"));
|
|
4913
|
+
const DEFAULT_LIST_LIMIT = 50;
|
|
4914
|
+
const MONGO_DUPLICATE_KEY = 11e3;
|
|
4915
|
+
const MONGO_KNOWLEDGE_PAGES_COLLECTIONS = {
|
|
4916
|
+
pages: "mastra_knowledge_pages",
|
|
4917
|
+
notes: "mastra_knowledge_notes",
|
|
4918
|
+
pins: "mastra_knowledge_pins",
|
|
4919
|
+
activity: "mastra_knowledge_activity",
|
|
4920
|
+
state: "mastra_knowledge_access_state"
|
|
4921
|
+
};
|
|
4922
|
+
/** Thrown when existing Knowledge collections can't be used as schema 4 without a reset. */
|
|
4923
|
+
var MongoKnowledgeResetRequiredError = class extends Error {
|
|
4924
|
+
inspection;
|
|
4925
|
+
constructor(inspection) {
|
|
4926
|
+
super(knowledgePagesResetMessage(inspection.reason));
|
|
4927
|
+
this.name = "KnowledgeSchemaResetRequiredError";
|
|
4928
|
+
this.inspection = inspection;
|
|
4781
4929
|
}
|
|
4782
|
-
|
|
4930
|
+
};
|
|
4931
|
+
function escapeRegex(value) {
|
|
4932
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4783
4933
|
}
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
assertRecordTextWithinBound(text);
|
|
4934
|
+
function isDuplicateKey(error) {
|
|
4935
|
+
return typeof error === "object" && error !== null && error.code === MONGO_DUPLICATE_KEY;
|
|
4936
|
+
}
|
|
4937
|
+
function encodeCursor(parts) {
|
|
4938
|
+
return Buffer.from(JSON.stringify(parts)).toString("base64url");
|
|
4790
4939
|
}
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
const
|
|
4794
|
-
|
|
4795
|
-
function visibleScopeKeys(scope) {
|
|
4796
|
-
const canonical = canonicalizeKnowledgeScope(scope);
|
|
4797
|
-
const subsets = [[]];
|
|
4798
|
-
for (const entry of canonical) subsets.push(...subsets.map((subset) => [...subset, entry]));
|
|
4799
|
-
const keys = /* @__PURE__ */ new Set();
|
|
4800
|
-
for (const subset of subsets.slice(1)) try {
|
|
4801
|
-
keys.add(knowledgeScopeKey(subset));
|
|
4940
|
+
function decodeCursor(cursor) {
|
|
4941
|
+
try {
|
|
4942
|
+
const parts = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
4943
|
+
if (Array.isArray(parts) && parts.length === 2 && parts.every((part) => typeof part === "string")) return parts;
|
|
4802
4944
|
} catch {}
|
|
4803
|
-
|
|
4945
|
+
throw new Error("Invalid Knowledge cursor");
|
|
4946
|
+
}
|
|
4947
|
+
function clampLimit(limit, max) {
|
|
4948
|
+
const value = limit ?? DEFAULT_LIST_LIMIT;
|
|
4949
|
+
if (!Number.isInteger(value) || value < 1) throw new Error("Knowledge list limit must be a positive integer");
|
|
4950
|
+
return Math.min(value, max);
|
|
4804
4951
|
}
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
name: parsed.name,
|
|
4811
|
-
id: { $gt: parsed.id }
|
|
4812
|
-
}]
|
|
4813
|
-
}] };
|
|
4952
|
+
const live = { deletedAt: { $exists: false } };
|
|
4953
|
+
const notScope = { isScope: { $ne: true } };
|
|
4954
|
+
/** Unique-while-live page key per parent; unset on delete so the key can be reused. */
|
|
4955
|
+
function activeKeyOf(parentId, key) {
|
|
4956
|
+
return `${parentId}\u0000${key}`;
|
|
4814
4957
|
}
|
|
4815
|
-
|
|
4958
|
+
/** Pipeline stage recomputing `activeKey` for pages that have a live key after their parent changed. */
|
|
4959
|
+
function recomputeActiveKey(parentId) {
|
|
4960
|
+
return { $set: { activeKey: { $cond: [
|
|
4961
|
+
{ $and: [{ $eq: [{ $type: "$key" }, "string"] }, { $eq: [{ $type: "$deletedAt" }, "missing"] }] },
|
|
4962
|
+
{ $concat: [
|
|
4963
|
+
parentId,
|
|
4964
|
+
"\0",
|
|
4965
|
+
"$key"
|
|
4966
|
+
] },
|
|
4967
|
+
"$$REMOVE"
|
|
4968
|
+
] } } };
|
|
4969
|
+
}
|
|
4970
|
+
function toPage(row) {
|
|
4816
4971
|
return {
|
|
4817
4972
|
id: String(row.id),
|
|
4818
|
-
|
|
4973
|
+
scopeId: String(row.scopeId),
|
|
4974
|
+
parentId: row.parentId == null ? null : String(row.parentId),
|
|
4975
|
+
isScope: row.isScope === true,
|
|
4976
|
+
...row.address == null ? {} : { address: String(row.address) },
|
|
4977
|
+
...row.declaration == null ? {} : { declaration: String(row.declaration) },
|
|
4978
|
+
...row.key == null ? {} : { key: String(row.key) },
|
|
4979
|
+
position: String(row.position),
|
|
4819
4980
|
name: String(row.name),
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
description: row.description == null ? void 0 : String(row.description),
|
|
4823
|
-
scope: cloneScope(row.scope),
|
|
4981
|
+
...row.description == null ? {} : { description: String(row.description) },
|
|
4982
|
+
...row.body == null ? {} : { body: String(row.body) },
|
|
4824
4983
|
version: Number(row.version),
|
|
4825
|
-
mergedInto: row.mergedInto
|
|
4984
|
+
...row.mergedInto == null ? {} : { mergedInto: String(row.mergedInto) },
|
|
4985
|
+
createdBy: row.createdBy,
|
|
4986
|
+
updatedBy: row.updatedBy,
|
|
4826
4987
|
createdAt: new Date(row.createdAt),
|
|
4827
|
-
updatedAt: new Date(row.updatedAt)
|
|
4988
|
+
updatedAt: new Date(row.updatedAt),
|
|
4989
|
+
...row.deletedAt == null ? {} : { deletedAt: new Date(row.deletedAt) },
|
|
4990
|
+
...row.deletedBy == null ? {} : { deletedBy: String(row.deletedBy) }
|
|
4828
4991
|
};
|
|
4829
4992
|
}
|
|
4830
|
-
function
|
|
4993
|
+
function toNote(row) {
|
|
4831
4994
|
return {
|
|
4832
4995
|
id: String(row.id),
|
|
4833
|
-
|
|
4996
|
+
pageId: String(row.pageId),
|
|
4834
4997
|
text: String(row.text),
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4998
|
+
...row.when == null ? {} : { when: new Date(row.when) },
|
|
4999
|
+
stampScopeIds: [...row.stampScopeIds ?? []].sort(),
|
|
5000
|
+
mentions: [...row.mentions ?? []].sort(),
|
|
5001
|
+
provenance: row.provenance,
|
|
5002
|
+
version: Number(row.version),
|
|
5003
|
+
createdAt: new Date(row.createdAt),
|
|
5004
|
+
updatedAt: new Date(row.updatedAt),
|
|
5005
|
+
...row.deletedAt == null ? {} : { deletedAt: new Date(row.deletedAt) },
|
|
5006
|
+
...row.deletedBy == null ? {} : { deletedBy: String(row.deletedBy) }
|
|
4842
5007
|
};
|
|
4843
5008
|
}
|
|
4844
|
-
function
|
|
5009
|
+
function toListedPin(row) {
|
|
5010
|
+
return {
|
|
5011
|
+
...toPin(row),
|
|
5012
|
+
pageName: String(row.pageName)
|
|
5013
|
+
};
|
|
5014
|
+
}
|
|
5015
|
+
function toPin(row) {
|
|
4845
5016
|
return {
|
|
4846
5017
|
id: String(row.id),
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
operation: row.operation,
|
|
4851
|
-
scope: cloneScope(row.scope),
|
|
4852
|
-
status: row.status,
|
|
4853
|
-
attempts: Number(row.attempts),
|
|
4854
|
-
availableAt: new Date(row.availableAt),
|
|
4855
|
-
claimedAt: row.claimedAt ? new Date(row.claimedAt) : void 0,
|
|
4856
|
-
claimedBy: row.claimedBy ?? void 0,
|
|
5018
|
+
pageId: String(row.pageId),
|
|
5019
|
+
audience: [...row.audience ?? []],
|
|
5020
|
+
createdBy: String(row.createdBy),
|
|
4857
5021
|
createdAt: new Date(row.createdAt),
|
|
4858
|
-
|
|
5022
|
+
updatedAt: new Date(row.updatedAt)
|
|
4859
5023
|
};
|
|
4860
5024
|
}
|
|
4861
|
-
const
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
5025
|
+
const noProjection = { projection: { _id: 0 } };
|
|
5026
|
+
/**
|
|
5027
|
+
* MongoDB implementation of the schema-4 {@link KnowledgePageStore}.
|
|
5028
|
+
*
|
|
5029
|
+
* Notes embed their stamps and mentions and carry two denormalized copies of page scope
|
|
5030
|
+
* (`pageScopeId`, `mentionScopeIds`); activity events carry `pageScopeId`. Moves and merges rewrite
|
|
5031
|
+
* those copies in the same transaction as the page rows, so a read is one indexed filter against the
|
|
5032
|
+
* set of readable scope ids resolved from the read scope (never a per-address query).
|
|
5033
|
+
*/
|
|
5034
|
+
var MongoKnowledgePages = class {
|
|
4870
5035
|
#connector;
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
this.#connector =
|
|
5036
|
+
#bounds;
|
|
5037
|
+
constructor(options) {
|
|
5038
|
+
this.#connector = options.connector;
|
|
5039
|
+
this.#bounds = options.bounds;
|
|
5040
|
+
}
|
|
5041
|
+
#c(name) {
|
|
5042
|
+
return this.#connector.getCollection(MONGO_KNOWLEDGE_PAGES_COLLECTIONS[name]);
|
|
5043
|
+
}
|
|
5044
|
+
#tx(operation) {
|
|
5045
|
+
return this.#connector.withTransaction(operation);
|
|
5046
|
+
}
|
|
5047
|
+
async inspectSchema() {
|
|
5048
|
+
return inspectKnowledgePagesSchema(await this.#snapshot());
|
|
5049
|
+
}
|
|
5050
|
+
async #snapshot() {
|
|
5051
|
+
const tableNames = [];
|
|
5052
|
+
for (const name of KNOWLEDGE_RESETTABLE_TABLE_NAMES) if (await this.#connector.collectionExists(name)) tableNames.push(name);
|
|
5053
|
+
let schemaVersion;
|
|
5054
|
+
if (tableNames.includes(MONGO_KNOWLEDGE_PAGES_COLLECTIONS.state)) {
|
|
5055
|
+
const state = await (await this.#c("state")).findOne({ id: "knowledge" });
|
|
5056
|
+
schemaVersion = typeof state?.schemaVersion === "number" ? state.schemaVersion : void 0;
|
|
5057
|
+
}
|
|
5058
|
+
return {
|
|
5059
|
+
tableNames,
|
|
5060
|
+
schemaVersion
|
|
5061
|
+
};
|
|
5062
|
+
}
|
|
5063
|
+
async #drop(name) {
|
|
5064
|
+
const collection = await this.#connector.getCollection(name);
|
|
5065
|
+
try {
|
|
5066
|
+
await collection.drop();
|
|
5067
|
+
} catch (error) {
|
|
5068
|
+
if (error.codeName !== "NamespaceNotFound") throw error;
|
|
5069
|
+
}
|
|
4874
5070
|
}
|
|
4875
5071
|
async init() {
|
|
4876
|
-
const
|
|
4877
|
-
const
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
5072
|
+
const stateName = MONGO_KNOWLEDGE_PAGES_COLLECTIONS.state;
|
|
5073
|
+
for (const name of legacyKnowledgeTablesToDrop(await this.#snapshot())) if (name === stateName) await (await this.#c("state")).deleteMany({ schemaVersion: { $ne: 4 } });
|
|
5074
|
+
else await this.#drop(name);
|
|
5075
|
+
await (await this.#c("state")).updateOne({ id: "knowledge" }, { $setOnInsert: {
|
|
5076
|
+
id: "knowledge",
|
|
5077
|
+
schemaVersion: 4
|
|
5078
|
+
} }, { upsert: true });
|
|
5079
|
+
const inspection = await this.inspectSchema();
|
|
5080
|
+
if (inspection.status === "incompatible-reset-required") throw new MongoKnowledgeResetRequiredError(inspection);
|
|
5081
|
+
const [pages, notes, pins, activity] = await Promise.all([
|
|
5082
|
+
this.#c("pages"),
|
|
5083
|
+
this.#c("notes"),
|
|
5084
|
+
this.#c("pins"),
|
|
5085
|
+
this.#c("activity")
|
|
5086
|
+
]);
|
|
4881
5087
|
await Promise.all([
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
}, { unique: true }),
|
|
4887
|
-
nodes.createIndex({
|
|
4888
|
-
scopeKey: 1,
|
|
4889
|
-
type: 1
|
|
5088
|
+
pages.createIndex({ id: 1 }, { unique: true }),
|
|
5089
|
+
pages.createIndex({ address: 1 }, {
|
|
5090
|
+
unique: true,
|
|
5091
|
+
partialFilterExpression: { isScope: true }
|
|
4890
5092
|
}),
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
5093
|
+
pages.createIndex({ activeKey: 1 }, {
|
|
5094
|
+
unique: true,
|
|
5095
|
+
partialFilterExpression: { activeKey: { $type: "string" } }
|
|
4894
5096
|
}),
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
5097
|
+
pages.createIndex({
|
|
5098
|
+
parentId: 1,
|
|
5099
|
+
position: 1,
|
|
5100
|
+
id: 1
|
|
4898
5101
|
}),
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
5102
|
+
pages.createIndex({
|
|
5103
|
+
scopeId: 1,
|
|
5104
|
+
name: 1,
|
|
5105
|
+
id: 1
|
|
4902
5106
|
}),
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
5107
|
+
pages.createIndex({
|
|
5108
|
+
canonicalName: 1,
|
|
5109
|
+
scopeId: 1
|
|
4906
5110
|
}),
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
}, { unique: true }),
|
|
4912
|
-
mentions.createIndex({
|
|
4913
|
-
recordId: 1,
|
|
4914
|
-
sourceType: 1,
|
|
4915
|
-
sourceId: 1
|
|
5111
|
+
pages.createIndex({
|
|
5112
|
+
isScope: 1,
|
|
5113
|
+
declaration: 1,
|
|
5114
|
+
parentId: 1
|
|
4916
5115
|
}),
|
|
4917
|
-
|
|
5116
|
+
notes.createIndex({ id: 1 }, { unique: true }),
|
|
5117
|
+
notes.createIndex({
|
|
5118
|
+
pageId: 1,
|
|
5119
|
+
createdAt: 1,
|
|
5120
|
+
id: 1
|
|
5121
|
+
}),
|
|
5122
|
+
notes.createIndex({ mentions: 1 }),
|
|
5123
|
+
notes.createIndex({ pageScopeId: 1 }),
|
|
5124
|
+
notes.createIndex({ stampScopeIds: 1 }),
|
|
5125
|
+
pins.createIndex({ id: 1 }, { unique: true }),
|
|
5126
|
+
pins.createIndex({ pageId: 1 }, { unique: true }),
|
|
5127
|
+
pins.createIndex({ audience: 1 }),
|
|
5128
|
+
activity.createIndex({ id: 1 }, { unique: true }),
|
|
4918
5129
|
activity.createIndex({
|
|
4919
|
-
|
|
5130
|
+
pageScopeId: 1,
|
|
5131
|
+
createdAt: -1,
|
|
4920
5132
|
id: -1
|
|
4921
5133
|
}),
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
createdAt: 1
|
|
5134
|
+
activity.createIndex({
|
|
5135
|
+
pageId: 1,
|
|
5136
|
+
createdAt: -1,
|
|
5137
|
+
id: -1
|
|
4927
5138
|
})
|
|
4928
5139
|
]);
|
|
4929
5140
|
}
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
5141
|
+
/** Drops exactly the Knowledge collections (never Memory's) and recreates schema 4. */
|
|
5142
|
+
async reset() {
|
|
5143
|
+
for (const name of KNOWLEDGE_RESETTABLE_TABLE_NAMES) await this.#drop(name);
|
|
5144
|
+
await this.init();
|
|
5145
|
+
}
|
|
5146
|
+
/** The ids of every scope page the read scope can see; `null` means everything. */
|
|
5147
|
+
async #readable(read, session) {
|
|
5148
|
+
if (read === "all") return null;
|
|
5149
|
+
const any = this.#readableClauses(read);
|
|
5150
|
+
if (!any.length) return [];
|
|
5151
|
+
return (await (await this.#c("pages")).find({
|
|
5152
|
+
isScope: true,
|
|
5153
|
+
$or: any
|
|
5154
|
+
}, {
|
|
5155
|
+
projection: {
|
|
5156
|
+
_id: 0,
|
|
5157
|
+
id: 1
|
|
5158
|
+
},
|
|
5159
|
+
session
|
|
5160
|
+
}).toArray()).map((row) => String(row.id));
|
|
5161
|
+
}
|
|
5162
|
+
#readableClauses(read) {
|
|
5163
|
+
const any = [];
|
|
5164
|
+
if (read.addresses.length) any.push({ address: { $in: read.addresses } });
|
|
5165
|
+
for (const entry of read.prefixes) any.push({
|
|
5166
|
+
declaration: entry.declaration,
|
|
5167
|
+
address: { $regex: `^${escapeRegex(entry.prefix)}` }
|
|
4933
5168
|
});
|
|
5169
|
+
for (const entry of read.children) any.push({
|
|
5170
|
+
declaration: entry.declaration,
|
|
5171
|
+
parentId: entry.parentId
|
|
5172
|
+
});
|
|
5173
|
+
return any;
|
|
4934
5174
|
}
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
5175
|
+
/**
|
|
5176
|
+
* Whether one scope page is readable, answered by a single indexed lookup instead of resolving the
|
|
5177
|
+
* whole frontier. Use it when a call targets one scope; the frontier can be thousands of scopes.
|
|
5178
|
+
*/
|
|
5179
|
+
async #scopeReadable(read, scopeId) {
|
|
5180
|
+
if (read === "all") return true;
|
|
5181
|
+
const any = this.#readableClauses(read);
|
|
5182
|
+
if (!any.length) return false;
|
|
5183
|
+
return await (await this.#c("pages")).countDocuments({
|
|
5184
|
+
id: scopeId,
|
|
5185
|
+
isScope: true,
|
|
5186
|
+
$or: any
|
|
5187
|
+
}, { limit: 1 }) > 0;
|
|
5188
|
+
}
|
|
5189
|
+
#inScopes(field, ids) {
|
|
5190
|
+
return ids === null ? {} : { [field]: { $in: ids } };
|
|
5191
|
+
}
|
|
5192
|
+
#visibleNotes(ids) {
|
|
5193
|
+
if (ids === null) return {};
|
|
5194
|
+
return {
|
|
5195
|
+
pageScopeId: { $in: ids },
|
|
5196
|
+
stampScopeIds: { $in: ids },
|
|
5197
|
+
mentionScopeIds: { $not: { $elemMatch: { $nin: ids } } }
|
|
5198
|
+
};
|
|
5199
|
+
}
|
|
5200
|
+
async #page(id, session) {
|
|
5201
|
+
const row = await (await this.#c("pages")).findOne({ id }, {
|
|
5202
|
+
...noProjection,
|
|
5203
|
+
session
|
|
5204
|
+
});
|
|
5205
|
+
return row ? toPage(row) : null;
|
|
5206
|
+
}
|
|
5207
|
+
async #requireLivePage(id, session) {
|
|
5208
|
+
const page = await this.#page(id, session);
|
|
5209
|
+
if (!page || page.deletedAt) throw new KnowledgeNotFoundError("page", id);
|
|
5210
|
+
return page;
|
|
5211
|
+
}
|
|
5212
|
+
async #log(session, event) {
|
|
5213
|
+
await (await this.#c("activity")).insertOne({
|
|
5214
|
+
id: createKnowledgeUlid(),
|
|
5215
|
+
action: event.action,
|
|
5216
|
+
scopeId: event.scopeId,
|
|
5217
|
+
pageScopeId: event.pageScopeId ?? event.scopeId,
|
|
5218
|
+
pageId: event.pageId,
|
|
5219
|
+
...event.noteId ? { noteId: event.noteId } : {},
|
|
5220
|
+
provenance: event.provenance,
|
|
5221
|
+
createdAt: event.at
|
|
5222
|
+
}, { session });
|
|
5223
|
+
}
|
|
5224
|
+
async #lastPosition(parentId, session) {
|
|
5225
|
+
const row = await (await this.#c("pages")).find({
|
|
5226
|
+
parentId,
|
|
5227
|
+
...live
|
|
5228
|
+
}, {
|
|
5229
|
+
projection: {
|
|
5230
|
+
_id: 0,
|
|
5231
|
+
position: 1
|
|
5232
|
+
},
|
|
5233
|
+
session
|
|
5234
|
+
}).sort({ position: -1 }).limit(1).next();
|
|
5235
|
+
return row ? String(row.position) : void 0;
|
|
5236
|
+
}
|
|
5237
|
+
/**
|
|
5238
|
+
* Ordinary descendants of `rootId`, level by level. Stops counting once the live total passes the
|
|
5239
|
+
* move cap, so an oversize subtree is reported without being walked to the end.
|
|
5240
|
+
*/
|
|
5241
|
+
async #subtree(rootId, session, find) {
|
|
5242
|
+
const core = await loadPagesCore();
|
|
5243
|
+
const pages = await this.#c("pages");
|
|
5244
|
+
const ids = [];
|
|
5245
|
+
let liveCount = 0;
|
|
5246
|
+
let maxDepth = 0;
|
|
5247
|
+
let frontier = [rootId];
|
|
5248
|
+
while (frontier.length) {
|
|
5249
|
+
const rows = await pages.find({
|
|
5250
|
+
parentId: { $in: frontier },
|
|
5251
|
+
...notScope
|
|
5252
|
+
}, {
|
|
5253
|
+
projection: {
|
|
5254
|
+
_id: 0,
|
|
5255
|
+
id: 1,
|
|
5256
|
+
depth: 1,
|
|
5257
|
+
deletedAt: 1
|
|
5258
|
+
},
|
|
5259
|
+
session
|
|
5260
|
+
}).toArray();
|
|
5261
|
+
frontier = [];
|
|
5262
|
+
for (const row of rows) {
|
|
5263
|
+
const id = String(row.id);
|
|
5264
|
+
if (find !== void 0 && id === find) return {
|
|
5265
|
+
ids,
|
|
5266
|
+
live: liveCount,
|
|
5267
|
+
maxDepth,
|
|
5268
|
+
found: true,
|
|
5269
|
+
truncated: false
|
|
5270
|
+
};
|
|
5271
|
+
ids.push(id);
|
|
5272
|
+
frontier.push(id);
|
|
5273
|
+
if (row.deletedAt == null) {
|
|
5274
|
+
liveCount++;
|
|
5275
|
+
maxDepth = Math.max(maxDepth, Number(row.depth));
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
if (liveCount > core.MAX_KNOWLEDGE_MOVE_PAGES) return {
|
|
5279
|
+
ids,
|
|
5280
|
+
live: liveCount,
|
|
5281
|
+
maxDepth,
|
|
5282
|
+
found: false,
|
|
5283
|
+
truncated: true
|
|
5284
|
+
};
|
|
4941
5285
|
}
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
5286
|
+
return {
|
|
5287
|
+
ids,
|
|
5288
|
+
live: liveCount,
|
|
5289
|
+
maxDepth,
|
|
5290
|
+
found: false,
|
|
5291
|
+
truncated: false
|
|
5292
|
+
};
|
|
5293
|
+
}
|
|
5294
|
+
/** Refuses before any write when a live arriving page's key is already live under `parentId`. */
|
|
5295
|
+
async #assertNoKeyConflicts(keys, parentId, session, exceptId) {
|
|
5296
|
+
const core = await loadPagesCore();
|
|
5297
|
+
if (!keys.length) return;
|
|
5298
|
+
const clash = await (await this.#c("pages")).findOne({
|
|
5299
|
+
...live,
|
|
5300
|
+
parentId,
|
|
5301
|
+
key: { $in: keys },
|
|
5302
|
+
...exceptId ? { id: { $ne: exceptId } } : {}
|
|
5303
|
+
}, {
|
|
5304
|
+
projection: {
|
|
5305
|
+
_id: 0,
|
|
5306
|
+
key: 1
|
|
5307
|
+
},
|
|
5308
|
+
session
|
|
5309
|
+
});
|
|
5310
|
+
if (clash) throw new core.KnowledgePageKeyConflictError(String(clash.key), parentId);
|
|
5311
|
+
}
|
|
5312
|
+
/** Recomputes the denormalized scope of every page a note mentions, for notes touching `pageIds`. */
|
|
5313
|
+
async #refreshMentionScopes(pageIds, session) {
|
|
5314
|
+
if (!pageIds.length) return;
|
|
5315
|
+
const notes = await this.#c("notes");
|
|
5316
|
+
const pages = await this.#c("pages");
|
|
5317
|
+
const affected = await notes.find({ mentions: { $in: pageIds } }, {
|
|
5318
|
+
projection: {
|
|
5319
|
+
_id: 0,
|
|
5320
|
+
id: 1,
|
|
5321
|
+
mentions: 1
|
|
5322
|
+
},
|
|
5323
|
+
session
|
|
5324
|
+
}).toArray();
|
|
5325
|
+
for (const note of affected) {
|
|
5326
|
+
const mentioned = await pages.find({ id: { $in: note.mentions } }, {
|
|
5327
|
+
projection: {
|
|
5328
|
+
_id: 0,
|
|
5329
|
+
scopeId: 1
|
|
5330
|
+
},
|
|
5331
|
+
session
|
|
5332
|
+
}).toArray();
|
|
5333
|
+
await notes.updateOne({ id: note.id }, { $set: { mentionScopeIds: [...new Set(mentioned.map((page) => String(page.scopeId)))] } }, { session });
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
async materializeScope(input) {
|
|
5337
|
+
for (let attempt = 0;; attempt++) try {
|
|
5338
|
+
return await this.#materializeScopeOnce(input);
|
|
5339
|
+
} catch (error) {
|
|
5340
|
+
if (!(error instanceof (await loadPagesCore()).KnowledgePageKeyConflictError || error instanceof KnowledgeConflictError || isDuplicateKey(error) || error?.hasErrorLabel?.("TransientTransactionError")) || attempt >= 4) throw error;
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5343
|
+
async #materializeScopeOnce(input) {
|
|
5344
|
+
const bounds = await this.#bounds();
|
|
5345
|
+
bounds.assertKnowledgePageDescriptionWithinBound(input.description);
|
|
5346
|
+
for (const child of input.children ?? []) bounds.assertKnowledgePageDescriptionWithinBound(child.description);
|
|
5347
|
+
const checkTemplates = (templates) => {
|
|
5348
|
+
for (const template of templates) {
|
|
5349
|
+
bounds.assertKnowledgePageDescriptionWithinBound(template.description);
|
|
5350
|
+
checkTemplates(template.pages ?? []);
|
|
4949
5351
|
}
|
|
5352
|
+
};
|
|
5353
|
+
checkTemplates(input.pages ?? []);
|
|
5354
|
+
return this.#tx(async (session) => {
|
|
4950
5355
|
const now = /* @__PURE__ */ new Date();
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
5356
|
+
let scope = await this.#scopeByAddress(input.address, session);
|
|
5357
|
+
let created = false;
|
|
5358
|
+
let parentConflict;
|
|
5359
|
+
if (!scope) {
|
|
5360
|
+
if (input.parentId) await this.#requireLivePage(input.parentId, session);
|
|
5361
|
+
scope = await this.#insertScope(session, {
|
|
5362
|
+
address: input.address,
|
|
5363
|
+
declaration: input.declaration,
|
|
5364
|
+
name: input.name,
|
|
5365
|
+
description: input.description,
|
|
5366
|
+
parentId: input.parentId ?? null,
|
|
5367
|
+
provenance: input.provenance,
|
|
5368
|
+
at: now
|
|
5369
|
+
});
|
|
5370
|
+
created = true;
|
|
5371
|
+
} else if (input.parentId && scope.parentId === null) {
|
|
5372
|
+
await this.#requireLivePage(input.parentId, session);
|
|
5373
|
+
const linked = await (await this.#c("pages")).findOneAndUpdate({
|
|
5374
|
+
id: scope.id,
|
|
5375
|
+
parentId: null
|
|
5376
|
+
}, { $set: { parentId: input.parentId } }, {
|
|
5377
|
+
returnDocument: "after",
|
|
5378
|
+
projection: { _id: 0 },
|
|
5379
|
+
session
|
|
5380
|
+
});
|
|
5381
|
+
scope = toPage(linked ?? await this.#scopeByAddress(input.address, session));
|
|
5382
|
+
if (!linked && input.parentId !== scope.parentId) parentConflict = {
|
|
5383
|
+
existingParentId: scope.parentId,
|
|
5384
|
+
requestedParentId: input.parentId
|
|
5385
|
+
};
|
|
5386
|
+
} else if (input.parentId !== void 0 && (input.parentId ?? null) !== scope.parentId) parentConflict = {
|
|
5387
|
+
existingParentId: scope.parentId,
|
|
5388
|
+
requestedParentId: input.parentId ?? null
|
|
5389
|
+
};
|
|
5390
|
+
for (const child of input.children ?? []) {
|
|
5391
|
+
if (await this.#scopeByAddress(child.address, session)) continue;
|
|
5392
|
+
await this.#insertScope(session, {
|
|
5393
|
+
address: child.address,
|
|
5394
|
+
declaration: child.declaration,
|
|
5395
|
+
name: child.name,
|
|
5396
|
+
description: child.description,
|
|
5397
|
+
parentId: scope.id,
|
|
5398
|
+
provenance: input.provenance,
|
|
5399
|
+
at: now
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
await this.#materializeTemplates(session, scope, input.pages ?? [], input.provenance, now);
|
|
5403
|
+
return {
|
|
4958
5404
|
scope,
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
updatedAt: now
|
|
5405
|
+
created,
|
|
5406
|
+
...parentConflict ? { parentConflict } : {}
|
|
4962
5407
|
};
|
|
4963
|
-
try {
|
|
4964
|
-
await (await this.#nodes()).insertOne({
|
|
4965
|
-
...node,
|
|
4966
|
-
canonicalName: canonicalName(node.name),
|
|
4967
|
-
scopeKey: knowledgeScopeKey(scope),
|
|
4968
|
-
mergedInto: null
|
|
4969
|
-
}, sessionOptions(session));
|
|
4970
|
-
} catch (error) {
|
|
4971
|
-
if (error.code !== 11e3) throw error;
|
|
4972
|
-
const concurrent = await this.#getNodeByName(input.name, scope, session);
|
|
4973
|
-
if (!concurrent) throw error;
|
|
4974
|
-
return await this.#resolveTerminalNode(concurrent.id, session);
|
|
4975
|
-
}
|
|
4976
|
-
await this.#replaceMentions("node", node.id, node.content ?? "", input.resolutionScope ?? scope, scope, session);
|
|
4977
|
-
await this.#activity("node-created", "node", node.id, scope, void 0, session);
|
|
4978
|
-
await this.#outbox("node", node.id, "upsert", 1, scope, session);
|
|
4979
|
-
return node;
|
|
4980
5408
|
});
|
|
4981
5409
|
}
|
|
4982
|
-
async
|
|
4983
|
-
|
|
5410
|
+
async #materializeTemplates(session, parent, templates, provenance, at) {
|
|
5411
|
+
const pages = await this.#c("pages");
|
|
5412
|
+
for (const template of templates) {
|
|
5413
|
+
const problem = knowledgePageKeyProblem(template.key);
|
|
5414
|
+
if (problem) throw new Error(problem);
|
|
5415
|
+
const existing = await pages.findOne({
|
|
5416
|
+
parentId: parent.id,
|
|
5417
|
+
key: template.key,
|
|
5418
|
+
...live
|
|
5419
|
+
}, {
|
|
5420
|
+
...noProjection,
|
|
5421
|
+
session
|
|
5422
|
+
});
|
|
5423
|
+
const page = existing ? toPage(existing) : await this.#insertPage(session, {
|
|
5424
|
+
parent,
|
|
5425
|
+
key: template.key,
|
|
5426
|
+
name: template.name,
|
|
5427
|
+
description: template.description,
|
|
5428
|
+
provenance,
|
|
5429
|
+
at
|
|
5430
|
+
});
|
|
5431
|
+
if (template.pages?.length) await this.#materializeTemplates(session, page, template.pages, provenance, at);
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
async #scopeByAddress(address, session) {
|
|
5435
|
+
const row = await (await this.#c("pages")).findOne({
|
|
5436
|
+
isScope: true,
|
|
5437
|
+
address
|
|
5438
|
+
}, {
|
|
5439
|
+
...noProjection,
|
|
5440
|
+
session
|
|
5441
|
+
});
|
|
5442
|
+
return row ? toPage(row) : null;
|
|
5443
|
+
}
|
|
5444
|
+
async #insertScope(session, input) {
|
|
5445
|
+
const core = await loadPagesCore();
|
|
5446
|
+
const id = createKnowledgeUlid();
|
|
5447
|
+
const doc = {
|
|
5448
|
+
id,
|
|
5449
|
+
scopeId: id,
|
|
5450
|
+
parentId: input.parentId,
|
|
5451
|
+
isScope: true,
|
|
5452
|
+
address: input.address,
|
|
5453
|
+
declaration: input.declaration,
|
|
5454
|
+
position: core.knowledgePositionBetween(await this.#lastPosition(input.parentId, session)),
|
|
5455
|
+
name: input.name,
|
|
5456
|
+
canonicalName: core.canonicalKnowledgeName(input.name),
|
|
5457
|
+
...input.description === void 0 ? {} : { description: input.description },
|
|
5458
|
+
depth: 0,
|
|
5459
|
+
version: 1,
|
|
5460
|
+
createdBy: input.provenance,
|
|
5461
|
+
updatedBy: input.provenance,
|
|
5462
|
+
createdAt: input.at,
|
|
5463
|
+
updatedAt: input.at
|
|
5464
|
+
};
|
|
5465
|
+
try {
|
|
5466
|
+
await (await this.#c("pages")).insertOne({ ...doc }, { session });
|
|
5467
|
+
} catch (error) {
|
|
5468
|
+
if (isDuplicateKey(error)) throw new KnowledgeConflictError(input.address);
|
|
5469
|
+
throw error;
|
|
5470
|
+
}
|
|
5471
|
+
await this.#log(session, {
|
|
5472
|
+
action: "scope-created",
|
|
5473
|
+
scopeId: id,
|
|
5474
|
+
pageId: id,
|
|
5475
|
+
provenance: input.provenance,
|
|
5476
|
+
at: input.at
|
|
5477
|
+
});
|
|
5478
|
+
return toPage(doc);
|
|
5479
|
+
}
|
|
5480
|
+
async getScopesByAddress(addresses) {
|
|
5481
|
+
if (addresses.length === 0) return [];
|
|
5482
|
+
const rows = await (await this.#c("pages")).find({
|
|
5483
|
+
isScope: true,
|
|
5484
|
+
address: { $in: addresses }
|
|
5485
|
+
}, noProjection).toArray();
|
|
5486
|
+
const byAddress = new Map(rows.map((row) => [String(row.address), toPage(row)]));
|
|
5487
|
+
return [...new Set(addresses)].flatMap((address) => byAddress.get(address) ?? []);
|
|
5488
|
+
}
|
|
5489
|
+
async listScopeDeclarations() {
|
|
5490
|
+
return (await (await this.#c("pages")).aggregate([{ $match: {
|
|
5491
|
+
isScope: true,
|
|
5492
|
+
declaration: { $type: "string" }
|
|
5493
|
+
} }, { $group: {
|
|
5494
|
+
_id: "$declaration",
|
|
5495
|
+
scopes: { $sum: 1 }
|
|
5496
|
+
} }]).toArray()).map((row) => ({
|
|
5497
|
+
declaration: String(row._id),
|
|
5498
|
+
scopes: Number(row.scopes)
|
|
5499
|
+
})).sort((a, b) => a.declaration < b.declaration ? -1 : a.declaration > b.declaration ? 1 : 0);
|
|
5500
|
+
}
|
|
5501
|
+
async renameScopeDeclaration(input) {
|
|
5502
|
+
return this.#tx(async (session) => {
|
|
5503
|
+
const pages = await this.#c("pages");
|
|
5504
|
+
const rows = await pages.find({
|
|
5505
|
+
isScope: true,
|
|
5506
|
+
$or: [{ declaration: input.from }, { declaration: { $regex: `^${escapeRegex(`${input.from}/`)}` } }]
|
|
5507
|
+
}, {
|
|
5508
|
+
projection: {
|
|
5509
|
+
_id: 0,
|
|
5510
|
+
id: 1,
|
|
5511
|
+
address: 1,
|
|
5512
|
+
declaration: 1
|
|
5513
|
+
},
|
|
5514
|
+
session
|
|
5515
|
+
}).toArray();
|
|
5516
|
+
if (rows.length === 0) return 0;
|
|
5517
|
+
const renames = rows.map((row) => ({
|
|
5518
|
+
address: String(row.address),
|
|
5519
|
+
from: String(row.declaration),
|
|
5520
|
+
to: input.to + String(row.declaration).slice(input.from.length)
|
|
5521
|
+
}));
|
|
5522
|
+
input.check(renames);
|
|
5523
|
+
if ((await pages.bulkWrite(rows.map((row, index) => ({ updateOne: {
|
|
5524
|
+
filter: {
|
|
5525
|
+
id: row.id,
|
|
5526
|
+
isScope: true
|
|
5527
|
+
},
|
|
5528
|
+
update: { $set: { declaration: renames[index].to } }
|
|
5529
|
+
} })), { session })).matchedCount < rows.length) throw new KnowledgeConflictError("Knowledge scope declarations changed while renaming; retry the rename");
|
|
5530
|
+
return rows.length;
|
|
5531
|
+
});
|
|
5532
|
+
}
|
|
5533
|
+
async listChildScopes(input) {
|
|
5534
|
+
const core = await loadPagesCore();
|
|
5535
|
+
const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
5536
|
+
const filter = {
|
|
5537
|
+
isScope: true,
|
|
5538
|
+
parentId: input.parentId,
|
|
5539
|
+
...live
|
|
5540
|
+
};
|
|
5541
|
+
if (input.cursor) {
|
|
5542
|
+
const [position, id] = decodeCursor(input.cursor);
|
|
5543
|
+
filter.$or = [{ position: { $gt: position } }, {
|
|
5544
|
+
position,
|
|
5545
|
+
id: { $gt: id }
|
|
5546
|
+
}];
|
|
5547
|
+
}
|
|
5548
|
+
const rows = await (await this.#c("pages")).find(filter, noProjection).sort({
|
|
5549
|
+
position: 1,
|
|
5550
|
+
id: 1
|
|
5551
|
+
}).limit(limit + 1).toArray();
|
|
5552
|
+
return this.#pageOutput(rows, limit, (page) => [page.position, page.id]);
|
|
5553
|
+
}
|
|
5554
|
+
#pageOutput(rows, limit, cursorOf) {
|
|
5555
|
+
const pages = rows.slice(0, limit).map(toPage);
|
|
5556
|
+
const last = pages[pages.length - 1];
|
|
5557
|
+
return {
|
|
5558
|
+
pages,
|
|
5559
|
+
...rows.length > limit && last ? { nextCursor: encodeCursor(cursorOf(last)) } : {}
|
|
5560
|
+
};
|
|
5561
|
+
}
|
|
5562
|
+
async getPage(input) {
|
|
5563
|
+
const row = await (await this.#c("pages")).findOne({ id: input.id }, noProjection);
|
|
5564
|
+
return row && await this.#scopeReadable(input.read, String(row.scopeId)) ? toPage(row) : null;
|
|
5565
|
+
}
|
|
5566
|
+
async getPageByPath(input) {
|
|
5567
|
+
if (!input.path.length) return null;
|
|
5568
|
+
if (!await this.#scopeReadable(input.read, input.scopeId)) return null;
|
|
5569
|
+
const pages = await this.#c("pages");
|
|
5570
|
+
let page;
|
|
5571
|
+
let parentId = input.scopeId;
|
|
5572
|
+
for (const key of input.path) {
|
|
5573
|
+
const row = await pages.findOne({
|
|
5574
|
+
parentId,
|
|
5575
|
+
key,
|
|
5576
|
+
scopeId: input.scopeId,
|
|
5577
|
+
...live,
|
|
5578
|
+
...notScope
|
|
5579
|
+
}, noProjection);
|
|
5580
|
+
if (!row) return null;
|
|
5581
|
+
page = toPage(row);
|
|
5582
|
+
parentId = page.id;
|
|
5583
|
+
}
|
|
5584
|
+
return page ?? null;
|
|
5585
|
+
}
|
|
5586
|
+
async findPagesByName(input) {
|
|
5587
|
+
const core = await loadPagesCore();
|
|
5588
|
+
const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
5589
|
+
const filter = {
|
|
5590
|
+
canonicalName: core.canonicalKnowledgeName(input.name),
|
|
5591
|
+
...live
|
|
5592
|
+
};
|
|
5593
|
+
if (!await this.#scopeFilter(filter, input.read, input.scopeId)) return [];
|
|
5594
|
+
return (await (await this.#c("pages")).find(filter, noProjection).sort({
|
|
5595
|
+
createdAt: -1,
|
|
5596
|
+
id: -1
|
|
5597
|
+
}).limit(limit).toArray()).map(toPage);
|
|
4984
5598
|
}
|
|
4985
|
-
|
|
4986
|
-
|
|
5599
|
+
/**
|
|
5600
|
+
* Adds the home-scope constraint: the explicit scope when given (one indexed readability check),
|
|
5601
|
+
* otherwise the readable frontier. Returns false when nothing can match.
|
|
5602
|
+
*/
|
|
5603
|
+
async #scopeFilter(filter, read, scopeId) {
|
|
5604
|
+
if (scopeId !== void 0) {
|
|
5605
|
+
if (!await this.#scopeReadable(read, scopeId)) return false;
|
|
5606
|
+
filter.scopeId = scopeId;
|
|
5607
|
+
return true;
|
|
5608
|
+
}
|
|
5609
|
+
const ids = await this.#readable(read);
|
|
5610
|
+
if (ids !== null) filter.scopeId = { $in: ids };
|
|
5611
|
+
return true;
|
|
4987
5612
|
}
|
|
4988
|
-
async
|
|
4989
|
-
|
|
5613
|
+
async listChildPages(input) {
|
|
5614
|
+
const core = await loadPagesCore();
|
|
5615
|
+
const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
5616
|
+
const ids = await this.#readable(input.read);
|
|
5617
|
+
const filter = {
|
|
5618
|
+
parentId: input.parentId,
|
|
5619
|
+
...notScope,
|
|
5620
|
+
...live,
|
|
5621
|
+
...this.#inScopes("scopeId", ids)
|
|
5622
|
+
};
|
|
5623
|
+
if (input.cursor) {
|
|
5624
|
+
const [position, id] = decodeCursor(input.cursor);
|
|
5625
|
+
filter.$or = [{ position: { $gt: position } }, {
|
|
5626
|
+
position,
|
|
5627
|
+
id: { $gt: id }
|
|
5628
|
+
}];
|
|
5629
|
+
}
|
|
5630
|
+
const rows = await (await this.#c("pages")).find(filter, noProjection).sort({
|
|
5631
|
+
position: 1,
|
|
5632
|
+
id: 1
|
|
5633
|
+
}).limit(limit + 1).toArray();
|
|
5634
|
+
return this.#pageOutput(rows, limit, (page) => [page.position, page.id]);
|
|
4990
5635
|
}
|
|
4991
|
-
async
|
|
5636
|
+
async listPages(input) {
|
|
5637
|
+
const core = await loadPagesCore();
|
|
5638
|
+
const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
4992
5639
|
const filter = {
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
scopeKey: { $in: visibleScopeKeys(canonicalizeKnowledgeScope(input.scope)) },
|
|
4996
|
-
...input.namePrefix ? { canonicalName: { $regex: `^${this.#escapeRegex(canonicalName(input.namePrefix))}` } } : {},
|
|
4997
|
-
...input.kind ? { kind: input.kind } : {},
|
|
4998
|
-
...input.hasContent === void 0 ? {} : input.hasContent ? { content: {
|
|
4999
|
-
$exists: true,
|
|
5000
|
-
$nin: [null, ""]
|
|
5001
|
-
} } : { $or: [
|
|
5002
|
-
{ content: { $exists: false } },
|
|
5003
|
-
{ content: null },
|
|
5004
|
-
{ content: "" }
|
|
5005
|
-
] },
|
|
5006
|
-
...input.cursor ? recordCursorFilter(input.cursor, {
|
|
5007
|
-
namePrefix: input.namePrefix,
|
|
5008
|
-
kind: input.kind,
|
|
5009
|
-
hasContent: input.hasContent
|
|
5010
|
-
}) : {}
|
|
5640
|
+
...notScope,
|
|
5641
|
+
...live
|
|
5011
5642
|
};
|
|
5012
|
-
|
|
5013
|
-
|
|
5643
|
+
if (!await this.#scopeFilter(filter, input.read, input.scopeId)) return { pages: [] };
|
|
5644
|
+
if (input.namePrefix) filter.canonicalName = { $regex: `^${escapeRegex(core.canonicalKnowledgeName(input.namePrefix))}` };
|
|
5645
|
+
if (input.cursor) {
|
|
5646
|
+
const [name, id] = decodeCursor(input.cursor);
|
|
5647
|
+
filter.$or = [{ name: { $gt: name } }, {
|
|
5648
|
+
name,
|
|
5649
|
+
id: { $gt: id }
|
|
5650
|
+
}];
|
|
5651
|
+
}
|
|
5652
|
+
const rows = await (await this.#c("pages")).find(filter, noProjection).sort({
|
|
5014
5653
|
name: 1,
|
|
5015
5654
|
id: 1
|
|
5016
|
-
}).limit(
|
|
5655
|
+
}).limit(limit + 1).toArray();
|
|
5656
|
+
return this.#pageOutput(rows, limit, (page) => [page.name, page.id]);
|
|
5657
|
+
}
|
|
5658
|
+
async createPage(input) {
|
|
5659
|
+
(await this.#bounds()).assertKnowledgePageDescriptionWithinBound(input.description);
|
|
5660
|
+
if (input.key !== void 0) {
|
|
5661
|
+
const problem = knowledgePageKeyProblem(input.key);
|
|
5662
|
+
if (problem) throw new Error(problem);
|
|
5663
|
+
}
|
|
5664
|
+
return this.#tx(async (session) => {
|
|
5665
|
+
const parent = await this.#requireLivePage(input.parentId, session);
|
|
5666
|
+
return this.#insertPage(session, {
|
|
5667
|
+
id: input.id,
|
|
5668
|
+
parent,
|
|
5669
|
+
key: input.key,
|
|
5670
|
+
name: input.name,
|
|
5671
|
+
description: input.description,
|
|
5672
|
+
body: input.body,
|
|
5673
|
+
position: input.position,
|
|
5674
|
+
provenance: input.provenance,
|
|
5675
|
+
at: /* @__PURE__ */ new Date()
|
|
5676
|
+
});
|
|
5677
|
+
});
|
|
5017
5678
|
}
|
|
5018
|
-
async
|
|
5019
|
-
await
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5679
|
+
async #insertPage(session, input) {
|
|
5680
|
+
const core = await loadPagesCore();
|
|
5681
|
+
const id = input.id ?? createKnowledgeUlid();
|
|
5682
|
+
const scopeId = input.parent.isScope ? input.parent.id : input.parent.scopeId;
|
|
5683
|
+
const depth = input.parent.isScope ? 1 : await this.#depthOf(input.parent.id, session) + 1;
|
|
5684
|
+
if (depth > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(id);
|
|
5685
|
+
const doc = {
|
|
5686
|
+
id,
|
|
5687
|
+
scopeId,
|
|
5688
|
+
parentId: input.parent.id,
|
|
5689
|
+
isScope: false,
|
|
5690
|
+
...input.key === void 0 ? {} : {
|
|
5691
|
+
key: input.key,
|
|
5692
|
+
activeKey: activeKeyOf(input.parent.id, input.key)
|
|
5693
|
+
},
|
|
5694
|
+
position: input.position ?? core.knowledgePositionBetween(await this.#lastPosition(input.parent.id, session)),
|
|
5695
|
+
name: input.name,
|
|
5696
|
+
canonicalName: core.canonicalKnowledgeName(input.name),
|
|
5697
|
+
...input.description === void 0 ? {} : { description: input.description },
|
|
5698
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
5699
|
+
depth,
|
|
5700
|
+
version: 1,
|
|
5701
|
+
createdBy: input.provenance,
|
|
5702
|
+
updatedBy: input.provenance,
|
|
5703
|
+
createdAt: input.at,
|
|
5704
|
+
updatedAt: input.at
|
|
5705
|
+
};
|
|
5706
|
+
try {
|
|
5707
|
+
await (await this.#c("pages")).insertOne({ ...doc }, { session });
|
|
5708
|
+
} catch (error) {
|
|
5709
|
+
if (isDuplicateKey(error)) {
|
|
5710
|
+
if (input.key) throw new core.KnowledgePageKeyConflictError(input.key, input.parent.id);
|
|
5711
|
+
throw new KnowledgeConflictError(id);
|
|
5712
|
+
}
|
|
5713
|
+
throw error;
|
|
5714
|
+
}
|
|
5715
|
+
await this.#log(session, {
|
|
5716
|
+
action: "page-created",
|
|
5717
|
+
scopeId,
|
|
5718
|
+
pageId: id,
|
|
5719
|
+
provenance: input.provenance,
|
|
5720
|
+
at: input.at
|
|
5721
|
+
});
|
|
5722
|
+
return toPage(doc);
|
|
5723
|
+
}
|
|
5724
|
+
async #depthOf(id, session) {
|
|
5725
|
+
const row = await (await this.#c("pages")).findOne({ id }, {
|
|
5726
|
+
projection: {
|
|
5727
|
+
_id: 0,
|
|
5728
|
+
depth: 1
|
|
5729
|
+
},
|
|
5730
|
+
session
|
|
5731
|
+
});
|
|
5732
|
+
return Number(row?.depth ?? 0);
|
|
5733
|
+
}
|
|
5734
|
+
async updatePage(input) {
|
|
5735
|
+
const core = await loadPagesCore();
|
|
5736
|
+
(await this.#bounds()).assertKnowledgePageDescriptionWithinBound(input.description);
|
|
5737
|
+
return this.#tx(async (session) => {
|
|
5738
|
+
const page = await this.#requireLivePage(input.id, session);
|
|
5739
|
+
if (page.version !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
|
|
5028
5740
|
const now = /* @__PURE__ */ new Date();
|
|
5029
|
-
const
|
|
5741
|
+
const set = {
|
|
5742
|
+
updatedAt: now,
|
|
5743
|
+
updatedBy: input.provenance
|
|
5744
|
+
};
|
|
5745
|
+
const unset = {};
|
|
5746
|
+
if (input.name !== void 0) {
|
|
5747
|
+
set.name = input.name;
|
|
5748
|
+
set.canonicalName = core.canonicalKnowledgeName(input.name);
|
|
5749
|
+
}
|
|
5750
|
+
if (input.description !== void 0) {
|
|
5751
|
+
if (input.description === "") unset.description = "";
|
|
5752
|
+
else set.description = input.description;
|
|
5753
|
+
}
|
|
5754
|
+
if (input.body !== void 0) set.body = input.body;
|
|
5755
|
+
const updated = await (await this.#c("pages")).findOneAndUpdate({
|
|
5030
5756
|
id: input.id,
|
|
5031
|
-
|
|
5032
|
-
|
|
5757
|
+
version: input.expectedVersion,
|
|
5758
|
+
...live
|
|
5033
5759
|
}, {
|
|
5034
|
-
$set:
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
kind: input.kind ?? existing.kind,
|
|
5038
|
-
content: content ?? null,
|
|
5039
|
-
description: description ?? null,
|
|
5040
|
-
scope,
|
|
5041
|
-
scopeKey: knowledgeScopeKey(scope),
|
|
5042
|
-
updatedAt: now
|
|
5043
|
-
},
|
|
5044
|
-
$inc: { version: 1 }
|
|
5760
|
+
$set: set,
|
|
5761
|
+
$inc: { version: 1 },
|
|
5762
|
+
...Object.keys(unset).length ? { $unset: unset } : {}
|
|
5045
5763
|
}, {
|
|
5046
|
-
|
|
5047
|
-
|
|
5764
|
+
returnDocument: "after",
|
|
5765
|
+
projection: { _id: 0 },
|
|
5766
|
+
session
|
|
5048
5767
|
});
|
|
5049
|
-
if (!
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5768
|
+
if (!updated) throw new KnowledgeConflictError(input.id);
|
|
5769
|
+
await this.#log(session, {
|
|
5770
|
+
action: "page-updated",
|
|
5771
|
+
scopeId: page.scopeId,
|
|
5772
|
+
pageId: page.id,
|
|
5773
|
+
provenance: input.provenance,
|
|
5774
|
+
at: now
|
|
5775
|
+
});
|
|
5776
|
+
return toPage(updated);
|
|
5777
|
+
});
|
|
5778
|
+
}
|
|
5779
|
+
/** Rewrites everything that follows a page's home scope to `scopeId`, for the pages in `moved`. */
|
|
5780
|
+
async #rehome(moved, scopeId, depthDelta, session) {
|
|
5781
|
+
const pages = await this.#c("pages");
|
|
5782
|
+
const descendants = moved.slice(1);
|
|
5783
|
+
if (descendants.length) await pages.updateMany({ id: { $in: descendants } }, [{ $set: {
|
|
5784
|
+
scopeId,
|
|
5785
|
+
depth: { $add: ["$depth", depthDelta] }
|
|
5786
|
+
} }], { session });
|
|
5787
|
+
await (await this.#c("notes")).updateMany({ pageId: { $in: moved } }, { $set: { pageScopeId: scopeId } }, { session });
|
|
5788
|
+
await (await this.#c("activity")).updateMany({ pageId: { $in: moved } }, { $set: { pageScopeId: scopeId } }, { session });
|
|
5789
|
+
}
|
|
5790
|
+
async movePage(input) {
|
|
5791
|
+
const core = await loadPagesCore();
|
|
5792
|
+
return this.#tx(async (session) => {
|
|
5793
|
+
const page = await this.#requireLivePage(input.id, session);
|
|
5794
|
+
if (page.version !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
|
|
5795
|
+
if (page.isScope) throw new Error(`Knowledge scope page ${input.id} can't be moved`);
|
|
5796
|
+
if (input.parentId === input.id) throw new Error(`Knowledge page ${input.id} can't move under itself or its own subtree`);
|
|
5797
|
+
const tree = await this.#subtree(input.id, session, input.parentId);
|
|
5798
|
+
if (tree.found) throw new Error(`Knowledge page ${input.id} can't move under itself or its own subtree`);
|
|
5799
|
+
const parent = await this.#requireLivePage(input.parentId, session);
|
|
5800
|
+
if (tree.live + 1 > core.MAX_KNOWLEDGE_MOVE_PAGES) throw new core.KnowledgeMoveTooLargeError(input.id, tree.live + 1);
|
|
5801
|
+
const newScopeId = parent.isScope ? parent.id : parent.scopeId;
|
|
5802
|
+
const newDepth = parent.isScope ? 1 : await this.#depthOf(parent.id, session) + 1;
|
|
5803
|
+
const delta = newDepth - await this.#depthOf(page.id, session);
|
|
5804
|
+
if (page.key) await this.#assertNoKeyConflicts([page.key], parent.id, session, page.id);
|
|
5805
|
+
if (tree.maxDepth + delta > core.MAX_KNOWLEDGE_PAGE_DEPTH || newDepth > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(input.id);
|
|
5806
|
+
const position = input.position ?? core.knowledgePositionBetween(await this.#lastPosition(parent.id, session));
|
|
5807
|
+
const now = /* @__PURE__ */ new Date();
|
|
5808
|
+
let updated;
|
|
5809
|
+
try {
|
|
5810
|
+
await this.#rehome([page.id, ...tree.ids], newScopeId, delta, session);
|
|
5811
|
+
updated = await (await this.#c("pages")).findOneAndUpdate({
|
|
5812
|
+
id: input.id,
|
|
5813
|
+
version: input.expectedVersion
|
|
5814
|
+
}, {
|
|
5815
|
+
$set: {
|
|
5816
|
+
parentId: parent.id,
|
|
5817
|
+
scopeId: newScopeId,
|
|
5818
|
+
...page.key ? { activeKey: activeKeyOf(parent.id, page.key) } : {},
|
|
5819
|
+
depth: newDepth,
|
|
5820
|
+
position,
|
|
5821
|
+
updatedAt: now,
|
|
5822
|
+
updatedBy: input.provenance
|
|
5823
|
+
},
|
|
5824
|
+
$inc: { version: 1 }
|
|
5825
|
+
}, {
|
|
5826
|
+
returnDocument: "after",
|
|
5827
|
+
projection: { _id: 0 },
|
|
5828
|
+
session
|
|
5829
|
+
});
|
|
5830
|
+
} catch (error) {
|
|
5831
|
+
if (isDuplicateKey(error)) throw new KnowledgeConflictError(page.key ? `${parent.id}#${page.key}` : input.id);
|
|
5832
|
+
throw error;
|
|
5058
5833
|
}
|
|
5059
|
-
|
|
5060
|
-
await this.#
|
|
5061
|
-
|
|
5834
|
+
if (!updated) throw new KnowledgeConflictError(input.id);
|
|
5835
|
+
await this.#refreshMentionScopes([page.id, ...tree.ids], session);
|
|
5836
|
+
await this.#log(session, {
|
|
5837
|
+
action: "page-moved",
|
|
5838
|
+
scopeId: newScopeId,
|
|
5839
|
+
pageId: input.id,
|
|
5840
|
+
provenance: input.provenance,
|
|
5841
|
+
at: now
|
|
5842
|
+
});
|
|
5843
|
+
return toPage(updated);
|
|
5062
5844
|
});
|
|
5063
5845
|
}
|
|
5064
|
-
async
|
|
5065
|
-
|
|
5066
|
-
return this.#
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
const target = await this.#
|
|
5070
|
-
if (
|
|
5071
|
-
if (
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
const
|
|
5076
|
-
|
|
5846
|
+
async mergePages(input) {
|
|
5847
|
+
const core = await loadPagesCore();
|
|
5848
|
+
return this.#tx(async (session) => {
|
|
5849
|
+
if (input.sourceId === input.targetId) throw new Error(`Knowledge page ${input.sourceId} can't merge into itself or its own subtree`);
|
|
5850
|
+
const source = await this.#requireLivePage(input.sourceId, session);
|
|
5851
|
+
const target = await this.#requireLivePage(input.targetId, session);
|
|
5852
|
+
if (source.version !== input.sourceVersion) throw new KnowledgeConflictError(input.sourceId);
|
|
5853
|
+
if (source.isScope || target.isScope) throw new Error("Knowledge scope pages can't be merged");
|
|
5854
|
+
const tree = await this.#subtree(source.id, session, target.id);
|
|
5855
|
+
if (tree.found) throw new Error(`Knowledge page ${source.id} can't merge into itself or its own subtree`);
|
|
5856
|
+
if (tree.live > core.MAX_KNOWLEDGE_MOVE_PAGES) throw new core.KnowledgeMoveTooLargeError(source.id, tree.live);
|
|
5857
|
+
const delta = await this.#depthOf(target.id, session) - await this.#depthOf(source.id, session);
|
|
5858
|
+
const arriving = await (await this.#c("pages")).find({
|
|
5859
|
+
parentId: source.id,
|
|
5860
|
+
...live,
|
|
5861
|
+
...notScope,
|
|
5862
|
+
key: { $type: "string" }
|
|
5863
|
+
}, {
|
|
5864
|
+
projection: {
|
|
5865
|
+
_id: 0,
|
|
5866
|
+
key: 1
|
|
5867
|
+
},
|
|
5868
|
+
session
|
|
5869
|
+
}).toArray();
|
|
5870
|
+
await this.#assertNoKeyConflicts(arriving.map((row) => String(row.key)), target.id, session, source.id);
|
|
5871
|
+
if (tree.maxDepth > 0 && tree.maxDepth + delta > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(source.id);
|
|
5872
|
+
const now = /* @__PURE__ */ new Date();
|
|
5873
|
+
const pages = await this.#c("pages");
|
|
5874
|
+
const notes = await this.#c("notes");
|
|
5875
|
+
if ((await pages.updateOne({
|
|
5077
5876
|
id: source.id,
|
|
5078
|
-
|
|
5079
|
-
version: input.sourceVersion,
|
|
5080
|
-
mergedInto: null
|
|
5877
|
+
version: input.sourceVersion
|
|
5081
5878
|
}, {
|
|
5082
5879
|
$set: {
|
|
5880
|
+
deletedAt: now,
|
|
5881
|
+
deletedBy: input.provenance.author,
|
|
5083
5882
|
mergedInto: target.id,
|
|
5084
|
-
|
|
5883
|
+
scopeId: target.scopeId,
|
|
5884
|
+
updatedAt: now,
|
|
5885
|
+
updatedBy: input.provenance
|
|
5085
5886
|
},
|
|
5887
|
+
$unset: { activeKey: "" },
|
|
5086
5888
|
$inc: { version: 1 }
|
|
5087
|
-
},
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
},
|
|
5101
|
-
|
|
5889
|
+
}, { session })).matchedCount === 0) throw new KnowledgeConflictError(input.sourceId);
|
|
5890
|
+
try {
|
|
5891
|
+
if (tree.ids.length) {
|
|
5892
|
+
await pages.updateMany({ id: { $in: tree.ids } }, [{ $set: {
|
|
5893
|
+
scopeId: target.scopeId,
|
|
5894
|
+
depth: { $add: ["$depth", delta] }
|
|
5895
|
+
} }], { session });
|
|
5896
|
+
await notes.updateMany({ pageId: { $in: tree.ids } }, { $set: { pageScopeId: target.scopeId } }, { session });
|
|
5897
|
+
await (await this.#c("activity")).updateMany({ pageId: { $in: tree.ids } }, { $set: { pageScopeId: target.scopeId } }, { session });
|
|
5898
|
+
}
|
|
5899
|
+
await pages.updateMany({
|
|
5900
|
+
parentId: source.id,
|
|
5901
|
+
...notScope
|
|
5902
|
+
}, [{ $set: { parentId: target.id } }, recomputeActiveKey(target.id)], { session });
|
|
5903
|
+
} catch (error) {
|
|
5904
|
+
if (isDuplicateKey(error)) throw new KnowledgeConflictError(source.id);
|
|
5905
|
+
throw error;
|
|
5102
5906
|
}
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
await this.#activity("node-updated", "node", target.id, target.scope, void 0, session);
|
|
5125
|
-
}
|
|
5907
|
+
await notes.updateMany({ pageId: source.id }, { $set: {
|
|
5908
|
+
pageId: target.id,
|
|
5909
|
+
pageScopeId: target.scopeId
|
|
5910
|
+
} }, { session });
|
|
5911
|
+
await notes.updateMany({ mentions: source.id }, [{ $set: { mentions: { $setUnion: [{ $map: {
|
|
5912
|
+
input: "$mentions",
|
|
5913
|
+
in: { $cond: [
|
|
5914
|
+
{ $eq: ["$$this", source.id] },
|
|
5915
|
+
target.id,
|
|
5916
|
+
"$$this"
|
|
5917
|
+
] }
|
|
5918
|
+
} }] } } }], { session });
|
|
5919
|
+
await this.#refreshMentionScopes([target.id, ...tree.ids], session);
|
|
5920
|
+
const pins = await this.#c("pins");
|
|
5921
|
+
const sourcePin = await pins.findOne({ pageId: source.id }, {
|
|
5922
|
+
...noProjection,
|
|
5923
|
+
session
|
|
5924
|
+
});
|
|
5925
|
+
if (sourcePin) {
|
|
5926
|
+
await this.#upsertPinAudience(session, target.id, sourcePin.audience ?? [], String(sourcePin.createdBy), now);
|
|
5927
|
+
await pins.deleteOne({ id: sourcePin.id }, { session });
|
|
5126
5928
|
}
|
|
5127
|
-
await this.#
|
|
5128
|
-
await this.#
|
|
5129
|
-
|
|
5130
|
-
|
|
5929
|
+
await (await this.#c("activity")).updateMany({ pageId: source.id }, { $set: { pageScopeId: target.scopeId } }, { session });
|
|
5930
|
+
await this.#log(session, {
|
|
5931
|
+
action: "page-merged",
|
|
5932
|
+
scopeId: target.scopeId,
|
|
5933
|
+
pageScopeId: target.scopeId,
|
|
5934
|
+
pageId: source.id,
|
|
5935
|
+
provenance: input.provenance,
|
|
5936
|
+
at: now
|
|
5937
|
+
});
|
|
5938
|
+
return await this.#page(target.id, session);
|
|
5131
5939
|
});
|
|
5132
5940
|
}
|
|
5133
|
-
async
|
|
5134
|
-
await
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
id,
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
}
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5941
|
+
async deletePage(input) {
|
|
5942
|
+
const core = await loadPagesCore();
|
|
5943
|
+
return this.#tx(async (session) => {
|
|
5944
|
+
const page = await this.#requireLivePage(input.id, session);
|
|
5945
|
+
if (page.version !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
|
|
5946
|
+
if (page.isScope) throw new Error(`Knowledge scope page ${input.id} can't be deleted`);
|
|
5947
|
+
const pages = await this.#c("pages");
|
|
5948
|
+
if (await pages.findOne({
|
|
5949
|
+
parentId: input.id,
|
|
5950
|
+
...live
|
|
5951
|
+
}, {
|
|
5952
|
+
projection: { id: 1 },
|
|
5953
|
+
session
|
|
5954
|
+
})) throw new core.KnowledgePageHasChildrenError(input.id);
|
|
5955
|
+
const now = /* @__PURE__ */ new Date();
|
|
5956
|
+
const updated = await pages.findOneAndUpdate({
|
|
5957
|
+
id: input.id,
|
|
5958
|
+
version: input.expectedVersion,
|
|
5959
|
+
...live
|
|
5960
|
+
}, {
|
|
5961
|
+
$set: {
|
|
5962
|
+
deletedAt: now,
|
|
5963
|
+
deletedBy: input.deletedBy,
|
|
5964
|
+
updatedAt: now,
|
|
5965
|
+
updatedBy: { author: input.deletedBy }
|
|
5966
|
+
},
|
|
5967
|
+
$unset: { activeKey: "" },
|
|
5968
|
+
$inc: { version: 1 }
|
|
5969
|
+
}, {
|
|
5970
|
+
returnDocument: "after",
|
|
5971
|
+
projection: { _id: 0 },
|
|
5972
|
+
session
|
|
5973
|
+
});
|
|
5974
|
+
if (!updated) throw new KnowledgeConflictError(input.id);
|
|
5975
|
+
await (await this.#c("pins")).deleteOne({ pageId: input.id }, { session });
|
|
5976
|
+
await this.#log(session, {
|
|
5977
|
+
action: "page-deleted",
|
|
5978
|
+
scopeId: page.scopeId,
|
|
5979
|
+
pageId: page.id,
|
|
5980
|
+
provenance: { author: input.deletedBy },
|
|
5981
|
+
at: now
|
|
5982
|
+
});
|
|
5983
|
+
return toPage(updated);
|
|
5164
5984
|
});
|
|
5165
5985
|
}
|
|
5166
|
-
async
|
|
5167
|
-
const
|
|
5986
|
+
async getNote(input) {
|
|
5987
|
+
const ids = await this.#readable(input.read);
|
|
5988
|
+
const filter = {
|
|
5168
5989
|
id: input.id,
|
|
5169
|
-
...
|
|
5170
|
-
}
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5990
|
+
...this.#visibleNotes(ids)
|
|
5991
|
+
};
|
|
5992
|
+
if (!input.includeDeleted) Object.assign(filter, live);
|
|
5993
|
+
const row = await (await this.#c("notes")).findOne(filter, noProjection);
|
|
5994
|
+
return row ? toNote(row) : null;
|
|
5995
|
+
}
|
|
5996
|
+
async listNotes(input) {
|
|
5997
|
+
const core = await loadPagesCore();
|
|
5998
|
+
const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
5999
|
+
const ids = await this.#readable(input.read);
|
|
6000
|
+
const filter = {
|
|
6001
|
+
pageId: input.pageId,
|
|
6002
|
+
...this.#visibleNotes(ids)
|
|
6003
|
+
};
|
|
6004
|
+
if (!input.includeDeleted) Object.assign(filter, live);
|
|
6005
|
+
if (input.cursor) {
|
|
6006
|
+
const [createdAt, id] = decodeCursor(input.cursor);
|
|
6007
|
+
const at = new Date(createdAt);
|
|
6008
|
+
filter.$or = [{ createdAt: { $gt: at } }, {
|
|
6009
|
+
createdAt: at,
|
|
6010
|
+
id: { $gt: id }
|
|
6011
|
+
}];
|
|
6012
|
+
}
|
|
6013
|
+
const rows = await (await this.#c("notes")).find(filter, noProjection).sort({
|
|
6014
|
+
createdAt: 1,
|
|
6015
|
+
id: 1
|
|
6016
|
+
}).limit(limit + 1).toArray();
|
|
6017
|
+
return this.#noteOutput(rows, limit);
|
|
6018
|
+
}
|
|
6019
|
+
async countNotes(input) {
|
|
6020
|
+
const ids = [...new Set(input.pageIds)];
|
|
6021
|
+
const counts = Object.fromEntries(ids.map((id) => [id, 0]));
|
|
6022
|
+
if (!ids.length) return counts;
|
|
6023
|
+
const readable = await this.#readable(input.read);
|
|
6024
|
+
const rows = await (await this.#c("notes")).aggregate([{ $match: {
|
|
6025
|
+
pageId: { $in: ids },
|
|
6026
|
+
...live,
|
|
6027
|
+
...this.#visibleNotes(readable)
|
|
6028
|
+
} }, { $group: {
|
|
6029
|
+
_id: "$pageId",
|
|
6030
|
+
count: { $sum: 1 }
|
|
6031
|
+
} }]).toArray();
|
|
6032
|
+
for (const row of rows) counts[row._id] = row.count;
|
|
6033
|
+
return counts;
|
|
6034
|
+
}
|
|
6035
|
+
async listNotesMentioning(input) {
|
|
6036
|
+
const core = await loadPagesCore();
|
|
6037
|
+
const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
6038
|
+
const ids = await this.#readable(input.read);
|
|
6039
|
+
const match = {
|
|
6040
|
+
mentions: input.pageId,
|
|
6041
|
+
pageId: { $ne: input.pageId },
|
|
6042
|
+
...live,
|
|
6043
|
+
...this.#visibleNotes(ids)
|
|
6044
|
+
};
|
|
6045
|
+
if (input.cursor) {
|
|
6046
|
+
const [createdAt, id] = decodeCursor(input.cursor);
|
|
6047
|
+
const at = new Date(createdAt);
|
|
6048
|
+
match.$or = [{ createdAt: { $lt: at } }, {
|
|
6049
|
+
createdAt: at,
|
|
6050
|
+
id: { $lt: id }
|
|
6051
|
+
}];
|
|
6052
|
+
}
|
|
6053
|
+
const rows = await (await this.#c("notes")).aggregate([
|
|
6054
|
+
{ $match: match },
|
|
6055
|
+
{ $lookup: {
|
|
6056
|
+
from: MONGO_KNOWLEDGE_PAGES_COLLECTIONS.pages,
|
|
6057
|
+
localField: "pageId",
|
|
6058
|
+
foreignField: "id",
|
|
6059
|
+
as: "page"
|
|
6060
|
+
} },
|
|
6061
|
+
{ $match: { "page.deletedAt": { $exists: false } } },
|
|
6062
|
+
{ $project: {
|
|
6063
|
+
_id: 0,
|
|
6064
|
+
page: 0
|
|
6065
|
+
} },
|
|
6066
|
+
{ $sort: {
|
|
6067
|
+
createdAt: -1,
|
|
6068
|
+
id: -1
|
|
6069
|
+
} },
|
|
6070
|
+
{ $limit: limit + 1 }
|
|
6071
|
+
]).toArray();
|
|
6072
|
+
return this.#noteOutput(rows, limit);
|
|
6073
|
+
}
|
|
6074
|
+
#noteOutput(rows, limit) {
|
|
6075
|
+
const notes = rows.slice(0, limit).map(toNote);
|
|
6076
|
+
const last = notes[notes.length - 1];
|
|
5191
6077
|
return {
|
|
5192
|
-
|
|
5193
|
-
|
|
6078
|
+
notes,
|
|
6079
|
+
...rows.length > limit && last ? { nextCursor: encodeCursor([last.createdAt.toISOString(), last.id]) } : {}
|
|
5194
6080
|
};
|
|
5195
6081
|
}
|
|
5196
|
-
async
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
id:
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
6082
|
+
async #assertScopePages(ids, session) {
|
|
6083
|
+
const rows = await (await this.#c("pages")).find({
|
|
6084
|
+
id: { $in: ids },
|
|
6085
|
+
isScope: true
|
|
6086
|
+
}, {
|
|
6087
|
+
projection: {
|
|
6088
|
+
_id: 0,
|
|
6089
|
+
id: 1
|
|
6090
|
+
},
|
|
6091
|
+
session
|
|
6092
|
+
}).toArray();
|
|
6093
|
+
const found = new Set(rows.map((row) => String(row.id)));
|
|
6094
|
+
const missing = ids.find((id) => !found.has(id));
|
|
6095
|
+
if (missing) throw new Error(`Knowledge note stamp ${missing} is not a scope page`);
|
|
6096
|
+
}
|
|
6097
|
+
async addNote(input) {
|
|
6098
|
+
(await this.#bounds()).assertKnowledgeNoteTextWithinBound(input.text);
|
|
6099
|
+
if (input.stampScopeIds.length === 0) throw new Error("A Knowledge note needs at least one stamp scope");
|
|
6100
|
+
return this.#tx(async (session) => {
|
|
6101
|
+
const page = await this.#requireLivePage(input.pageId, session);
|
|
6102
|
+
const stampIds = [...new Set(input.stampScopeIds)];
|
|
6103
|
+
await this.#assertScopePages(stampIds, session);
|
|
6104
|
+
const mentionIds = [...new Set(input.mentions)];
|
|
6105
|
+
let mentionScopeIds = [];
|
|
6106
|
+
if (mentionIds.length) {
|
|
6107
|
+
const found = await (await this.#c("pages")).find({ id: { $in: mentionIds } }, {
|
|
6108
|
+
projection: {
|
|
6109
|
+
_id: 0,
|
|
6110
|
+
id: 1,
|
|
6111
|
+
scopeId: 1
|
|
6112
|
+
},
|
|
6113
|
+
session
|
|
6114
|
+
}).toArray();
|
|
6115
|
+
const known = new Set(found.map((row) => String(row.id)));
|
|
6116
|
+
const missing = mentionIds.find((id) => !known.has(id));
|
|
6117
|
+
if (missing) throw new KnowledgeNotFoundError("page", missing);
|
|
6118
|
+
mentionScopeIds = [...new Set(found.map((row) => String(row.scopeId)))];
|
|
6119
|
+
}
|
|
6120
|
+
const id = input.id ?? createKnowledgeUlid();
|
|
6121
|
+
const now = /* @__PURE__ */ new Date();
|
|
6122
|
+
const doc = {
|
|
6123
|
+
id,
|
|
6124
|
+
pageId: page.id,
|
|
6125
|
+
pageScopeId: page.scopeId,
|
|
6126
|
+
text: input.text,
|
|
6127
|
+
...input.when ? { when: input.when } : {},
|
|
6128
|
+
stampScopeIds: stampIds,
|
|
6129
|
+
mentions: mentionIds,
|
|
6130
|
+
mentionScopeIds,
|
|
6131
|
+
provenance: input.provenance,
|
|
6132
|
+
version: 1,
|
|
6133
|
+
createdAt: now,
|
|
6134
|
+
updatedAt: now
|
|
5215
6135
|
};
|
|
6136
|
+
try {
|
|
6137
|
+
await (await this.#c("notes")).insertOne({ ...doc }, { session });
|
|
6138
|
+
} catch (error) {
|
|
6139
|
+
if (isDuplicateKey(error)) throw new KnowledgeConflictError(id);
|
|
6140
|
+
throw error;
|
|
6141
|
+
}
|
|
6142
|
+
await this.#log(session, {
|
|
6143
|
+
action: "note-added",
|
|
6144
|
+
scopeId: page.scopeId,
|
|
6145
|
+
pageId: page.id,
|
|
6146
|
+
noteId: id,
|
|
6147
|
+
provenance: input.provenance,
|
|
6148
|
+
at: now
|
|
6149
|
+
});
|
|
6150
|
+
return toNote(doc);
|
|
5216
6151
|
});
|
|
5217
6152
|
}
|
|
5218
|
-
async
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
6153
|
+
async #requireNote(id, session) {
|
|
6154
|
+
const row = await (await this.#c("notes")).findOne({ id }, {
|
|
6155
|
+
...noProjection,
|
|
6156
|
+
session
|
|
6157
|
+
});
|
|
6158
|
+
if (!row) throw new KnowledgeNotFoundError("note", id);
|
|
6159
|
+
return row;
|
|
6160
|
+
}
|
|
6161
|
+
async restampNote(input) {
|
|
6162
|
+
if (input.stampScopeIds.length === 0) throw new Error("A Knowledge note needs at least one stamp scope");
|
|
6163
|
+
return this.#tx(async (session) => {
|
|
6164
|
+
const note = await this.#requireNote(input.id, session);
|
|
6165
|
+
if (note.deletedAt) throw new KnowledgeNotFoundError("note", input.id);
|
|
6166
|
+
if (Number(note.version) !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
|
|
6167
|
+
const stampIds = [...new Set(input.stampScopeIds)];
|
|
6168
|
+
await this.#assertScopePages(stampIds, session);
|
|
6169
|
+
const now = /* @__PURE__ */ new Date();
|
|
6170
|
+
const updated = await (await this.#c("notes")).findOneAndUpdate({
|
|
6171
|
+
id: input.id,
|
|
6172
|
+
version: input.expectedVersion
|
|
6173
|
+
}, {
|
|
6174
|
+
$set: {
|
|
6175
|
+
stampScopeIds: stampIds,
|
|
6176
|
+
updatedAt: now
|
|
6177
|
+
},
|
|
6178
|
+
$inc: { version: 1 }
|
|
6179
|
+
}, {
|
|
6180
|
+
returnDocument: "after",
|
|
6181
|
+
projection: { _id: 0 },
|
|
6182
|
+
session
|
|
6183
|
+
});
|
|
6184
|
+
if (!updated) throw new KnowledgeConflictError(input.id);
|
|
6185
|
+
await this.#log(session, {
|
|
6186
|
+
action: "note-restamped",
|
|
6187
|
+
scopeId: String(note.pageScopeId),
|
|
6188
|
+
pageId: String(note.pageId),
|
|
6189
|
+
noteId: input.id,
|
|
6190
|
+
provenance: input.provenance,
|
|
6191
|
+
at: now
|
|
6192
|
+
});
|
|
6193
|
+
return toNote(updated);
|
|
5234
6194
|
});
|
|
5235
6195
|
}
|
|
5236
|
-
async
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
await (await this.#
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
6196
|
+
async removeNote(input) {
|
|
6197
|
+
return this.#tx(async (session) => {
|
|
6198
|
+
const note = await this.#requireNote(input.id, session);
|
|
6199
|
+
if (note.deletedAt) return toNote(note);
|
|
6200
|
+
const now = /* @__PURE__ */ new Date();
|
|
6201
|
+
const updated = await (await this.#c("notes")).findOneAndUpdate({ id: input.id }, {
|
|
6202
|
+
$set: {
|
|
6203
|
+
deletedAt: now,
|
|
6204
|
+
deletedBy: input.deletedBy,
|
|
6205
|
+
updatedAt: now
|
|
6206
|
+
},
|
|
6207
|
+
$inc: { version: 1 }
|
|
6208
|
+
}, {
|
|
6209
|
+
returnDocument: "after",
|
|
6210
|
+
projection: { _id: 0 },
|
|
6211
|
+
session
|
|
6212
|
+
});
|
|
6213
|
+
await this.#log(session, {
|
|
6214
|
+
action: "note-removed",
|
|
6215
|
+
scopeId: String(note.pageScopeId),
|
|
6216
|
+
pageId: String(note.pageId),
|
|
6217
|
+
noteId: input.id,
|
|
6218
|
+
provenance: input.provenance,
|
|
6219
|
+
at: now
|
|
6220
|
+
});
|
|
6221
|
+
return toNote(updated);
|
|
5252
6222
|
});
|
|
5253
6223
|
}
|
|
5254
|
-
async
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
{
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
6224
|
+
async restoreNote(input) {
|
|
6225
|
+
return this.#tx(async (session) => {
|
|
6226
|
+
const note = await this.#requireNote(input.id, session);
|
|
6227
|
+
if (!note.deletedAt) return toNote(note);
|
|
6228
|
+
const now = /* @__PURE__ */ new Date();
|
|
6229
|
+
const updated = await (await this.#c("notes")).findOneAndUpdate({ id: input.id }, {
|
|
6230
|
+
$set: { updatedAt: now },
|
|
6231
|
+
$unset: {
|
|
6232
|
+
deletedAt: "",
|
|
6233
|
+
deletedBy: ""
|
|
6234
|
+
},
|
|
6235
|
+
$inc: { version: 1 }
|
|
6236
|
+
}, {
|
|
6237
|
+
returnDocument: "after",
|
|
6238
|
+
projection: { _id: 0 },
|
|
6239
|
+
session
|
|
6240
|
+
});
|
|
6241
|
+
await this.#log(session, {
|
|
6242
|
+
action: "note-restored",
|
|
6243
|
+
scopeId: String(note.pageScopeId),
|
|
6244
|
+
pageId: String(note.pageId),
|
|
6245
|
+
noteId: input.id,
|
|
6246
|
+
provenance: input.provenance,
|
|
6247
|
+
at: now
|
|
6248
|
+
});
|
|
6249
|
+
return toNote(updated);
|
|
6250
|
+
});
|
|
6251
|
+
}
|
|
6252
|
+
async searchPages(input) {
|
|
6253
|
+
const core = await loadPagesCore();
|
|
6254
|
+
const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
6255
|
+
const words = knowledgeSearchWords(input.query);
|
|
6256
|
+
if (!words.length) return [];
|
|
6257
|
+
const phrase = input.query.trim().toLowerCase().replace(/\s+/g, " ");
|
|
6258
|
+
const ids = await this.#readable(input.read);
|
|
6259
|
+
const scopes = input.scopeIds?.length ? input.scopeIds : void 0;
|
|
6260
|
+
const inScope = (field) => {
|
|
6261
|
+
if (ids === null) return scopes ? { [field]: { $in: scopes } } : {};
|
|
6262
|
+
const allowed = scopes ? scopes.filter((id) => ids.includes(id)) : ids;
|
|
6263
|
+
return { [field]: { $in: allowed } };
|
|
6264
|
+
};
|
|
6265
|
+
const matcher = (word) => ({
|
|
6266
|
+
$regex: escapeRegex(word),
|
|
6267
|
+
$options: "i"
|
|
6268
|
+
});
|
|
6269
|
+
const pages = await this.#c("pages");
|
|
6270
|
+
const pageFilter = {
|
|
6271
|
+
...live,
|
|
6272
|
+
...notScope,
|
|
6273
|
+
...inScope("scopeId"),
|
|
6274
|
+
$and: words.map((word) => ({ $or: [
|
|
6275
|
+
{ name: matcher(word) },
|
|
6276
|
+
{ description: matcher(word) },
|
|
6277
|
+
{ body: matcher(word) }
|
|
6278
|
+
] }))
|
|
6279
|
+
};
|
|
6280
|
+
const hits = (await pages.aggregate([
|
|
6281
|
+
{ $match: pageFilter },
|
|
6282
|
+
{ $addFields: { searchTier: { $cond: [
|
|
6283
|
+
{ $regexMatch: {
|
|
6284
|
+
input: "$canonicalName",
|
|
6285
|
+
regex: escapeRegex(phrase)
|
|
6286
|
+
} },
|
|
6287
|
+
0,
|
|
6288
|
+
{ $cond: [
|
|
6289
|
+
{ $and: words.map((word) => ({ $regexMatch: {
|
|
6290
|
+
input: "$name",
|
|
6291
|
+
regex: escapeRegex(word),
|
|
6292
|
+
options: "i"
|
|
6293
|
+
} })) },
|
|
6294
|
+
1,
|
|
6295
|
+
2
|
|
6296
|
+
] }
|
|
6297
|
+
] } } },
|
|
6298
|
+
{ $sort: {
|
|
6299
|
+
searchTier: 1,
|
|
6300
|
+
updatedAt: -1,
|
|
6301
|
+
id: 1
|
|
6302
|
+
} },
|
|
6303
|
+
{ $limit: limit },
|
|
6304
|
+
{ $project: {
|
|
6305
|
+
searchTier: 0,
|
|
6306
|
+
_id: 0
|
|
6307
|
+
} }
|
|
6308
|
+
]).toArray()).map((row) => ({
|
|
6309
|
+
type: "page",
|
|
6310
|
+
page: toPage(row)
|
|
5280
6311
|
}));
|
|
5281
|
-
if (
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
6312
|
+
if (hits.length >= limit) return hits;
|
|
6313
|
+
const noteRows = await (await this.#c("notes")).find({
|
|
6314
|
+
...live,
|
|
6315
|
+
...this.#visibleNotes(ids),
|
|
6316
|
+
...scopes ? { pageScopeId: { $in: ids === null ? scopes : scopes.filter((id) => ids.includes(id)) } } : {},
|
|
6317
|
+
$and: words.map((word) => ({ text: matcher(word) }))
|
|
6318
|
+
}, noProjection).sort({
|
|
6319
|
+
updatedAt: -1,
|
|
6320
|
+
id: -1
|
|
6321
|
+
}).limit(limit - hits.length).toArray();
|
|
6322
|
+
const notePages = new Map((await pages.find({
|
|
6323
|
+
id: { $in: [...new Set(noteRows.map((row) => String(row.pageId)))] },
|
|
6324
|
+
...live
|
|
6325
|
+
}, noProjection).toArray()).map((row) => [String(row.id), toPage(row)]));
|
|
6326
|
+
for (const row of noteRows) {
|
|
6327
|
+
const page = notePages.get(String(row.pageId));
|
|
6328
|
+
if (page) hits.push({
|
|
6329
|
+
type: "note",
|
|
6330
|
+
note: toNote(row),
|
|
6331
|
+
page
|
|
6332
|
+
});
|
|
5299
6333
|
}
|
|
5300
|
-
return
|
|
5301
|
-
}
|
|
5302
|
-
/**
|
|
5303
|
-
* @deprecated Curation cursors were removed. Observation-time curate is the only Knowledge writer and needs no
|
|
5304
|
-
* cursor. Always throws.
|
|
5305
|
-
*/
|
|
5306
|
-
async getCurationCursor(_input) {
|
|
5307
|
-
throw new Error(KNOWLEDGE_CURATION_CURSOR_REMOVED_MESSAGE);
|
|
6334
|
+
return hits;
|
|
5308
6335
|
}
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
}
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
6336
|
+
async listPageActivity(input) {
|
|
6337
|
+
const core = await loadPagesCore();
|
|
6338
|
+
const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
6339
|
+
const ids = await this.#readable(input.read);
|
|
6340
|
+
const activity = await this.#c("activity");
|
|
6341
|
+
const notes = await this.#c("notes");
|
|
6342
|
+
const filter = {};
|
|
6343
|
+
if (ids !== null) filter.pageScopeId = { $in: ids };
|
|
6344
|
+
delete filter.scopeId;
|
|
6345
|
+
if (input.scopeId) {
|
|
6346
|
+
if (ids !== null && !ids.includes(input.scopeId)) filter.pageScopeId = { $in: [] };
|
|
6347
|
+
else filter.pageScopeId = input.scopeId;
|
|
6348
|
+
}
|
|
6349
|
+
if (input.pageId) filter.pageId = input.pageId;
|
|
6350
|
+
if (input.cursor) {
|
|
6351
|
+
const [createdAt, id] = decodeCursor(input.cursor);
|
|
6352
|
+
const at = new Date(createdAt);
|
|
6353
|
+
filter.$or = [{ createdAt: { $lt: at } }, {
|
|
6354
|
+
createdAt: at,
|
|
6355
|
+
id: { $lt: id }
|
|
6356
|
+
}];
|
|
6357
|
+
}
|
|
6358
|
+
const events = [];
|
|
6359
|
+
let cursorFilter = filter;
|
|
6360
|
+
for (;;) {
|
|
6361
|
+
const batch = await activity.find(cursorFilter, noProjection).sort({
|
|
6362
|
+
createdAt: -1,
|
|
6363
|
+
id: -1
|
|
6364
|
+
}).limit(limit * 4 + 1).toArray();
|
|
6365
|
+
const noteIds = [...new Set(batch.flatMap((row) => row.noteId ? [String(row.noteId)] : []))];
|
|
6366
|
+
const visible = new Set(noteIds.length ? (await notes.find({
|
|
6367
|
+
id: { $in: noteIds },
|
|
6368
|
+
...this.#visibleNotes(ids)
|
|
6369
|
+
}, { projection: {
|
|
6370
|
+
_id: 0,
|
|
6371
|
+
id: 1
|
|
6372
|
+
} }).toArray()).map((row) => String(row.id)) : []);
|
|
6373
|
+
for (const row of batch) {
|
|
6374
|
+
if (row.noteId && !visible.has(String(row.noteId))) continue;
|
|
6375
|
+
events.push(row);
|
|
6376
|
+
if (events.length > limit) break;
|
|
6377
|
+
}
|
|
6378
|
+
if (events.length > limit || batch.length <= limit * 4) break;
|
|
6379
|
+
const last = batch[batch.length - 1];
|
|
6380
|
+
cursorFilter = {
|
|
6381
|
+
...filter,
|
|
6382
|
+
$or: [{ createdAt: { $lt: last.createdAt } }, {
|
|
6383
|
+
createdAt: last.createdAt,
|
|
6384
|
+
id: { $lt: last.id }
|
|
6385
|
+
}]
|
|
6386
|
+
};
|
|
6387
|
+
}
|
|
6388
|
+
const page = events.slice(0, limit).map((row) => ({
|
|
6389
|
+
id: String(row.id),
|
|
6390
|
+
action: String(row.action),
|
|
6391
|
+
scopeId: String(row.scopeId),
|
|
6392
|
+
pageId: String(row.pageId),
|
|
6393
|
+
...row.noteId == null ? {} : { noteId: String(row.noteId) },
|
|
6394
|
+
provenance: row.provenance,
|
|
5328
6395
|
createdAt: new Date(row.createdAt)
|
|
5329
6396
|
}));
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
...
|
|
5334
|
-
...input.scope ? { scopeKey: { $in: visibleScopeKeys(input.scope) } } : {}
|
|
6397
|
+
const last = page[page.length - 1];
|
|
6398
|
+
return {
|
|
6399
|
+
events: page,
|
|
6400
|
+
...events.length > limit && last ? { nextCursor: encodeCursor([last.createdAt.toISOString(), last.id]) } : {}
|
|
5335
6401
|
};
|
|
5336
|
-
return (await (await this.#outboxCollection()).find(filter).sort({
|
|
5337
|
-
createdAt: 1,
|
|
5338
|
-
id: 1
|
|
5339
|
-
}).limit(input.limit ?? 100).toArray()).map(outboxFromDocument);
|
|
5340
6402
|
}
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
const
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
6403
|
+
/** Deletes every document but keeps the collections, indexes and schema-version marker. */
|
|
6404
|
+
async clearAll() {
|
|
6405
|
+
await this.#tx(async (session) => {
|
|
6406
|
+
for (const name of [
|
|
6407
|
+
"activity",
|
|
6408
|
+
"pins",
|
|
6409
|
+
"notes",
|
|
6410
|
+
"pages"
|
|
6411
|
+
]) await (await this.#c(name)).deleteMany({}, { session });
|
|
6412
|
+
});
|
|
6413
|
+
}
|
|
6414
|
+
async addPinAudience(input) {
|
|
6415
|
+
return this.#tx(async (session) => {
|
|
6416
|
+
const page = await this.#requireLivePage(input.pageId, session);
|
|
6417
|
+
const now = /* @__PURE__ */ new Date();
|
|
6418
|
+
const { pin, changed } = await this.#upsertPinAudience(session, page.id, input.audience, input.createdBy, now);
|
|
6419
|
+
if (changed) await this.#log(session, {
|
|
6420
|
+
action: "pin-added",
|
|
6421
|
+
scopeId: page.scopeId,
|
|
6422
|
+
pageId: page.id,
|
|
6423
|
+
provenance: { author: input.createdBy },
|
|
6424
|
+
at: now
|
|
6425
|
+
});
|
|
6426
|
+
return pin;
|
|
6427
|
+
});
|
|
6428
|
+
}
|
|
6429
|
+
async #upsertPinAudience(session, pageId, audience, createdBy, now) {
|
|
6430
|
+
const pins = await this.#c("pins");
|
|
6431
|
+
const existing = await pins.findOne({ pageId }, {
|
|
6432
|
+
...noProjection,
|
|
6433
|
+
session
|
|
6434
|
+
});
|
|
6435
|
+
if (!existing) {
|
|
6436
|
+
const doc = {
|
|
6437
|
+
id: createKnowledgeUlid(),
|
|
6438
|
+
pageId,
|
|
6439
|
+
audience: [...new Set(audience)],
|
|
6440
|
+
createdBy,
|
|
6441
|
+
createdAt: now,
|
|
6442
|
+
updatedAt: now
|
|
5355
6443
|
};
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
6444
|
+
try {
|
|
6445
|
+
await pins.insertOne({ ...doc }, { session });
|
|
6446
|
+
} catch (error) {
|
|
6447
|
+
if (isDuplicateKey(error)) throw new KnowledgeConflictError(pageId);
|
|
6448
|
+
throw error;
|
|
6449
|
+
}
|
|
6450
|
+
return {
|
|
6451
|
+
pin: toPin(doc),
|
|
6452
|
+
changed: true
|
|
6453
|
+
};
|
|
6454
|
+
}
|
|
6455
|
+
const current = existing.audience ?? [];
|
|
6456
|
+
const merged = [.../* @__PURE__ */ new Set([...current, ...audience])];
|
|
6457
|
+
if (merged.length === current.length) return {
|
|
6458
|
+
pin: toPin(existing),
|
|
6459
|
+
changed: false
|
|
6460
|
+
};
|
|
6461
|
+
return {
|
|
6462
|
+
pin: toPin(await pins.findOneAndUpdate({ id: existing.id }, { $set: {
|
|
6463
|
+
audience: merged,
|
|
6464
|
+
updatedAt: now
|
|
6465
|
+
} }, {
|
|
6466
|
+
returnDocument: "after",
|
|
6467
|
+
projection: { _id: 0 },
|
|
6468
|
+
session
|
|
6469
|
+
})),
|
|
6470
|
+
changed: true
|
|
6471
|
+
};
|
|
6472
|
+
}
|
|
6473
|
+
async removePinAudience(input) {
|
|
6474
|
+
return this.#tx(async (session) => {
|
|
6475
|
+
const pins = await this.#c("pins");
|
|
6476
|
+
const existing = await pins.findOne({ pageId: input.pageId }, {
|
|
6477
|
+
...noProjection,
|
|
6478
|
+
session
|
|
6479
|
+
});
|
|
6480
|
+
if (!existing) return null;
|
|
6481
|
+
const current = existing.audience ?? [];
|
|
6482
|
+
const remaining = current.filter((entry) => !input.audience.includes(entry));
|
|
6483
|
+
if (remaining.length === current.length) return toPin(existing);
|
|
6484
|
+
const now = /* @__PURE__ */ new Date();
|
|
6485
|
+
let pin = null;
|
|
6486
|
+
if (remaining.length === 0) await pins.deleteOne({ id: existing.id }, { session });
|
|
6487
|
+
else pin = toPin(await pins.findOneAndUpdate({ id: existing.id }, { $set: {
|
|
6488
|
+
audience: remaining,
|
|
6489
|
+
updatedAt: now
|
|
6490
|
+
} }, {
|
|
6491
|
+
returnDocument: "after",
|
|
6492
|
+
projection: { _id: 0 },
|
|
6493
|
+
session
|
|
6494
|
+
}));
|
|
6495
|
+
const page = await this.#page(input.pageId, session);
|
|
6496
|
+
await this.#log(session, {
|
|
6497
|
+
action: "pin-removed",
|
|
6498
|
+
scopeId: page?.scopeId ?? input.pageId,
|
|
6499
|
+
pageId: input.pageId,
|
|
6500
|
+
provenance: { author: input.removedBy },
|
|
6501
|
+
at: now
|
|
6502
|
+
});
|
|
6503
|
+
return pin;
|
|
6504
|
+
});
|
|
6505
|
+
}
|
|
6506
|
+
async getPages(input) {
|
|
6507
|
+
if (!input.ids.length) return [];
|
|
6508
|
+
const rows = await (await this.#c("pages")).find({
|
|
6509
|
+
id: { $in: [...new Set(input.ids)] },
|
|
6510
|
+
...live
|
|
6511
|
+
}, noProjection).toArray();
|
|
6512
|
+
if (input.read === "all") return rows.map(toPage);
|
|
6513
|
+
const read = input.read;
|
|
6514
|
+
const scopeIds = [...new Set(rows.map((row) => String(row.scopeId)))];
|
|
6515
|
+
const readable = /* @__PURE__ */ new Set();
|
|
6516
|
+
const any = this.#readableClauses(read);
|
|
6517
|
+
if (scopeIds.length && any.length) {
|
|
6518
|
+
const scopes = await (await this.#c("pages")).find({
|
|
6519
|
+
id: { $in: scopeIds },
|
|
6520
|
+
isScope: true,
|
|
6521
|
+
$or: any
|
|
6522
|
+
}, { projection: {
|
|
6523
|
+
_id: 0,
|
|
5359
6524
|
id: 1
|
|
5360
|
-
}
|
|
5361
|
-
const
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
6525
|
+
} }).toArray();
|
|
6526
|
+
for (const scope of scopes) readable.add(String(scope.id));
|
|
6527
|
+
}
|
|
6528
|
+
return rows.filter((row) => readable.has(String(row.scopeId))).map(toPage);
|
|
6529
|
+
}
|
|
6530
|
+
async listPins(input) {
|
|
6531
|
+
const core = await loadPagesCore();
|
|
6532
|
+
const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
6533
|
+
const ids = await this.#readable(input.read);
|
|
6534
|
+
const match = {};
|
|
6535
|
+
if (input.audience !== "all") match.audience = { $in: input.audience };
|
|
6536
|
+
const pageId = {};
|
|
6537
|
+
if (input.pageIds) pageId.$in = input.pageIds;
|
|
6538
|
+
if (input.excludePageIds?.length) pageId.$nin = input.excludePageIds;
|
|
6539
|
+
if (Object.keys(pageId).length) match.pageId = pageId;
|
|
6540
|
+
const pins = await this.#c("pins");
|
|
6541
|
+
const base = [
|
|
6542
|
+
{ $match: match },
|
|
6543
|
+
{ $lookup: {
|
|
6544
|
+
from: MONGO_KNOWLEDGE_PAGES_COLLECTIONS.pages,
|
|
6545
|
+
localField: "pageId",
|
|
6546
|
+
foreignField: "id",
|
|
6547
|
+
as: "page"
|
|
6548
|
+
} },
|
|
6549
|
+
{ $match: {
|
|
6550
|
+
"page.deletedAt": { $exists: false },
|
|
6551
|
+
...ids === null ? {} : { "page.scopeId": { $in: ids } },
|
|
6552
|
+
...input.withBody ? { "page.body": { $regex: "\\S" } } : {}
|
|
6553
|
+
} }
|
|
6554
|
+
];
|
|
6555
|
+
const pageMatch = [];
|
|
6556
|
+
if (input.cursor) {
|
|
6557
|
+
const [createdAt, id] = decodeCursor(input.cursor);
|
|
6558
|
+
const at = new Date(createdAt);
|
|
6559
|
+
pageMatch.push({ $match: { $or: [{ createdAt: { $lt: at } }, {
|
|
6560
|
+
createdAt: at,
|
|
6561
|
+
id: { $lt: id }
|
|
6562
|
+
}] } });
|
|
6563
|
+
}
|
|
6564
|
+
const rows = await pins.aggregate([
|
|
6565
|
+
...base,
|
|
6566
|
+
...pageMatch,
|
|
6567
|
+
{ $addFields: { pageName: { $arrayElemAt: ["$page.name", 0] } } },
|
|
6568
|
+
{ $project: {
|
|
6569
|
+
_id: 0,
|
|
6570
|
+
page: 0
|
|
6571
|
+
} },
|
|
6572
|
+
{ $sort: {
|
|
6573
|
+
createdAt: -1,
|
|
6574
|
+
id: -1
|
|
6575
|
+
} },
|
|
6576
|
+
{ $limit: limit + 1 }
|
|
6577
|
+
]).toArray();
|
|
6578
|
+
let total;
|
|
6579
|
+
if (input.count) {
|
|
6580
|
+
const counted = await pins.aggregate([...base, { $count: "total" }]).toArray();
|
|
6581
|
+
total = Number(counted[0]?.total ?? 0);
|
|
6582
|
+
}
|
|
6583
|
+
const listed = rows.slice(0, limit).map(toListedPin);
|
|
6584
|
+
const last = listed[listed.length - 1];
|
|
6585
|
+
return {
|
|
6586
|
+
pins: listed,
|
|
6587
|
+
...rows.length > limit && last ? { nextCursor: encodeCursor([last.createdAt.toISOString(), last.id]) } : {},
|
|
6588
|
+
...total === void 0 ? {} : { total }
|
|
6589
|
+
};
|
|
6590
|
+
}
|
|
6591
|
+
};
|
|
6592
|
+
// src/storage/domains/knowledge/index.ts
|
|
6593
|
+
const loadKnowledgePagesCore = createKnowledgePagesCoreLoader(coreFeatures, () => import("@mastra/core/storage"));
|
|
6594
|
+
/** @experimental Knowledge APIs are experimental and may change without notice. */
|
|
6595
|
+
var KnowledgeMongoDB = class extends KnowledgeStorage {
|
|
6596
|
+
static MANAGED_COLLECTIONS = Object.values(MONGO_KNOWLEDGE_PAGES_COLLECTIONS);
|
|
6597
|
+
#connector;
|
|
6598
|
+
#pages;
|
|
6599
|
+
constructor(config) {
|
|
6600
|
+
super();
|
|
6601
|
+
this.#connector = resolveMongoDBConfig(config);
|
|
6602
|
+
this.#pages = new MongoKnowledgePages({
|
|
6603
|
+
connector: this.#connector,
|
|
6604
|
+
bounds: async () => {
|
|
6605
|
+
const core = await loadKnowledgePagesCore();
|
|
6606
|
+
return {
|
|
6607
|
+
assertKnowledgePageDescriptionWithinBound: core.assertKnowledgePageDescriptionWithinBound,
|
|
6608
|
+
assertKnowledgeNoteTextWithinBound: core.assertKnowledgeNoteTextWithinBound
|
|
6609
|
+
};
|
|
5393
6610
|
}
|
|
5394
|
-
return claimed.map(outboxFromDocument);
|
|
5395
6611
|
});
|
|
5396
6612
|
}
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
}
|
|
5404
|
-
status: "completed",
|
|
5405
|
-
completedAt: /* @__PURE__ */ new Date(),
|
|
5406
|
-
claimedAt: null,
|
|
5407
|
-
claimedBy: null
|
|
5408
|
-
} });
|
|
5409
|
-
}
|
|
5410
|
-
async releaseSemanticOutbox(input) {
|
|
5411
|
-
if (!input.ids.length) return;
|
|
5412
|
-
await (await this.#outboxCollection()).updateMany({
|
|
5413
|
-
id: { $in: input.ids },
|
|
5414
|
-
status: "processing",
|
|
5415
|
-
claimedBy: input.workerId
|
|
5416
|
-
}, { $set: {
|
|
5417
|
-
status: "pending",
|
|
5418
|
-
availableAt: input.retryAt ?? /* @__PURE__ */ new Date(),
|
|
5419
|
-
claimedAt: null,
|
|
5420
|
-
claimedBy: null
|
|
5421
|
-
} });
|
|
6613
|
+
getCapabilities() {
|
|
6614
|
+
return {
|
|
6615
|
+
...super.getCapabilities(),
|
|
6616
|
+
schemaVersion: 4,
|
|
6617
|
+
supportsSchemaInspection: true,
|
|
6618
|
+
supportsExplicitReset: true
|
|
6619
|
+
};
|
|
5422
6620
|
}
|
|
5423
|
-
async
|
|
5424
|
-
|
|
6621
|
+
async init() {
|
|
6622
|
+
await this.#pages.init();
|
|
5425
6623
|
}
|
|
5426
|
-
|
|
5427
|
-
return this.#
|
|
6624
|
+
async inspectSchema() {
|
|
6625
|
+
return this.#pages.inspectSchema();
|
|
5428
6626
|
}
|
|
5429
|
-
|
|
5430
|
-
|
|
6627
|
+
async dangerouslyReset() {
|
|
6628
|
+
await this.#pages.reset();
|
|
5431
6629
|
}
|
|
5432
|
-
|
|
5433
|
-
|
|
6630
|
+
async dangerouslyClearAll() {
|
|
6631
|
+
await this.#pages.clearAll();
|
|
5434
6632
|
}
|
|
5435
|
-
|
|
5436
|
-
return this.#
|
|
6633
|
+
materializeScope(input) {
|
|
6634
|
+
return this.#pages.materializeScope(input);
|
|
5437
6635
|
}
|
|
5438
|
-
|
|
5439
|
-
return this.#
|
|
6636
|
+
getScopesByAddress(input) {
|
|
6637
|
+
return this.#pages.getScopesByAddress(input);
|
|
5440
6638
|
}
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
id,
|
|
5444
|
-
type: "node"
|
|
5445
|
-
}, sessionOptions(session));
|
|
5446
|
-
return row ? nodeFromDocument(row) : null;
|
|
5447
|
-
}
|
|
5448
|
-
async #getNodeByName(name, scope, session) {
|
|
5449
|
-
const row = await (await this.#nodes()).findOne({
|
|
5450
|
-
type: "node",
|
|
5451
|
-
scopeKey: knowledgeScopeKey(scope),
|
|
5452
|
-
canonicalName: canonicalName(name)
|
|
5453
|
-
}, sessionOptions(session));
|
|
5454
|
-
return row ? nodeFromDocument(row) : null;
|
|
5455
|
-
}
|
|
5456
|
-
async #resolveNode(name, scope, session) {
|
|
5457
|
-
const candidates = (await (await this.#nodes()).find({
|
|
5458
|
-
type: "node",
|
|
5459
|
-
canonicalName: canonicalName(name),
|
|
5460
|
-
$or: [{ scopeKey: { $in: visibleScopeKeys(scope) } }, { mergedInto: { $ne: null } }]
|
|
5461
|
-
}, sessionOptions(session)).toArray()).map(nodeFromDocument).sort((left, right) => right.scope.length - left.scope.length);
|
|
5462
|
-
for (const candidate of candidates) {
|
|
5463
|
-
const terminal = await this.#resolveTerminalNode(candidate.id, session);
|
|
5464
|
-
if (terminal && isKnowledgeScopeVisible(terminal.scope, scope)) return terminal;
|
|
5465
|
-
}
|
|
5466
|
-
return null;
|
|
5467
|
-
}
|
|
5468
|
-
async #resolveTerminalNode(id, session) {
|
|
5469
|
-
let node = await this.#getNode(id, session);
|
|
5470
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5471
|
-
while (node?.mergedInto) {
|
|
5472
|
-
if (seen.has(node.id)) throw new Error(`Knowledge merge cycle detected at ${node.id}`);
|
|
5473
|
-
seen.add(node.id);
|
|
5474
|
-
node = await this.#getNode(node.mergedInto, session);
|
|
5475
|
-
}
|
|
5476
|
-
return node;
|
|
6639
|
+
listScopeDeclarations() {
|
|
6640
|
+
return this.#pages.listScopeDeclarations();
|
|
5477
6641
|
}
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
type: "page",
|
|
5481
|
-
scopeKey: knowledgeScopeKey(scope),
|
|
5482
|
-
canonicalName: canonicalName(name)
|
|
5483
|
-
}, sessionOptions(session));
|
|
5484
|
-
return row ? nodeFromDocument(row) : null;
|
|
6642
|
+
renameScopeDeclaration(input) {
|
|
6643
|
+
return this.#pages.renameScopeDeclaration(input);
|
|
5485
6644
|
}
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
id,
|
|
5489
|
-
...includeDeleted ? {} : { deletedAt: null }
|
|
5490
|
-
}, sessionOptions(session));
|
|
5491
|
-
return row ? recordFromDocument(row) : null;
|
|
5492
|
-
}
|
|
5493
|
-
async #queryKnowledge(input, relationship) {
|
|
5494
|
-
const scope = canonicalizeKnowledgeScope(input.scope);
|
|
5495
|
-
const node = await this.#resolveTerminalNode(nodeReferenceId(input.node));
|
|
5496
|
-
if (!node) return { records: [] };
|
|
5497
|
-
const nodeIds = [node.id];
|
|
5498
|
-
if (relationship !== "about") {
|
|
5499
|
-
const mentions = await (await this.#mentions()).find({
|
|
5500
|
-
recordId: node.id,
|
|
5501
|
-
sourceType: "record"
|
|
5502
|
-
}).toArray();
|
|
5503
|
-
nodeIds.push(...mentions.map((row) => row.sourceId));
|
|
5504
|
-
}
|
|
5505
|
-
const limit = input.limit ?? 100;
|
|
5506
|
-
const filter = relationship === "about" ? { node: node.id } : relationship === "mentioning" ? { id: { $in: nodeIds.slice(1) } } : { $or: [{ node: node.id }, { id: { $in: nodeIds.slice(1) } }] };
|
|
5507
|
-
Object.assign(filter, {
|
|
5508
|
-
scopeKey: { $in: visibleScopeKeys(scope) },
|
|
5509
|
-
...input.includeDeleted ? {} : { deletedAt: null },
|
|
5510
|
-
...input.after ? { id: { $lt: input.after } } : {}
|
|
5511
|
-
});
|
|
5512
|
-
const rows = await (await this.#knowledge()).find(filter).sort({ id: -1 }).limit(limit + 1).toArray();
|
|
5513
|
-
return {
|
|
5514
|
-
records: rows.slice(0, limit).map(recordFromDocument),
|
|
5515
|
-
nextCursor: rows.length > limit ? rows[limit - 1]?.id : void 0
|
|
5516
|
-
};
|
|
6645
|
+
listChildScopes(input) {
|
|
6646
|
+
return this.#pages.listChildScopes(input);
|
|
5517
6647
|
}
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
await mentions.deleteMany({
|
|
5521
|
-
sourceType,
|
|
5522
|
-
sourceId
|
|
5523
|
-
}, sessionOptions(session));
|
|
5524
|
-
for (const name of parseKnowledgeWikilinks(text)) {
|
|
5525
|
-
let node = await this.#resolveNode(name, resolutionScope, session);
|
|
5526
|
-
if (!node) {
|
|
5527
|
-
const existing = await this.#getNodeByName(name, defaultScope, session);
|
|
5528
|
-
node = existing ? await this.#resolveTerminalNode(existing.id, session) : null;
|
|
5529
|
-
}
|
|
5530
|
-
if (!node) {
|
|
5531
|
-
const now = /* @__PURE__ */ new Date();
|
|
5532
|
-
node = {
|
|
5533
|
-
id: crypto.randomUUID(),
|
|
5534
|
-
type: "node",
|
|
5535
|
-
name,
|
|
5536
|
-
kind: "node",
|
|
5537
|
-
scope: defaultScope,
|
|
5538
|
-
version: 1,
|
|
5539
|
-
createdAt: now,
|
|
5540
|
-
updatedAt: now
|
|
5541
|
-
};
|
|
5542
|
-
try {
|
|
5543
|
-
await (await this.#nodes()).insertOne({
|
|
5544
|
-
...node,
|
|
5545
|
-
canonicalName: canonicalName(name),
|
|
5546
|
-
scopeKey: knowledgeScopeKey(defaultScope),
|
|
5547
|
-
mergedInto: null
|
|
5548
|
-
}, sessionOptions(session));
|
|
5549
|
-
await this.#activity("node-created", "node", node.id, defaultScope, void 0, session);
|
|
5550
|
-
await this.#outbox("node", node.id, "upsert", 1, defaultScope, session);
|
|
5551
|
-
} catch (error) {
|
|
5552
|
-
if (error.code !== 11e3) throw error;
|
|
5553
|
-
node = await this.#getNodeByName(name, defaultScope, session);
|
|
5554
|
-
if (!node) throw error;
|
|
5555
|
-
}
|
|
5556
|
-
}
|
|
5557
|
-
await mentions.updateOne({
|
|
5558
|
-
sourceType,
|
|
5559
|
-
sourceId,
|
|
5560
|
-
recordId: node.id
|
|
5561
|
-
}, { $setOnInsert: {
|
|
5562
|
-
sourceType,
|
|
5563
|
-
sourceId,
|
|
5564
|
-
recordId: node.id
|
|
5565
|
-
} }, {
|
|
5566
|
-
...sessionOptions(session),
|
|
5567
|
-
upsert: true
|
|
5568
|
-
});
|
|
5569
|
-
}
|
|
6648
|
+
getPages(input) {
|
|
6649
|
+
return this.#pages.getPages(input);
|
|
5570
6650
|
}
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
}
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
6651
|
+
getPage(input) {
|
|
6652
|
+
return this.#pages.getPage(input);
|
|
6653
|
+
}
|
|
6654
|
+
getPageByPath(input) {
|
|
6655
|
+
return this.#pages.getPageByPath(input);
|
|
6656
|
+
}
|
|
6657
|
+
findPagesByName(input) {
|
|
6658
|
+
return this.#pages.findPagesByName(input);
|
|
6659
|
+
}
|
|
6660
|
+
listChildPages(input) {
|
|
6661
|
+
return this.#pages.listChildPages(input);
|
|
6662
|
+
}
|
|
6663
|
+
listPages(input) {
|
|
6664
|
+
return this.#pages.listPages(input);
|
|
6665
|
+
}
|
|
6666
|
+
createPage(input) {
|
|
6667
|
+
return this.#pages.createPage(input);
|
|
6668
|
+
}
|
|
6669
|
+
updatePage(input) {
|
|
6670
|
+
return this.#pages.updatePage(input);
|
|
6671
|
+
}
|
|
6672
|
+
movePage(input) {
|
|
6673
|
+
return this.#pages.movePage(input);
|
|
6674
|
+
}
|
|
6675
|
+
mergePages(input) {
|
|
6676
|
+
return this.#pages.mergePages(input);
|
|
6677
|
+
}
|
|
6678
|
+
deletePage(input) {
|
|
6679
|
+
return this.#pages.deletePage(input);
|
|
6680
|
+
}
|
|
6681
|
+
getNote(input) {
|
|
6682
|
+
return this.#pages.getNote(input);
|
|
6683
|
+
}
|
|
6684
|
+
listNotes(input) {
|
|
6685
|
+
return this.#pages.listNotes(input);
|
|
6686
|
+
}
|
|
6687
|
+
countNotes(input) {
|
|
6688
|
+
return this.#pages.countNotes(input);
|
|
6689
|
+
}
|
|
6690
|
+
listNotesMentioning(input) {
|
|
6691
|
+
return this.#pages.listNotesMentioning(input);
|
|
6692
|
+
}
|
|
6693
|
+
addNote(input) {
|
|
6694
|
+
return this.#pages.addNote(input);
|
|
6695
|
+
}
|
|
6696
|
+
restampNote(input) {
|
|
6697
|
+
return this.#pages.restampNote(input);
|
|
6698
|
+
}
|
|
6699
|
+
removeNote(input) {
|
|
6700
|
+
return this.#pages.removeNote(input);
|
|
6701
|
+
}
|
|
6702
|
+
restoreNote(input) {
|
|
6703
|
+
return this.#pages.restoreNote(input);
|
|
6704
|
+
}
|
|
6705
|
+
searchPages(input) {
|
|
6706
|
+
return this.#pages.searchPages(input);
|
|
6707
|
+
}
|
|
6708
|
+
listPageActivity(input) {
|
|
6709
|
+
return this.#pages.listPageActivity(input);
|
|
6710
|
+
}
|
|
6711
|
+
addPinAudience(input) {
|
|
6712
|
+
return this.#pages.addPinAudience(input);
|
|
6713
|
+
}
|
|
6714
|
+
removePinAudience(input) {
|
|
6715
|
+
return this.#pages.removePinAudience(input);
|
|
5607
6716
|
}
|
|
5608
|
-
|
|
5609
|
-
return
|
|
6717
|
+
listPins(input) {
|
|
6718
|
+
return this.#pages.listPins(input);
|
|
5610
6719
|
}
|
|
5611
6720
|
};
|
|
5612
6721
|
// src/storage/domains/mcp-clients/index.ts
|