@mastra/mysql 0.15.0 → 0.16.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 +1521 -774
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1522 -775
- package/dist/index.js.map +1 -1
- package/dist/storage/domains/knowledge/index.d.ts +39 -77
- package/dist/storage/domains/knowledge/index.d.ts.map +1 -1
- package/dist/storage/domains/knowledge/pages.d.ts +167 -0
- package/dist/storage/domains/knowledge/pages.d.ts.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
|
|
2
|
-
import { AGENTS_SCHEMA, AGENT_VERSIONS_SCHEMA, AgentsStorage, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, ExperimentsStorage, FAVORITES_SCHEMA, FavoritesStorage,
|
|
2
|
+
import { AGENTS_SCHEMA, AGENT_VERSIONS_SCHEMA, AgentsStorage, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, ExperimentsStorage, FAVORITES_SCHEMA, FavoritesStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeSchemaResetRequiredError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MCP_CLIENTS_SCHEMA, MCP_CLIENT_VERSIONS_SCHEMA, MCP_SERVERS_SCHEMA, MCP_SERVER_VERSIONS_SCHEMA, MastraCompositeStore, MemoryStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PROMPT_BLOCKS_SCHEMA, PROMPT_BLOCK_VERSIONS_SCHEMA, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, SCORERS_SCHEMA, SCORER_DEFINITIONS_SCHEMA, SCORER_DEFINITION_VERSIONS_SCHEMA, SKILLS_SCHEMA, SKILL_BLOBS_SCHEMA, SKILL_VERSIONS_SCHEMA, SPAN_SCHEMA, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, StoreOperations, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_CHANNEL_CONFIG, TABLE_CHANNEL_INSTALLATIONS, TABLE_CONFIGS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TOOL_PROVIDER_CONNECTIONS_SCHEMA, ToolProviderConnectionsStorage, TraceStatus, WORKFLOW_DEFINITIONS_SCHEMA, WORKSPACES_SCHEMA, WORKSPACE_VERSIONS_SCHEMA, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, createKnowledgeUlid, createStorageErrorId, executeRetentionPrune, hasErrorCode, isRunFenceConflictError, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, normalizePerPage, normalizeScheduleTarget, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, toTraceSpans, validateStorageMetadataFilter } from "@mastra/core/storage";
|
|
3
3
|
import { createPool } from "mysql2/promise";
|
|
4
4
|
import { parseSqlIdentifier } from "@mastra/core/utils";
|
|
5
|
+
import { coreFeatures } from "@mastra/core/features";
|
|
5
6
|
import { createHash } from "crypto";
|
|
6
7
|
import { MessageList } from "@mastra/core/agent";
|
|
7
8
|
import { skillSnapshotFieldValuesEqual } from "@mastra/core/storage/domains/skills";
|
|
@@ -1375,7 +1376,7 @@ function serializeJson(v) {
|
|
|
1375
1376
|
if (typeof v === "object" && v != null) return JSON.stringify(v);
|
|
1376
1377
|
return v ?? null;
|
|
1377
1378
|
}
|
|
1378
|
-
function parseJson$
|
|
1379
|
+
function parseJson$1(val) {
|
|
1379
1380
|
if (val == null) return void 0;
|
|
1380
1381
|
if (typeof val === "string") try {
|
|
1381
1382
|
return JSON.parse(val);
|
|
@@ -1390,14 +1391,14 @@ function rowToTask(row) {
|
|
|
1390
1391
|
status: String(row.status),
|
|
1391
1392
|
toolName: String(row.tool_name),
|
|
1392
1393
|
toolCallId: String(row.tool_call_id),
|
|
1393
|
-
args: parseJson$
|
|
1394
|
+
args: parseJson$1(row.args) ?? {},
|
|
1394
1395
|
agentId: String(row.agent_id),
|
|
1395
1396
|
threadId: row.thread_id != null ? String(row.thread_id) : void 0,
|
|
1396
1397
|
resourceId: row.resource_id != null ? String(row.resource_id) : void 0,
|
|
1397
1398
|
runId: String(row.run_id),
|
|
1398
|
-
result: parseJson$
|
|
1399
|
-
error: parseJson$
|
|
1400
|
-
suspendPayload: parseJson$
|
|
1399
|
+
result: parseJson$1(row.result),
|
|
1400
|
+
error: parseJson$1(row.error),
|
|
1401
|
+
suspendPayload: parseJson$1(row.suspend_payload),
|
|
1401
1402
|
retryCount: Number(row.retry_count),
|
|
1402
1403
|
maxRetries: Number(row.max_retries),
|
|
1403
1404
|
timeoutMs: Number(row.timeout_ms),
|
|
@@ -1408,7 +1409,7 @@ function rowToTask(row) {
|
|
|
1408
1409
|
ownerId: row.ownerId != null ? String(row.ownerId) : void 0,
|
|
1409
1410
|
leaseExpiresAt: parseDateTime(row.leaseExpiresAt),
|
|
1410
1411
|
expiresAt: parseDateTime(row.expiresAt),
|
|
1411
|
-
metadata: parseJson$
|
|
1412
|
+
metadata: parseJson$1(row.metadata)
|
|
1412
1413
|
};
|
|
1413
1414
|
}
|
|
1414
1415
|
var BackgroundTasksMySQL = class BackgroundTasksMySQL extends BackgroundTasksStorage {
|
|
@@ -4186,899 +4187,1645 @@ var FavoritesMySQL = class FavoritesMySQL extends FavoritesStorage {
|
|
|
4186
4187
|
}
|
|
4187
4188
|
}
|
|
4188
4189
|
};
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4190
|
+
const KNOWLEDGE_TABLE_NAMES = [
|
|
4191
|
+
"mastra_knowledge_nodes",
|
|
4192
|
+
"mastra_knowledge_records",
|
|
4193
|
+
"mastra_knowledge_mentions",
|
|
4194
|
+
"mastra_knowledge_activity",
|
|
4195
|
+
"mastra_knowledge_semantic_outbox",
|
|
4196
|
+
"mastra_knowledge_node_scopes",
|
|
4197
|
+
"mastra_knowledge_record_scopes",
|
|
4198
|
+
"mastra_knowledge_scope_grants",
|
|
4199
|
+
"mastra_knowledge_access_state",
|
|
4200
|
+
"mastra_knowledge_scope_addresses",
|
|
4201
|
+
"mastra_knowledge_node_addresses",
|
|
4202
|
+
"mastra_knowledge_import_state",
|
|
4203
|
+
"mastra_knowledge_import_runs",
|
|
4204
|
+
"mastra_knowledge_proposals"
|
|
4205
|
+
];
|
|
4206
|
+
/** Knowledge v1 tables with no v2 equivalent; an explicit schema reset drops them. */
|
|
4207
|
+
const RETIRED_KNOWLEDGE_TABLE_NAMES = ["mastra_knowledge_cursors"];
|
|
4208
|
+
const KNOWLEDGE_PAGES_CORE_FEATURE = "knowledge-pages";
|
|
4209
|
+
const TABLE_KNOWLEDGE_PAGES = "mastra_knowledge_pages";
|
|
4210
|
+
const TABLE_KNOWLEDGE_NOTES = "mastra_knowledge_notes";
|
|
4211
|
+
const TABLE_KNOWLEDGE_NOTE_STAMPS = "mastra_knowledge_note_stamps";
|
|
4212
|
+
/** Same name as the schema-2 table; schema 4 re-keys it to note → page. */
|
|
4213
|
+
const TABLE_KNOWLEDGE_PAGE_MENTIONS = "mastra_knowledge_mentions";
|
|
4214
|
+
const TABLE_KNOWLEDGE_PINS = "mastra_knowledge_pins";
|
|
4215
|
+
/** Same name as schema 2; schema 4 re-keys it to scope/page/note. */
|
|
4216
|
+
const TABLE_KNOWLEDGE_PAGE_ACTIVITY = "mastra_knowledge_activity";
|
|
4217
|
+
/** Holds `{ id: 'knowledge', schemaVersion: 4 }`, written when the schema-4 tables are created. */
|
|
4218
|
+
const TABLE_KNOWLEDGE_PAGES_STATE = "mastra_knowledge_access_state";
|
|
4219
|
+
const KNOWLEDGE_PAGES_TABLE_NAMES = [
|
|
4220
|
+
TABLE_KNOWLEDGE_PAGES,
|
|
4221
|
+
TABLE_KNOWLEDGE_NOTES,
|
|
4222
|
+
TABLE_KNOWLEDGE_NOTE_STAMPS,
|
|
4223
|
+
TABLE_KNOWLEDGE_PAGE_MENTIONS,
|
|
4224
|
+
TABLE_KNOWLEDGE_PINS,
|
|
4225
|
+
TABLE_KNOWLEDGE_PAGE_ACTIVITY,
|
|
4226
|
+
TABLE_KNOWLEDGE_PAGES_STATE
|
|
4227
|
+
];
|
|
4228
|
+
/** Every Knowledge table any schema version created. Reset drops these and nothing else. */
|
|
4229
|
+
const KNOWLEDGE_RESETTABLE_TABLE_NAMES = [.../* @__PURE__ */ new Set([
|
|
4230
|
+
...KNOWLEDGE_PAGES_TABLE_NAMES,
|
|
4231
|
+
...KNOWLEDGE_TABLE_NAMES,
|
|
4232
|
+
...RETIRED_KNOWLEDGE_TABLE_NAMES
|
|
4233
|
+
])];
|
|
4234
|
+
/** Schema-1/2 tables whose presence means the store needs a reset before schema 4 can run. */
|
|
4235
|
+
const LEGACY_KNOWLEDGE_TABLE_NAMES = new Set([...KNOWLEDGE_TABLE_NAMES, ...RETIRED_KNOWLEDGE_TABLE_NAMES].filter((name) => !KNOWLEDGE_PAGES_TABLE_NAMES.includes(name)));
|
|
4236
|
+
/**
|
|
4237
|
+
* Classifies what an adapter found before creating schema-4 tables. `tableNames` is every
|
|
4238
|
+
* `mastra_knowledge_*` table present; `schemaVersion` is the state row's version, if any.
|
|
4239
|
+
*/
|
|
4240
|
+
function inspectKnowledgePagesSchema(snapshot) {
|
|
4241
|
+
const legacy = snapshot.tableNames.filter((name) => LEGACY_KNOWLEDGE_TABLE_NAMES.has(name));
|
|
4242
|
+
if (legacy.length > 0) return {
|
|
4243
|
+
status: "incompatible-reset-required",
|
|
4244
|
+
schemaVersion: snapshot.schemaVersion ?? null,
|
|
4245
|
+
reason: `found Knowledge tables from an earlier schema (${legacy.join(", ")})`
|
|
4246
|
+
};
|
|
4247
|
+
if (snapshot.schemaVersion === 4) return {
|
|
4248
|
+
status: "compatible",
|
|
4249
|
+
schemaVersion: 4
|
|
4250
|
+
};
|
|
4251
|
+
if (snapshot.schemaVersion === void 0) {
|
|
4252
|
+
const present = snapshot.tableNames.filter((name) => KNOWLEDGE_PAGES_TABLE_NAMES.includes(name));
|
|
4253
|
+
if (present.length === 0) return {
|
|
4254
|
+
status: "uninitialized",
|
|
4255
|
+
schemaVersion: null
|
|
4220
4256
|
};
|
|
4221
4257
|
return {
|
|
4222
|
-
|
|
4223
|
-
|
|
4258
|
+
status: "incompatible-reset-required",
|
|
4259
|
+
schemaVersion: null,
|
|
4260
|
+
reason: `found Knowledge tables (${present.join(", ")}) without a schema version`
|
|
4224
4261
|
};
|
|
4225
|
-
}
|
|
4262
|
+
}
|
|
4263
|
+
return {
|
|
4264
|
+
status: "incompatible-reset-required",
|
|
4265
|
+
schemaVersion: snapshot.schemaVersion,
|
|
4266
|
+
reason: `Knowledge schema version ${snapshot.schemaVersion} is not 4`
|
|
4267
|
+
};
|
|
4226
4268
|
}
|
|
4269
|
+
/** Schema-2 table names schema 4 reuses with a different shape. */
|
|
4270
|
+
const REUSED_KNOWLEDGE_TABLE_NAMES = KNOWLEDGE_TABLE_NAMES.filter((name) => KNOWLEDGE_PAGES_TABLE_NAMES.includes(name));
|
|
4227
4271
|
/**
|
|
4228
|
-
*
|
|
4229
|
-
*
|
|
4230
|
-
*
|
|
4231
|
-
*
|
|
4272
|
+
* The old Knowledge tables an adapter drops on init, rows included: Knowledge before schema 4 was an
|
|
4273
|
+
* experimental store with no migration. Every schema-1/2-only table present is dropped. The table
|
|
4274
|
+
* names schema 4 reuses (mentions, activity, the state table) are dropped too, but only when an old
|
|
4275
|
+
* table is present and no schema-4 state row exists, so they are old-shaped. Schema-4-only tables
|
|
4276
|
+
* are never listed. On a schema-4 store this lists only tables an older process recreated during a
|
|
4277
|
+
* rolling deploy. Drop order is safe for adapters that enforce references.
|
|
4232
4278
|
*/
|
|
4233
|
-
function
|
|
4234
|
-
const
|
|
4235
|
-
const
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
return [...
|
|
4279
|
+
function legacyKnowledgeTablesToDrop(snapshot) {
|
|
4280
|
+
const present = new Set(snapshot.tableNames);
|
|
4281
|
+
const legacy = [...LEGACY_KNOWLEDGE_TABLE_NAMES].filter((name) => present.has(name));
|
|
4282
|
+
if (legacy.length === 0) return [];
|
|
4283
|
+
const reused = snapshot.schemaVersion === 4 ? [] : REUSED_KNOWLEDGE_TABLE_NAMES.filter((name) => present.has(name));
|
|
4284
|
+
const drop = /* @__PURE__ */ new Set([...legacy, ...reused]);
|
|
4285
|
+
return [...RETIRED_KNOWLEDGE_TABLE_NAMES, ...[...KNOWLEDGE_TABLE_NAMES].reverse()].filter((name) => drop.has(name));
|
|
4240
4286
|
}
|
|
4241
|
-
const
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4287
|
+
const KNOWLEDGE_PAGES_CORE_FUNCTIONS = [
|
|
4288
|
+
"assertKnowledgePageDescriptionWithinBound",
|
|
4289
|
+
"assertKnowledgeNoteTextWithinBound",
|
|
4290
|
+
"canonicalKnowledgeName",
|
|
4291
|
+
"knowledgePositionBetween",
|
|
4292
|
+
"KnowledgeMoveTooLargeError",
|
|
4293
|
+
"KnowledgePageDepthError",
|
|
4294
|
+
"KnowledgePageHasChildrenError",
|
|
4295
|
+
"KnowledgePageKeyConflictError"
|
|
4296
|
+
];
|
|
4297
|
+
const KNOWLEDGE_PAGES_CORE_NUMBERS = [
|
|
4298
|
+
"MAX_KNOWLEDGE_MOVE_PAGES",
|
|
4299
|
+
"MAX_KNOWLEDGE_PAGE_DEPTH",
|
|
4300
|
+
"MAX_KNOWLEDGE_PAGE_LIST_LIMIT"
|
|
4301
|
+
];
|
|
4302
|
+
function resolveKnowledgePagesCore(storageModule) {
|
|
4303
|
+
const module = typeof storageModule === "object" && storageModule !== null ? storageModule : {};
|
|
4304
|
+
const missing = [...KNOWLEDGE_PAGES_CORE_FUNCTIONS.filter((name) => typeof module[name] !== "function"), ...KNOWLEDGE_PAGES_CORE_NUMBERS.filter((name) => typeof module[name] !== "number")];
|
|
4305
|
+
if (missing.length > 0) throw new Error(`@mastra/core advertises Knowledge pages without the required storage API (missing ${missing.join(", ")})`);
|
|
4306
|
+
return module;
|
|
4247
4307
|
}
|
|
4248
|
-
|
|
4249
|
-
|
|
4308
|
+
/** Loads core's page-store runtime once, after checking core advertises `knowledge-pages`. */
|
|
4309
|
+
function createKnowledgePagesCoreLoader(features, loadStorageModule) {
|
|
4310
|
+
let core;
|
|
4311
|
+
return async () => {
|
|
4312
|
+
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.`);
|
|
4313
|
+
core ??= loadStorageModule().then(resolveKnowledgePagesCore).catch((error) => {
|
|
4314
|
+
core = void 0;
|
|
4315
|
+
throw error;
|
|
4316
|
+
});
|
|
4317
|
+
return core;
|
|
4318
|
+
};
|
|
4250
4319
|
}
|
|
4251
|
-
|
|
4252
|
-
|
|
4320
|
+
/**
|
|
4321
|
+
* Keyword-search words: the query split on whitespace, lowercased, deduplicated. Every word must
|
|
4322
|
+
* appear (as a case-insensitive substring) in a hit; an empty list means the search returns nothing.
|
|
4323
|
+
*/
|
|
4324
|
+
function knowledgeSearchWords(query) {
|
|
4325
|
+
return [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
|
|
4253
4326
|
}
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4327
|
+
/**
|
|
4328
|
+
* Why `key` can't be a Knowledge page key, or undefined when it can. Keys are one path segment:
|
|
4329
|
+
* non-empty, with no `/` (path separator), `#` (scope/path separator) or `%` (escape character).
|
|
4330
|
+
*/
|
|
4331
|
+
function knowledgePageKeyProblem(key) {
|
|
4332
|
+
if (!key) return "Knowledge page key must not be empty";
|
|
4333
|
+
if (/[/#%]/.test(key)) return `Knowledge page key "${key}" must not contain "/", "#" or "%"`;
|
|
4257
4334
|
}
|
|
4258
|
-
|
|
4259
|
-
|
|
4335
|
+
// src/storage/domains/knowledge/pages.ts
|
|
4336
|
+
const loadPagesCore = createKnowledgePagesCoreLoader(coreFeatures, () => import("@mastra/core/storage"));
|
|
4337
|
+
const assertKnowledgePageDescriptionWithinBound = async (description) => (await loadPagesCore()).assertKnowledgePageDescriptionWithinBound(description);
|
|
4338
|
+
const assertKnowledgeNoteTextWithinBound = async (text) => (await loadPagesCore()).assertKnowledgeNoteTextWithinBound(text);
|
|
4339
|
+
const T = {
|
|
4340
|
+
pages: "`mastra_knowledge_pages`",
|
|
4341
|
+
notes: "`mastra_knowledge_notes`",
|
|
4342
|
+
stamps: "`mastra_knowledge_note_stamps`",
|
|
4343
|
+
mentions: "`mastra_knowledge_mentions`",
|
|
4344
|
+
pins: "`mastra_knowledge_pins`",
|
|
4345
|
+
pinAudiences: "`mastra_knowledge_pin_audiences`",
|
|
4346
|
+
activity: "`mastra_knowledge_activity`"
|
|
4347
|
+
};
|
|
4348
|
+
const KEY = "`key`";
|
|
4349
|
+
const WHEN = "`when`";
|
|
4350
|
+
const BIN = "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin";
|
|
4351
|
+
const JV = `(SELECT jv.v FROM JSON_TABLE(?, '$[*]' COLUMNS (v VARCHAR(512) ${BIN} PATH '$')) jv)`;
|
|
4352
|
+
/** Live sibling keys are unique per parent; NULL for deleted pages and pages without a key. */
|
|
4353
|
+
const LIVE_KEY_COLUMN = `VARCHAR(768) ${BIN} AS (IF(deletedAt IS NULL AND \`key\` IS NOT NULL, CONCAT(parentId, '#', \`key\`), NULL)) STORED`;
|
|
4354
|
+
const MISSING_PAGE_INDEX = `SELECT 1 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'mastra_knowledge_pages' AND index_name = ?)`;
|
|
4355
|
+
/** Upgrades for Knowledge databases created by an earlier alpha build; `needed` returns a row when `apply` must run. */
|
|
4356
|
+
const KNOWLEDGE_PAGE_UPGRADES = [
|
|
4357
|
+
{
|
|
4358
|
+
needed: `SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mastra_knowledge_pages' AND column_name = 'liveKey' AND generation_expression LIKE '%scopeId%'`,
|
|
4359
|
+
args: [],
|
|
4360
|
+
apply: [`ALTER TABLE \`mastra_knowledge_pages\` DROP INDEX idx_kpages_live_key, DROP COLUMN liveKey, ADD COLUMN liveKey ${LIVE_KEY_COLUMN}, ADD UNIQUE KEY idx_kpages_live_key (liveKey)`]
|
|
4361
|
+
},
|
|
4362
|
+
{
|
|
4363
|
+
needed: `SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mastra_knowledge_pin_audiences' AND column_name = 'audience' AND character_maximum_length < 512`,
|
|
4364
|
+
args: [],
|
|
4365
|
+
apply: [`ALTER TABLE \`mastra_knowledge_pin_audiences\` MODIFY audience VARCHAR(512) ${BIN} NOT NULL`]
|
|
4366
|
+
},
|
|
4367
|
+
{
|
|
4368
|
+
needed: MISSING_PAGE_INDEX,
|
|
4369
|
+
args: ["idx_kpages_path"],
|
|
4370
|
+
apply: ["ALTER TABLE `mastra_knowledge_pages` ADD KEY idx_kpages_path (path(255))"]
|
|
4371
|
+
},
|
|
4372
|
+
{
|
|
4373
|
+
needed: MISSING_PAGE_INDEX,
|
|
4374
|
+
args: ["idx_kpages_declaration_parent"],
|
|
4375
|
+
apply: ["ALTER TABLE `mastra_knowledge_pages` ADD KEY idx_kpages_declaration_parent (declaration, parentId)"]
|
|
4376
|
+
},
|
|
4377
|
+
{
|
|
4378
|
+
needed: `SELECT 1 FROM DUAL WHERE EXISTS (SELECT 1 FROM mastra_knowledge_pins pin, JSON_TABLE(pin.audience, '$[*]' COLUMNS (v VARCHAR(512) ${BIN} PATH '$')) jv WHERE NOT EXISTS (SELECT 1 FROM mastra_knowledge_pin_audiences pa WHERE pa.pinId = pin.id AND pa.audience = jv.v))`,
|
|
4379
|
+
args: [],
|
|
4380
|
+
apply: [`INSERT IGNORE INTO mastra_knowledge_pin_audiences (audience, pinId) SELECT jv.v, pin.id FROM mastra_knowledge_pins pin, JSON_TABLE(pin.audience, '$[*]' COLUMNS (v VARCHAR(512) ${BIN} PATH '$')) jv`]
|
|
4381
|
+
}
|
|
4382
|
+
];
|
|
4383
|
+
/** DDL for the schema-4 page tables, in creation order. Every statement is idempotent. */
|
|
4384
|
+
const KNOWLEDGE_PAGE_DDL = [
|
|
4385
|
+
`CREATE TABLE IF NOT EXISTS \`mastra_knowledge_access_state\` (
|
|
4386
|
+
id VARCHAR(64) ${BIN} NOT NULL PRIMARY KEY,
|
|
4387
|
+
schemaVersion INT NOT NULL
|
|
4388
|
+
)`,
|
|
4389
|
+
`CREATE TABLE IF NOT EXISTS ${T.pages} (
|
|
4390
|
+
id VARCHAR(255) ${BIN} NOT NULL PRIMARY KEY,
|
|
4391
|
+
scopeId VARCHAR(255) ${BIN} NOT NULL,
|
|
4392
|
+
isScope TINYINT NOT NULL,
|
|
4393
|
+
address VARCHAR(512) ${BIN} NULL,
|
|
4394
|
+
declaration VARCHAR(255) ${BIN} NULL,
|
|
4395
|
+
\`key\` VARCHAR(255) ${BIN} NULL,
|
|
4396
|
+
parentId VARCHAR(255) ${BIN} NULL,
|
|
4397
|
+
position VARCHAR(255) ${BIN} NOT NULL,
|
|
4398
|
+
path VARCHAR(2048) ${BIN} NOT NULL,
|
|
4399
|
+
depth INT NOT NULL,
|
|
4400
|
+
name VARCHAR(512) ${BIN} NOT NULL,
|
|
4401
|
+
canonicalName VARCHAR(512) ${BIN} NOT NULL,
|
|
4402
|
+
description TEXT NULL,
|
|
4403
|
+
body LONGTEXT NULL,
|
|
4404
|
+
version INT NOT NULL,
|
|
4405
|
+
mergedInto VARCHAR(255) ${BIN} NULL,
|
|
4406
|
+
createdBy TEXT NOT NULL,
|
|
4407
|
+
updatedBy TEXT NOT NULL,
|
|
4408
|
+
createdAt VARCHAR(32) NOT NULL,
|
|
4409
|
+
updatedAt VARCHAR(32) NOT NULL,
|
|
4410
|
+
deletedAt VARCHAR(32) NULL,
|
|
4411
|
+
deletedBy VARCHAR(255) NULL,
|
|
4412
|
+
liveKey ${LIVE_KEY_COLUMN},
|
|
4413
|
+
UNIQUE KEY idx_kpages_address (address),
|
|
4414
|
+
UNIQUE KEY idx_kpages_live_key (liveKey),
|
|
4415
|
+
KEY idx_kpages_scope (scopeId),
|
|
4416
|
+
KEY idx_kpages_parent (parentId, position, id),
|
|
4417
|
+
KEY idx_kpages_path (path(255)),
|
|
4418
|
+
KEY idx_kpages_declaration_parent (declaration, parentId),
|
|
4419
|
+
KEY idx_kpages_canonical (canonicalName(255))
|
|
4420
|
+
)`,
|
|
4421
|
+
`CREATE TABLE IF NOT EXISTS ${T.notes} (
|
|
4422
|
+
id VARCHAR(255) ${BIN} NOT NULL PRIMARY KEY,
|
|
4423
|
+
pageId VARCHAR(255) ${BIN} NOT NULL,
|
|
4424
|
+
text LONGTEXT NOT NULL,
|
|
4425
|
+
\`when\` VARCHAR(32) NULL,
|
|
4426
|
+
provenance TEXT NOT NULL,
|
|
4427
|
+
version INT NOT NULL,
|
|
4428
|
+
createdAt VARCHAR(32) NOT NULL,
|
|
4429
|
+
updatedAt VARCHAR(32) NOT NULL,
|
|
4430
|
+
deletedAt VARCHAR(32) NULL,
|
|
4431
|
+
deletedBy VARCHAR(255) NULL,
|
|
4432
|
+
KEY idx_knotes_page (pageId, createdAt, id)
|
|
4433
|
+
)`,
|
|
4434
|
+
`CREATE TABLE IF NOT EXISTS ${T.stamps} (
|
|
4435
|
+
noteId VARCHAR(255) ${BIN} NOT NULL,
|
|
4436
|
+
scopeId VARCHAR(255) ${BIN} NOT NULL,
|
|
4437
|
+
PRIMARY KEY (noteId, scopeId),
|
|
4438
|
+
KEY idx_kstamps_scope (scopeId)
|
|
4439
|
+
)`,
|
|
4440
|
+
`CREATE TABLE IF NOT EXISTS ${T.mentions} (
|
|
4441
|
+
noteId VARCHAR(255) ${BIN} NOT NULL,
|
|
4442
|
+
pageId VARCHAR(255) ${BIN} NOT NULL,
|
|
4443
|
+
PRIMARY KEY (noteId, pageId),
|
|
4444
|
+
KEY idx_kmentions_page (pageId)
|
|
4445
|
+
)`,
|
|
4446
|
+
`CREATE TABLE IF NOT EXISTS ${T.pins} (
|
|
4447
|
+
id VARCHAR(255) ${BIN} NOT NULL PRIMARY KEY,
|
|
4448
|
+
pageId VARCHAR(255) ${BIN} NOT NULL,
|
|
4449
|
+
audience JSON NOT NULL,
|
|
4450
|
+
createdBy VARCHAR(255) NOT NULL,
|
|
4451
|
+
createdAt VARCHAR(32) NOT NULL,
|
|
4452
|
+
updatedAt VARCHAR(32) NOT NULL,
|
|
4453
|
+
UNIQUE KEY idx_kpins_page (pageId)
|
|
4454
|
+
)`,
|
|
4455
|
+
`CREATE TABLE IF NOT EXISTS ${T.pinAudiences} (
|
|
4456
|
+
audience VARCHAR(512) ${BIN} NOT NULL,
|
|
4457
|
+
pinId VARCHAR(255) ${BIN} NOT NULL,
|
|
4458
|
+
PRIMARY KEY (audience, pinId),
|
|
4459
|
+
KEY idx_kpin_audiences_pin (pinId)
|
|
4460
|
+
)`,
|
|
4461
|
+
`CREATE TABLE IF NOT EXISTS ${T.activity} (
|
|
4462
|
+
id VARCHAR(255) ${BIN} NOT NULL PRIMARY KEY,
|
|
4463
|
+
action VARCHAR(64) NOT NULL,
|
|
4464
|
+
scopeId VARCHAR(255) ${BIN} NOT NULL,
|
|
4465
|
+
pageId VARCHAR(255) ${BIN} NOT NULL,
|
|
4466
|
+
noteId VARCHAR(255) ${BIN} NULL,
|
|
4467
|
+
provenance TEXT NOT NULL,
|
|
4468
|
+
createdAt VARCHAR(32) NOT NULL,
|
|
4469
|
+
KEY idx_kactivity_page (pageId, id)
|
|
4470
|
+
)`
|
|
4471
|
+
];
|
|
4472
|
+
function str(value) {
|
|
4473
|
+
return value === null || value === void 0 ? void 0 : String(value);
|
|
4260
4474
|
}
|
|
4261
|
-
function
|
|
4262
|
-
return
|
|
4475
|
+
function date(value) {
|
|
4476
|
+
return value === null || value === void 0 ? void 0 : new Date(String(value));
|
|
4263
4477
|
}
|
|
4264
|
-
function
|
|
4265
|
-
return
|
|
4478
|
+
function provenance(raw) {
|
|
4479
|
+
return JSON.parse(String(raw));
|
|
4266
4480
|
}
|
|
4267
|
-
function
|
|
4481
|
+
function toPage(row) {
|
|
4268
4482
|
return {
|
|
4269
4483
|
id: String(row.id),
|
|
4270
|
-
|
|
4484
|
+
scopeId: String(row.scopeId),
|
|
4485
|
+
parentId: str(row.parentId) ?? null,
|
|
4486
|
+
isScope: Number(row.isScope) === 1,
|
|
4487
|
+
...row.address !== null && row.address !== void 0 ? { address: String(row.address) } : {},
|
|
4488
|
+
...row.declaration !== null && row.declaration !== void 0 ? { declaration: String(row.declaration) } : {},
|
|
4489
|
+
...row.key !== null && row.key !== void 0 ? { key: String(row.key) } : {},
|
|
4490
|
+
position: String(row.position),
|
|
4271
4491
|
name: String(row.name),
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
description: row.description == null ? void 0 : String(row.description),
|
|
4275
|
-
scope: parseJson$1(row.scopeJson ?? row.scope),
|
|
4492
|
+
...row.description !== null && row.description !== void 0 ? { description: String(row.description) } : {},
|
|
4493
|
+
...row.body !== null && row.body !== void 0 ? { body: String(row.body) } : {},
|
|
4276
4494
|
version: Number(row.version),
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4495
|
+
...row.mergedInto !== null && row.mergedInto !== void 0 ? { mergedInto: String(row.mergedInto) } : {},
|
|
4496
|
+
createdBy: provenance(row.createdBy),
|
|
4497
|
+
updatedBy: provenance(row.updatedBy),
|
|
4498
|
+
createdAt: new Date(String(row.createdAt)),
|
|
4499
|
+
updatedAt: new Date(String(row.updatedAt)),
|
|
4500
|
+
...row.deletedAt ? { deletedAt: new Date(String(row.deletedAt)) } : {},
|
|
4501
|
+
...row.deletedBy ? { deletedBy: String(row.deletedBy) } : {}
|
|
4280
4502
|
};
|
|
4281
4503
|
}
|
|
4282
|
-
function
|
|
4504
|
+
function toNote(row, stamps, mentions) {
|
|
4505
|
+
const when = date(row.when);
|
|
4283
4506
|
return {
|
|
4284
4507
|
id: String(row.id),
|
|
4285
|
-
|
|
4508
|
+
pageId: String(row.pageId),
|
|
4286
4509
|
text: String(row.text),
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4510
|
+
...when ? { when } : {},
|
|
4511
|
+
stampScopeIds: stamps,
|
|
4512
|
+
mentions,
|
|
4513
|
+
provenance: provenance(row.provenance),
|
|
4514
|
+
version: Number(row.version),
|
|
4515
|
+
createdAt: new Date(String(row.createdAt)),
|
|
4516
|
+
updatedAt: new Date(String(row.updatedAt)),
|
|
4517
|
+
...row.deletedAt ? { deletedAt: new Date(String(row.deletedAt)) } : {},
|
|
4518
|
+
...row.deletedBy ? { deletedBy: String(row.deletedBy) } : {}
|
|
4294
4519
|
};
|
|
4295
4520
|
}
|
|
4296
|
-
function
|
|
4521
|
+
function toPin(row) {
|
|
4297
4522
|
return {
|
|
4298
4523
|
id: String(row.id),
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
status: String(row.status),
|
|
4305
|
-
attempts: Number(row.attempts),
|
|
4306
|
-
availableAt: toDate(row.availableAt),
|
|
4307
|
-
claimedAt: optionalDate(row.claimedAt),
|
|
4308
|
-
claimedBy: row.claimedBy == null ? void 0 : String(row.claimedBy),
|
|
4309
|
-
createdAt: toDate(row.createdAt),
|
|
4310
|
-
completedAt: optionalDate(row.completedAt)
|
|
4524
|
+
pageId: String(row.pageId),
|
|
4525
|
+
audience: parseJsonList(row.audience),
|
|
4526
|
+
createdBy: String(row.createdBy),
|
|
4527
|
+
createdAt: new Date(String(row.createdAt)),
|
|
4528
|
+
updatedAt: new Date(String(row.updatedAt))
|
|
4311
4529
|
};
|
|
4312
4530
|
}
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
return
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
}),
|
|
4338
|
-
generateTableSQL({
|
|
4339
|
-
tableName: TABLE_KNOWLEDGE_MENTIONS,
|
|
4340
|
-
schema: KNOWLEDGE_MENTIONS_SCHEMA,
|
|
4341
|
-
compositePrimaryKey: [
|
|
4342
|
-
"sourceType",
|
|
4343
|
-
"sourceId",
|
|
4344
|
-
"recordId"
|
|
4345
|
-
]
|
|
4346
|
-
}),
|
|
4347
|
-
generateTableSQL({
|
|
4348
|
-
tableName: TABLE_KNOWLEDGE_ACTIVITY,
|
|
4349
|
-
schema: KNOWLEDGE_ACTIVITY_SCHEMA
|
|
4350
|
-
}),
|
|
4351
|
-
generateTableSQL({
|
|
4352
|
-
tableName: TABLE_KNOWLEDGE_SEMANTIC_OUTBOX,
|
|
4353
|
-
schema: KNOWLEDGE_SEMANTIC_OUTBOX_SCHEMA
|
|
4354
|
-
}),
|
|
4355
|
-
...KNOWLEDGE_INDEX_DDL.map(mysqlSql)
|
|
4356
|
-
];
|
|
4357
|
-
}
|
|
4358
|
-
#pool;
|
|
4359
|
-
#client;
|
|
4360
|
-
#operations;
|
|
4361
|
-
constructor({ pool, operations }) {
|
|
4362
|
-
super();
|
|
4363
|
-
this.#pool = pool;
|
|
4364
|
-
this.#client = createExecutor(pool);
|
|
4365
|
-
this.#operations = operations;
|
|
4531
|
+
function toListedPin(row) {
|
|
4532
|
+
return {
|
|
4533
|
+
...toPin(row),
|
|
4534
|
+
pageName: String(row.pageName)
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
function limitOf(limit, max) {
|
|
4538
|
+
return Math.max(1, Math.min(limit ?? max, max));
|
|
4539
|
+
}
|
|
4540
|
+
function encodeCursor(value) {
|
|
4541
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
4542
|
+
}
|
|
4543
|
+
function decodeCursor(cursor, length) {
|
|
4544
|
+
if (cursor === void 0) return void 0;
|
|
4545
|
+
try {
|
|
4546
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
4547
|
+
if (Array.isArray(parsed) && parsed.length === length && parsed.every((item) => typeof item === "string")) return parsed;
|
|
4548
|
+
} catch {}
|
|
4549
|
+
throw new Error("Invalid Knowledge cursor");
|
|
4550
|
+
}
|
|
4551
|
+
function isRetryableRace(error) {
|
|
4552
|
+
for (let current = error; current && typeof current === "object"; current = current.cause) {
|
|
4553
|
+
const { code, errno } = current;
|
|
4554
|
+
if (code === "ER_DUP_ENTRY" || errno === 1062 || code === "ER_LOCK_DEADLOCK" || errno === 1213) return true;
|
|
4366
4555
|
}
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
tableName: TABLE_KNOWLEDGE_ACTIVITY,
|
|
4387
|
-
schema: KNOWLEDGE_ACTIVITY_SCHEMA
|
|
4388
|
-
});
|
|
4389
|
-
await this.#operations.createTable({
|
|
4390
|
-
tableName: TABLE_KNOWLEDGE_SEMANTIC_OUTBOX,
|
|
4391
|
-
schema: KNOWLEDGE_SEMANTIC_OUTBOX_SCHEMA
|
|
4392
|
-
});
|
|
4393
|
-
for (const sql of KNOWLEDGE_INDEX_DDL) try {
|
|
4394
|
-
await this.#client.execute(sql);
|
|
4395
|
-
} catch (error) {
|
|
4396
|
-
if (error.code !== "ER_DUP_KEYNAME") throw error;
|
|
4556
|
+
return false;
|
|
4557
|
+
}
|
|
4558
|
+
function escapeLike(value) {
|
|
4559
|
+
return value.replace(/[=%_]/g, (match) => `=${match}`);
|
|
4560
|
+
}
|
|
4561
|
+
/**
|
|
4562
|
+
* A string above every string that starts with `prefix`, comparing by code point (which is how
|
|
4563
|
+
* `utf8mb4_bin` orders). `column >= prefix AND column < bound` is an exact, case-sensitive prefix
|
|
4564
|
+
* match that an index range scan can serve, where `LEFT(column, CHAR_LENGTH(prefix)) = prefix`
|
|
4565
|
+
* cannot. When the last code point cannot be raised (an empty prefix, or one made only of
|
|
4566
|
+
* U+10FFFF), the bound is the prefix plus more U+10FFFF than an address column can hold (512).
|
|
4567
|
+
*/
|
|
4568
|
+
function prefixUpperBound(prefix) {
|
|
4569
|
+
const points = Array.from(prefix);
|
|
4570
|
+
while (points.length > 0) {
|
|
4571
|
+
const last = points.pop().codePointAt(0);
|
|
4572
|
+
if (last < 1114111) {
|
|
4573
|
+
const next = last === 55295 ? 57344 : last + 1;
|
|
4574
|
+
return points.join("") + String.fromCodePoint(next);
|
|
4397
4575
|
}
|
|
4398
4576
|
}
|
|
4399
|
-
|
|
4577
|
+
return prefix + "".repeat(600);
|
|
4578
|
+
}
|
|
4579
|
+
/**
|
|
4580
|
+
* `utf8mb4_bin` is PAD SPACE: a value compares as if padded with spaces, so `p:a\t` sorts below `p:a`.
|
|
4581
|
+
* Starting the range at the prefix plus U+0000 keeps every value that begins with the prefix, whatever
|
|
4582
|
+
* code point follows. (Values equal to the prefix plus trailing spaces still match, as they always have.)
|
|
4583
|
+
*/
|
|
4584
|
+
function prefixLowerBound(prefix) {
|
|
4585
|
+
return prefix + "\0";
|
|
4586
|
+
}
|
|
4587
|
+
/** `column` starts with `prefix`, as an index-friendly range. */
|
|
4588
|
+
function prefixRange(column, prefix) {
|
|
4589
|
+
return {
|
|
4590
|
+
sql: `(${column} >= ? AND ${column} < ?)`,
|
|
4591
|
+
args: [prefixLowerBound(prefix), prefixUpperBound(prefix)]
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
/**
|
|
4595
|
+
* The readable scope page ids as a subquery. The caller's addresses, prefixes and children go in as
|
|
4596
|
+
* JSON arrays so a large read scope never hits a bind-parameter limit. Each kind is its own index
|
|
4597
|
+
* lookup (address equality, address range for a prefix, `(declaration, parentId)` for children),
|
|
4598
|
+
* unioned, so the cost follows the caller's reach rather than the number of scopes in the store.
|
|
4599
|
+
*/
|
|
4600
|
+
function readableScopes(read) {
|
|
4601
|
+
if (read === "all") return {
|
|
4602
|
+
sql: `SELECT id FROM ${T.pages} WHERE isScope = 1`,
|
|
4603
|
+
args: []
|
|
4604
|
+
};
|
|
4605
|
+
const parts = [];
|
|
4606
|
+
const args = [];
|
|
4607
|
+
if (read.addresses.length > 0) {
|
|
4608
|
+
parts.push(`SELECT id FROM ${T.pages} WHERE isScope = 1 AND address IN ${JV}`);
|
|
4609
|
+
args.push(JSON.stringify(read.addresses));
|
|
4610
|
+
}
|
|
4611
|
+
if (read.prefixes.length > 0) {
|
|
4612
|
+
parts.push(`SELECT sp.id FROM JSON_TABLE(?, '$[*]' COLUMNS (d VARCHAR(255) ${BIN} PATH '$.declaration', lo VARCHAR(512) ${BIN} PATH '$.lo', hi VARCHAR(1200) ${BIN} PATH '$.hi')) j STRAIGHT_JOIN ${T.pages} sp USE INDEX (idx_kpages_address) ON sp.address >= j.lo AND sp.address < j.hi AND sp.isScope = 1 AND sp.declaration = j.d`);
|
|
4613
|
+
args.push(JSON.stringify(read.prefixes.map((entry) => ({
|
|
4614
|
+
declaration: entry.declaration,
|
|
4615
|
+
lo: prefixLowerBound(entry.prefix),
|
|
4616
|
+
hi: prefixUpperBound(entry.prefix)
|
|
4617
|
+
}))));
|
|
4618
|
+
}
|
|
4619
|
+
if (read.children.length > 0) {
|
|
4620
|
+
parts.push(`SELECT sp.id FROM JSON_TABLE(?, '$[*]' COLUMNS (d VARCHAR(255) ${BIN} PATH '$.declaration', pid VARCHAR(255) ${BIN} PATH '$.parentId' NULL ON EMPTY)) j STRAIGHT_JOIN ${T.pages} sp ON sp.isScope = 1 AND sp.declaration = j.d AND sp.parentId <=> j.pid`);
|
|
4621
|
+
args.push(JSON.stringify(read.children));
|
|
4622
|
+
}
|
|
4623
|
+
if (parts.length === 0) return {
|
|
4624
|
+
sql: `SELECT id FROM ${T.pages} WHERE 0`,
|
|
4625
|
+
args: []
|
|
4626
|
+
};
|
|
4627
|
+
return {
|
|
4628
|
+
sql: parts.join(" UNION "),
|
|
4629
|
+
args
|
|
4630
|
+
};
|
|
4631
|
+
}
|
|
4632
|
+
/**
|
|
4633
|
+
* `FROM` clause that reads the readable scopes first and then each one's pages through `idx_kpages_scope`.
|
|
4634
|
+
* MySQL otherwise tends to scan every page in the store and test its scope, which touches other
|
|
4635
|
+
* tenants' rows. The `alias` pages are in readable scopes; add the page filters in `WHERE`.
|
|
4636
|
+
*/
|
|
4637
|
+
function pagesInReadableScopes(alias, read) {
|
|
4638
|
+
const scopes = readableScopes(read);
|
|
4639
|
+
return {
|
|
4640
|
+
sql: `FROM (${scopes.sql}) rs STRAIGHT_JOIN ${T.pages} ${alias} FORCE INDEX (idx_kpages_scope) ON ${alias}.scopeId = rs.id`,
|
|
4641
|
+
args: scopes.args
|
|
4642
|
+
};
|
|
4643
|
+
}
|
|
4644
|
+
/** `alias.scopeId` is readable. */
|
|
4645
|
+
function pageReadable(alias, read) {
|
|
4646
|
+
const scopes = readableScopes(read);
|
|
4647
|
+
return {
|
|
4648
|
+
sql: `${alias}.scopeId IN (SELECT rs.id FROM (${scopes.sql}) rs)`,
|
|
4649
|
+
args: scopes.args
|
|
4650
|
+
};
|
|
4651
|
+
}
|
|
4652
|
+
/**
|
|
4653
|
+
* Note `alias` is visible: its page's scope is readable, a stamp is readable, and every resolved
|
|
4654
|
+
* mention's page sits in a readable scope.
|
|
4655
|
+
*/
|
|
4656
|
+
function noteReadable(alias, read) {
|
|
4657
|
+
const scopes = readableScopes(read);
|
|
4658
|
+
return {
|
|
4659
|
+
sql: `EXISTS (SELECT 1 FROM ${T.pages} vp WHERE vp.id = ${alias}.pageId AND vp.scopeId IN (${scopes.sql}))
|
|
4660
|
+
AND EXISTS (SELECT 1 FROM ${T.stamps} vs WHERE vs.noteId = ${alias}.id AND vs.scopeId IN (${scopes.sql}))
|
|
4661
|
+
AND NOT EXISTS (SELECT 1 FROM ${T.mentions} vm JOIN ${T.pages} vmp ON vmp.id = vm.pageId WHERE vm.noteId = ${alias}.id AND vmp.scopeId NOT IN (${scopes.sql}))`,
|
|
4662
|
+
args: [
|
|
4663
|
+
...scopes.args,
|
|
4664
|
+
...scopes.args,
|
|
4665
|
+
...scopes.args
|
|
4666
|
+
]
|
|
4667
|
+
};
|
|
4668
|
+
}
|
|
4669
|
+
function jsonList(values) {
|
|
4670
|
+
return JSON.stringify(values);
|
|
4671
|
+
}
|
|
4672
|
+
var MySQLKnowledgePages = class {
|
|
4673
|
+
#read;
|
|
4674
|
+
#transaction;
|
|
4675
|
+
constructor(config) {
|
|
4676
|
+
this.#read = config.read;
|
|
4677
|
+
this.#transaction = config.transaction;
|
|
4678
|
+
}
|
|
4679
|
+
/** Deletes every row from the page tables (scopes, pages, notes, pins, activity). */
|
|
4680
|
+
async clearAll() {
|
|
4400
4681
|
await this.#transaction(async (tx) => {
|
|
4401
4682
|
for (const table of [
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4683
|
+
T.activity,
|
|
4684
|
+
T.pinAudiences,
|
|
4685
|
+
T.pins,
|
|
4686
|
+
T.mentions,
|
|
4687
|
+
T.stamps,
|
|
4688
|
+
T.notes,
|
|
4689
|
+
T.pages
|
|
4690
|
+
]) await tx.execute({
|
|
4691
|
+
sql: `DELETE FROM ${table}`,
|
|
4692
|
+
args: []
|
|
4693
|
+
});
|
|
4408
4694
|
});
|
|
4409
4695
|
}
|
|
4410
|
-
async
|
|
4411
|
-
await
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4696
|
+
async #query(executor, sql, args = []) {
|
|
4697
|
+
return (await executor.execute({
|
|
4698
|
+
sql,
|
|
4699
|
+
args
|
|
4700
|
+
})).rows;
|
|
4701
|
+
}
|
|
4702
|
+
async #pageRow(executor, id) {
|
|
4703
|
+
return (await this.#query(executor, `SELECT * FROM ${T.pages} WHERE id = ?`, [id]))[0];
|
|
4704
|
+
}
|
|
4705
|
+
async #livePage(executor, id) {
|
|
4706
|
+
const row = await this.#pageRow(executor, id);
|
|
4707
|
+
if (!row || row.deletedAt) throw new KnowledgeNotFoundError("page", id);
|
|
4708
|
+
return row;
|
|
4709
|
+
}
|
|
4710
|
+
async #logActivity(tx, action, event, now) {
|
|
4711
|
+
await tx.execute({
|
|
4712
|
+
sql: `INSERT INTO ${T.activity} (id, action, scopeId, pageId, noteId, provenance, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
4713
|
+
args: [
|
|
4714
|
+
createKnowledgeUlid(),
|
|
4715
|
+
action,
|
|
4716
|
+
event.scopeId,
|
|
4717
|
+
event.pageId,
|
|
4718
|
+
event.noteId ?? null,
|
|
4719
|
+
JSON.stringify(event.provenance),
|
|
4720
|
+
now
|
|
4721
|
+
]
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4724
|
+
async #lastPosition(tx, parentId, scopesOnly) {
|
|
4725
|
+
const rows = await this.#query(tx, `SELECT position FROM ${T.pages} WHERE parentId <=> ? AND isScope = ? AND deletedAt IS NULL ORDER BY position DESC, id DESC LIMIT 1`, [parentId, scopesOnly ? 1 : 0]);
|
|
4726
|
+
return rows[0] ? String(rows[0].position) : void 0;
|
|
4727
|
+
}
|
|
4728
|
+
async materializeScope(input) {
|
|
4729
|
+
for (let attempt = 1;; attempt++) try {
|
|
4730
|
+
return await this.#materializeScopeOnce(input);
|
|
4731
|
+
} catch (error) {
|
|
4732
|
+
if (attempt >= 5 || !isRetryableRace(error)) throw error;
|
|
4416
4733
|
}
|
|
4417
|
-
|
|
4734
|
+
}
|
|
4735
|
+
async #materializeScopeOnce(input) {
|
|
4736
|
+
const core = await loadPagesCore();
|
|
4737
|
+
await assertKnowledgePageDescriptionWithinBound(input.description);
|
|
4418
4738
|
return this.#transaction(async (tx) => {
|
|
4419
|
-
const
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
return
|
|
4739
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4740
|
+
const ensureScope = async (spec, parentId) => {
|
|
4741
|
+
await assertKnowledgePageDescriptionWithinBound(spec.description);
|
|
4742
|
+
const existing = (await this.#query(tx, `SELECT * FROM ${T.pages} WHERE isScope = 1 AND address = ?`, [spec.address]))[0];
|
|
4743
|
+
if (existing) return {
|
|
4744
|
+
row: existing,
|
|
4745
|
+
created: false
|
|
4746
|
+
};
|
|
4747
|
+
if (parentId !== null) await this.#livePage(tx, parentId);
|
|
4748
|
+
const id = createKnowledgeUlid();
|
|
4749
|
+
const position = core.knowledgePositionBetween(await this.#lastPosition(tx, parentId, true));
|
|
4750
|
+
const who = JSON.stringify(input.provenance);
|
|
4751
|
+
await tx.execute({
|
|
4752
|
+
sql: `INSERT INTO ${T.pages} (id, scopeId, isScope, address, declaration, parentId, position, path, depth, name, canonicalName, description, version, createdBy, updatedBy, createdAt, updatedAt) VALUES (?, ?, 1, ?, ?, ?, ?, ?, 0, ?, ?, ?, 1, ?, ?, ?, ?)`,
|
|
4753
|
+
args: [
|
|
4754
|
+
id,
|
|
4755
|
+
id,
|
|
4756
|
+
spec.address,
|
|
4757
|
+
spec.declaration,
|
|
4758
|
+
parentId,
|
|
4759
|
+
position,
|
|
4760
|
+
`/${id}/`,
|
|
4761
|
+
spec.name,
|
|
4762
|
+
core.canonicalKnowledgeName(spec.name),
|
|
4763
|
+
spec.description ?? null,
|
|
4764
|
+
who,
|
|
4765
|
+
who,
|
|
4766
|
+
now,
|
|
4767
|
+
now
|
|
4768
|
+
]
|
|
4769
|
+
});
|
|
4770
|
+
await this.#logActivity(tx, "scope-created", {
|
|
4771
|
+
scopeId: id,
|
|
4772
|
+
pageId: id,
|
|
4773
|
+
provenance: input.provenance
|
|
4774
|
+
}, now);
|
|
4775
|
+
return {
|
|
4776
|
+
row: await this.#pageRow(tx, id),
|
|
4777
|
+
created: true
|
|
4778
|
+
};
|
|
4779
|
+
};
|
|
4780
|
+
const ensured = await ensureScope(input, input.parentId ?? null);
|
|
4781
|
+
let row = ensured.row;
|
|
4782
|
+
const created = ensured.created;
|
|
4783
|
+
const scopeId = String(row.id);
|
|
4784
|
+
if (!created && input.parentId != null && str(row.parentId) === void 0) {
|
|
4785
|
+
await this.#livePage(tx, input.parentId);
|
|
4786
|
+
if (input.parentId === scopeId) throw new Error(`Scope ${scopeId} cannot be its own parent`);
|
|
4787
|
+
let ancestor = input.parentId;
|
|
4788
|
+
while (ancestor) {
|
|
4789
|
+
const up = await this.#pageRow(tx, ancestor);
|
|
4790
|
+
if (String(up.id) === scopeId) throw new Error(`Scope ${scopeId} cannot be linked under its own descendant`);
|
|
4791
|
+
ancestor = str(up.parentId);
|
|
4792
|
+
}
|
|
4793
|
+
await tx.execute({
|
|
4794
|
+
sql: `UPDATE ${T.pages} SET parentId = ?, position = ?, version = version + 1, updatedAt = ? WHERE id = ? AND parentId IS NULL`,
|
|
4795
|
+
args: [
|
|
4796
|
+
input.parentId,
|
|
4797
|
+
core.knowledgePositionBetween(await this.#lastPosition(tx, input.parentId, true)),
|
|
4798
|
+
now,
|
|
4799
|
+
scopeId
|
|
4800
|
+
]
|
|
4801
|
+
});
|
|
4802
|
+
row = await this.#pageRow(tx, scopeId);
|
|
4424
4803
|
}
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4804
|
+
let parentConflict;
|
|
4805
|
+
if (!created && input.parentId !== void 0 && (str(row.parentId) ?? null) !== input.parentId) parentConflict = {
|
|
4806
|
+
existingParentId: str(row.parentId) ?? null,
|
|
4807
|
+
requestedParentId: input.parentId
|
|
4808
|
+
};
|
|
4809
|
+
for (const child of input.children ?? []) await ensureScope(child, scopeId);
|
|
4810
|
+
const ensureTemplates = async (templates, parentId) => {
|
|
4811
|
+
for (const template of templates) {
|
|
4812
|
+
const problem = knowledgePageKeyProblem(template.key);
|
|
4813
|
+
if (problem) throw new Error(problem);
|
|
4814
|
+
const existing = (await this.#query(tx, `SELECT id FROM ${T.pages} WHERE parentId = ? AND isScope = 0 AND ${KEY} = ? AND deletedAt IS NULL`, [parentId, template.key]))[0];
|
|
4815
|
+
const id = existing ? String(existing.id) : (await this.#insertPage(tx, {
|
|
4816
|
+
parentId,
|
|
4817
|
+
key: template.key,
|
|
4818
|
+
name: template.name,
|
|
4819
|
+
description: template.description,
|
|
4820
|
+
provenance: input.provenance
|
|
4821
|
+
})).id;
|
|
4822
|
+
if (template.pages?.length) await ensureTemplates(template.pages, id);
|
|
4823
|
+
}
|
|
4824
|
+
};
|
|
4825
|
+
await ensureTemplates(input.pages ?? [], scopeId);
|
|
4826
|
+
return {
|
|
4827
|
+
scope: toPage(await this.#pageRow(tx, scopeId)),
|
|
4828
|
+
created,
|
|
4829
|
+
...parentConflict ? { parentConflict } : {}
|
|
4437
4830
|
};
|
|
4438
|
-
await tx.execute({
|
|
4439
|
-
sql: `INSERT INTO "${TABLE_KNOWLEDGE_NODES}" (id,type,name,canonicalName,kind,content,description,scope,scopeKey,version,mergedInto,createdAt,updatedAt) VALUES (?,?,?,?,?,?,?,jsonb(?),?,?,NULL,?,?)`,
|
|
4440
|
-
args: [
|
|
4441
|
-
node.id,
|
|
4442
|
-
"node",
|
|
4443
|
-
node.name,
|
|
4444
|
-
canonicalName(node.name),
|
|
4445
|
-
node.kind,
|
|
4446
|
-
node.content ?? null,
|
|
4447
|
-
node.description ?? null,
|
|
4448
|
-
JSON.stringify(scope),
|
|
4449
|
-
knowledgeScopeKey(scope),
|
|
4450
|
-
node.version,
|
|
4451
|
-
now.toISOString(),
|
|
4452
|
-
now.toISOString()
|
|
4453
|
-
]
|
|
4454
|
-
});
|
|
4455
|
-
await this.#replaceMentions(tx, "node", node.id, node.content ?? "", input.resolutionScope ?? scope, scope);
|
|
4456
|
-
await this.#activity(tx, "node-created", "node", node.id, scope);
|
|
4457
|
-
await this.#outbox(tx, "node", node.id, "upsert", node.version, scope);
|
|
4458
|
-
return node;
|
|
4459
4831
|
});
|
|
4460
4832
|
}
|
|
4461
|
-
async
|
|
4462
|
-
return this.#
|
|
4833
|
+
async listScopeDeclarations() {
|
|
4834
|
+
return (await this.#query(this.#read, `SELECT declaration, COUNT(*) AS scopes FROM ${T.pages} WHERE isScope = 1 AND declaration IS NOT NULL GROUP BY declaration ORDER BY declaration`)).map((row) => ({
|
|
4835
|
+
declaration: String(row.declaration),
|
|
4836
|
+
scopes: Number(row.scopes)
|
|
4837
|
+
}));
|
|
4463
4838
|
}
|
|
4464
|
-
async
|
|
4465
|
-
return this.#
|
|
4839
|
+
async renameScopeDeclaration(input) {
|
|
4840
|
+
return this.#transaction(async (tx) => {
|
|
4841
|
+
const within = prefixRange("declaration", `${input.from}/`);
|
|
4842
|
+
const affected = (await this.#query(tx, `SELECT id, address, declaration FROM ${T.pages} WHERE isScope = 1 AND (declaration = ? OR ${within.sql}) FOR UPDATE`, [input.from, ...within.args])).map((row) => ({
|
|
4843
|
+
id: String(row.id),
|
|
4844
|
+
address: String(row.address),
|
|
4845
|
+
from: String(row.declaration)
|
|
4846
|
+
})).filter((row) => row.from === input.from || row.from.startsWith(`${input.from}/`)).map((row) => ({
|
|
4847
|
+
...row,
|
|
4848
|
+
to: input.to + row.from.slice(input.from.length)
|
|
4849
|
+
}));
|
|
4850
|
+
input.check(affected.map(({ address, from, to }) => ({
|
|
4851
|
+
address,
|
|
4852
|
+
from,
|
|
4853
|
+
to
|
|
4854
|
+
})));
|
|
4855
|
+
for (const row of affected) await tx.execute({
|
|
4856
|
+
sql: `UPDATE ${T.pages} SET declaration = ? WHERE id = ?`,
|
|
4857
|
+
args: [row.to, row.id]
|
|
4858
|
+
});
|
|
4859
|
+
return affected.length;
|
|
4860
|
+
});
|
|
4466
4861
|
}
|
|
4467
|
-
async
|
|
4468
|
-
|
|
4862
|
+
async getScopesByAddress(addresses) {
|
|
4863
|
+
if (addresses.length === 0) return [];
|
|
4864
|
+
return (await this.#query(this.#read, `SELECT * FROM ${T.pages} WHERE isScope = 1 AND address IN ${JV}`, [jsonList(addresses)])).map(toPage);
|
|
4469
4865
|
}
|
|
4470
|
-
async
|
|
4471
|
-
const
|
|
4472
|
-
const
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
}
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
kind: input.kind,
|
|
4491
|
-
hasContent: input.hasContent
|
|
4492
|
-
});
|
|
4493
|
-
const updatedAt = databaseTimestamp(cursor.updatedAt);
|
|
4494
|
-
clauses.push("(updatedAt < ? OR (updatedAt = ? AND (name > ? OR (name = ? AND id > ?))))");
|
|
4495
|
-
args.push(updatedAt, updatedAt, cursor.name, cursor.name, cursor.id);
|
|
4496
|
-
}
|
|
4497
|
-
args.push(input.limit ?? 100);
|
|
4498
|
-
return (await this.#client.execute({
|
|
4499
|
-
sql: `SELECT *, scope AS scopeJson FROM "${TABLE_KNOWLEDGE_NODES}" WHERE ${clauses.join(" AND ")} ORDER BY updatedAt DESC, name ASC, id ASC LIMIT ?`,
|
|
4500
|
-
args
|
|
4501
|
-
})).rows.map(parseNode);
|
|
4866
|
+
async listChildScopes(input) {
|
|
4867
|
+
const core = await loadPagesCore();
|
|
4868
|
+
const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
4869
|
+
const cursor = decodeCursor(input.cursor, 2);
|
|
4870
|
+
const args = [input.parentId];
|
|
4871
|
+
let after = "";
|
|
4872
|
+
if (cursor) {
|
|
4873
|
+
after = ` AND (position > ? OR (position = ? AND id > ?))`;
|
|
4874
|
+
args.push(cursor[0], cursor[0], cursor[1]);
|
|
4875
|
+
}
|
|
4876
|
+
args.push(limit + 1);
|
|
4877
|
+
const rows = await this.#query(this.#read, `SELECT * FROM ${T.pages} WHERE isScope = 1 AND parentId <=> ?${after} ORDER BY position, id LIMIT ?`, args);
|
|
4878
|
+
return this.#pageOutput(rows, limit, (row) => [String(row.position), String(row.id)]);
|
|
4879
|
+
}
|
|
4880
|
+
#pageOutput(rows, limit, cursorOf) {
|
|
4881
|
+
const page = rows.slice(0, limit);
|
|
4882
|
+
return {
|
|
4883
|
+
pages: page.map(toPage),
|
|
4884
|
+
...rows.length > limit ? { nextCursor: encodeCursor(cursorOf(page[page.length - 1])) } : {}
|
|
4885
|
+
};
|
|
4502
4886
|
}
|
|
4503
|
-
async
|
|
4504
|
-
|
|
4887
|
+
async getPage(input) {
|
|
4888
|
+
const readable = pageReadable("p", input.read);
|
|
4889
|
+
const rows = await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.id = ? AND ${readable.sql}`, [input.id, ...readable.args]);
|
|
4890
|
+
return rows[0] ? toPage(rows[0]) : null;
|
|
4891
|
+
}
|
|
4892
|
+
async getPages(input) {
|
|
4893
|
+
const ids = [...new Set(input.ids)];
|
|
4894
|
+
if (ids.length === 0) return [];
|
|
4895
|
+
const readable = pageReadable("p", input.read);
|
|
4896
|
+
return (await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.deletedAt IS NULL AND p.id IN ${JV} AND ${readable.sql}`, [jsonList(ids), ...readable.args])).map(toPage);
|
|
4897
|
+
}
|
|
4898
|
+
async getPageByPath(input) {
|
|
4899
|
+
const readable = pageReadable("p", input.read);
|
|
4900
|
+
let parentId = input.scopeId;
|
|
4901
|
+
let row;
|
|
4902
|
+
for (const key of input.path) {
|
|
4903
|
+
row = (await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.parentId = ? AND p.isScope = 0 AND p.${KEY} = ? AND p.deletedAt IS NULL AND ${readable.sql}`, [
|
|
4904
|
+
parentId,
|
|
4905
|
+
key,
|
|
4906
|
+
...readable.args
|
|
4907
|
+
]))[0];
|
|
4908
|
+
if (!row) return null;
|
|
4909
|
+
parentId = String(row.id);
|
|
4910
|
+
}
|
|
4911
|
+
return row ? toPage(row) : null;
|
|
4912
|
+
}
|
|
4913
|
+
async findPagesByName(input) {
|
|
4914
|
+
const core = await loadPagesCore();
|
|
4915
|
+
const readable = pageReadable("p", input.read);
|
|
4916
|
+
const args = [core.canonicalKnowledgeName(input.name), ...readable.args];
|
|
4917
|
+
let scope = "";
|
|
4918
|
+
if (input.scopeId !== void 0) {
|
|
4919
|
+
scope = ` AND p.scopeId = ?`;
|
|
4920
|
+
args.push(input.scopeId);
|
|
4921
|
+
}
|
|
4922
|
+
args.push(limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT));
|
|
4923
|
+
return (await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.canonicalName = ? AND p.deletedAt IS NULL AND ${readable.sql}${scope} ORDER BY p.createdAt DESC, p.id DESC LIMIT ?`, args)).map(toPage);
|
|
4924
|
+
}
|
|
4925
|
+
async listChildPages(input) {
|
|
4926
|
+
const core = await loadPagesCore();
|
|
4927
|
+
const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
4928
|
+
const cursor = decodeCursor(input.cursor, 2);
|
|
4929
|
+
const readable = pageReadable("p", input.read);
|
|
4930
|
+
const args = [input.parentId, ...readable.args];
|
|
4931
|
+
let after = "";
|
|
4932
|
+
if (cursor) {
|
|
4933
|
+
after = ` AND (p.position > ? OR (p.position = ? AND p.id > ?))`;
|
|
4934
|
+
args.push(cursor[0], cursor[0], cursor[1]);
|
|
4935
|
+
}
|
|
4936
|
+
args.push(limit + 1);
|
|
4937
|
+
const rows = await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.parentId = ? AND p.isScope = 0 AND p.deletedAt IS NULL AND ${readable.sql}${after} ORDER BY p.position, p.id LIMIT ?`, args);
|
|
4938
|
+
return this.#pageOutput(rows, limit, (row) => [String(row.position), String(row.id)]);
|
|
4939
|
+
}
|
|
4940
|
+
async listPages(input) {
|
|
4941
|
+
const core = await loadPagesCore();
|
|
4942
|
+
const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
4943
|
+
const cursor = decodeCursor(input.cursor, 2);
|
|
4944
|
+
const readable = pagesInReadableScopes("p", input.read);
|
|
4945
|
+
const args = [...readable.args];
|
|
4946
|
+
let filters = "";
|
|
4947
|
+
if (input.scopeId !== void 0) {
|
|
4948
|
+
filters += ` AND p.scopeId = ?`;
|
|
4949
|
+
args.push(input.scopeId);
|
|
4950
|
+
}
|
|
4951
|
+
if (input.namePrefix) {
|
|
4952
|
+
const prefix = core.canonicalKnowledgeName(input.namePrefix);
|
|
4953
|
+
filters += ` AND LEFT(p.canonicalName, CHAR_LENGTH(?)) = ?`;
|
|
4954
|
+
args.push(prefix, prefix);
|
|
4955
|
+
}
|
|
4956
|
+
if (cursor) {
|
|
4957
|
+
filters += ` AND (p.name > ? OR (p.name = ? AND p.id > ?))`;
|
|
4958
|
+
args.push(cursor[0], cursor[0], cursor[1]);
|
|
4959
|
+
}
|
|
4960
|
+
args.push(limit + 1);
|
|
4961
|
+
const rows = await this.#query(this.#read, `SELECT p.* ${readable.sql} WHERE p.isScope = 0 AND p.deletedAt IS NULL${filters} ORDER BY p.name, p.id LIMIT ?`, args);
|
|
4962
|
+
return this.#pageOutput(rows, limit, (row) => [String(row.name), String(row.id)]);
|
|
4963
|
+
}
|
|
4964
|
+
async #insertPage(tx, input) {
|
|
4965
|
+
const core = await loadPagesCore();
|
|
4966
|
+
await assertKnowledgePageDescriptionWithinBound(input.description);
|
|
4967
|
+
const parent = await this.#livePage(tx, input.parentId);
|
|
4968
|
+
const isScopeParent = Number(parent.isScope) === 1;
|
|
4969
|
+
const scopeId = isScopeParent ? String(parent.id) : String(parent.scopeId);
|
|
4970
|
+
const depth = isScopeParent ? 1 : Number(parent.depth) + 1;
|
|
4971
|
+
if (depth > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(input.parentId);
|
|
4972
|
+
const id = input.id ?? createKnowledgeUlid();
|
|
4973
|
+
if (await this.#pageRow(tx, id)) throw new KnowledgeConflictError(id);
|
|
4974
|
+
if (input.key !== void 0) {
|
|
4975
|
+
const problem = knowledgePageKeyProblem(input.key);
|
|
4976
|
+
if (problem) throw new Error(problem);
|
|
4977
|
+
if ((await this.#query(tx, `SELECT id FROM ${T.pages} WHERE parentId = ? AND isScope = 0 AND ${KEY} = ? AND deletedAt IS NULL`, [String(parent.id), input.key])).length > 0) throw new core.KnowledgePageKeyConflictError(input.key, String(parent.id));
|
|
4978
|
+
}
|
|
4979
|
+
const position = input.position ?? core.knowledgePositionBetween(await this.#lastPosition(tx, String(parent.id), false));
|
|
4980
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4981
|
+
const who = JSON.stringify(input.provenance);
|
|
4982
|
+
await tx.execute({
|
|
4983
|
+
sql: `INSERT INTO ${T.pages} (id, scopeId, isScope, ${KEY}, parentId, position, path, depth, name, canonicalName, description, body, version, createdBy, updatedBy, createdAt, updatedAt) VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?)`,
|
|
4984
|
+
args: [
|
|
4985
|
+
id,
|
|
4986
|
+
scopeId,
|
|
4987
|
+
input.key ?? null,
|
|
4988
|
+
String(parent.id),
|
|
4989
|
+
position,
|
|
4990
|
+
`${String(parent.path)}${id}/`,
|
|
4991
|
+
depth,
|
|
4992
|
+
input.name,
|
|
4993
|
+
core.canonicalKnowledgeName(input.name),
|
|
4994
|
+
input.description || null,
|
|
4995
|
+
input.body ?? null,
|
|
4996
|
+
who,
|
|
4997
|
+
who,
|
|
4998
|
+
now,
|
|
4999
|
+
now
|
|
5000
|
+
]
|
|
5001
|
+
});
|
|
5002
|
+
await this.#logActivity(tx, "page-created", {
|
|
5003
|
+
scopeId,
|
|
5004
|
+
pageId: id,
|
|
5005
|
+
provenance: input.provenance
|
|
5006
|
+
}, now);
|
|
5007
|
+
return toPage(await this.#pageRow(tx, id));
|
|
5008
|
+
}
|
|
5009
|
+
async createPage(input) {
|
|
5010
|
+
await assertKnowledgePageDescriptionWithinBound(input.description);
|
|
5011
|
+
return this.#transaction((tx) => this.#insertPage(tx, input));
|
|
5012
|
+
}
|
|
5013
|
+
async updatePage(input) {
|
|
5014
|
+
const core = await loadPagesCore();
|
|
5015
|
+
await assertKnowledgePageDescriptionWithinBound(input.description);
|
|
4505
5016
|
return this.#transaction(async (tx) => {
|
|
4506
|
-
const
|
|
4507
|
-
if (
|
|
4508
|
-
|
|
4509
|
-
const
|
|
4510
|
-
const
|
|
4511
|
-
const
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
if ((await tx.execute({
|
|
4515
|
-
sql: `UPDATE "${TABLE_KNOWLEDGE_NODES}" SET name=?,canonicalName=?,kind=?,content=?,description=?,scope=jsonb(?),scopeKey=?,version=version+1,updatedAt=? WHERE id=? AND type='node' AND version=?`,
|
|
5017
|
+
const row = await this.#livePage(tx, input.id);
|
|
5018
|
+
if (Number(row.version) !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
|
|
5019
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5020
|
+
const name = input.name ?? String(row.name);
|
|
5021
|
+
const description = input.description === void 0 ? row.description ?? null : input.description || null;
|
|
5022
|
+
const body = input.body === void 0 ? row.body ?? null : input.body;
|
|
5023
|
+
await tx.execute({
|
|
5024
|
+
sql: `UPDATE ${T.pages} SET name = ?, canonicalName = ?, description = ?, body = ?, version = version + 1, updatedBy = ?, updatedAt = ? WHERE id = ?`,
|
|
4516
5025
|
args: [
|
|
4517
5026
|
name,
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
now.toISOString(),
|
|
4525
|
-
input.id,
|
|
4526
|
-
input.version
|
|
5027
|
+
core.canonicalKnowledgeName(name),
|
|
5028
|
+
description,
|
|
5029
|
+
body,
|
|
5030
|
+
JSON.stringify(input.provenance),
|
|
5031
|
+
now,
|
|
5032
|
+
input.id
|
|
4527
5033
|
]
|
|
4528
|
-
})
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
args: [input.id]
|
|
4536
|
-
});
|
|
4537
|
-
for (const row of records.rows) {
|
|
4538
|
-
const factScope = parseJson$1(row.scopeJson);
|
|
4539
|
-
await this.#outbox(tx, "record", String(row.id), "delete", createKnowledgeUlid(), factScope);
|
|
4540
|
-
if (row.deletedAt == null) await this.#outbox(tx, "record", String(row.id), "upsert", createKnowledgeUlid(), factScope);
|
|
4541
|
-
}
|
|
4542
|
-
}
|
|
4543
|
-
await this.#outbox(tx, "node", input.id, "upsert", input.version + 1, scope);
|
|
4544
|
-
return {
|
|
4545
|
-
...existing,
|
|
4546
|
-
name,
|
|
4547
|
-
kind: input.kind ?? existing.kind,
|
|
4548
|
-
content,
|
|
4549
|
-
description,
|
|
4550
|
-
scope,
|
|
4551
|
-
version: input.version + 1,
|
|
4552
|
-
updatedAt: now
|
|
4553
|
-
};
|
|
5034
|
+
});
|
|
5035
|
+
await this.#logActivity(tx, "page-updated", {
|
|
5036
|
+
scopeId: String(row.scopeId),
|
|
5037
|
+
pageId: input.id,
|
|
5038
|
+
provenance: input.provenance
|
|
5039
|
+
}, now);
|
|
5040
|
+
return toPage(await this.#pageRow(tx, input.id));
|
|
4554
5041
|
});
|
|
4555
5042
|
}
|
|
4556
|
-
|
|
4557
|
-
|
|
5043
|
+
/**
|
|
5044
|
+
* Rehomes `root` (and its subtree) under `parent`: new path prefix, scope and depth in one go.
|
|
5045
|
+
* Throws when the subtree is too large, too deep, or the root's key clashes with a live child of `parent`.
|
|
5046
|
+
*/
|
|
5047
|
+
async #rehome(tx, root, parent, options) {
|
|
5048
|
+
const core = await loadPagesCore();
|
|
5049
|
+
const isScopeParent = Number(parent.isScope) === 1;
|
|
5050
|
+
const newScopeId = isScopeParent ? String(parent.id) : String(parent.scopeId);
|
|
5051
|
+
const newDepth = isScopeParent ? 1 : Number(parent.depth) + 1;
|
|
5052
|
+
const oldPath = String(root.path);
|
|
5053
|
+
const within = prefixRange("path", oldPath);
|
|
5054
|
+
const live = (await this.#query(tx, `SELECT id, ${KEY}, depth, deletedAt FROM ${T.pages} WHERE ${within.sql}`, within.args)).filter((row) => !row.deletedAt);
|
|
5055
|
+
if (live.length > core.MAX_KNOWLEDGE_MOVE_PAGES) throw new core.KnowledgeMoveTooLargeError(options.countLimitId, live.length);
|
|
5056
|
+
const shift = newDepth - Number(root.depth);
|
|
5057
|
+
if (Math.max(...live.map((row) => Number(row.depth))) + shift > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(String(root.id));
|
|
5058
|
+
if (!root.deletedAt && root.key !== null) {
|
|
5059
|
+
if ((await this.#query(tx, `SELECT id FROM ${T.pages} WHERE parentId = ? AND isScope = 0 AND ${KEY} = ? AND deletedAt IS NULL AND id <> ?`, [
|
|
5060
|
+
String(parent.id),
|
|
5061
|
+
String(root.key),
|
|
5062
|
+
String(root.id)
|
|
5063
|
+
])).length > 0) throw new core.KnowledgePageKeyConflictError(String(root.key), String(parent.id));
|
|
5064
|
+
}
|
|
5065
|
+
const newPath = `${String(parent.path)}${String(root.id)}/`;
|
|
5066
|
+
const position = options.position ?? core.knowledgePositionBetween(await this.#lastPosition(tx, String(parent.id), false));
|
|
5067
|
+
await tx.execute({
|
|
5068
|
+
sql: `UPDATE ${T.pages} SET path = CONCAT(?, SUBSTRING(path, CHAR_LENGTH(?) + 1)), depth = depth + ?, scopeId = ? WHERE ${within.sql}`,
|
|
5069
|
+
args: [
|
|
5070
|
+
newPath,
|
|
5071
|
+
oldPath,
|
|
5072
|
+
shift,
|
|
5073
|
+
newScopeId,
|
|
5074
|
+
...within.args
|
|
5075
|
+
]
|
|
5076
|
+
});
|
|
5077
|
+
await tx.execute({
|
|
5078
|
+
sql: `UPDATE ${T.pages} SET parentId = ?, position = ? WHERE id = ?`,
|
|
5079
|
+
args: [
|
|
5080
|
+
String(parent.id),
|
|
5081
|
+
position,
|
|
5082
|
+
String(root.id)
|
|
5083
|
+
]
|
|
5084
|
+
});
|
|
5085
|
+
return {
|
|
5086
|
+
moved: live.length,
|
|
5087
|
+
scopeId: newScopeId
|
|
5088
|
+
};
|
|
5089
|
+
}
|
|
5090
|
+
movePage(input) {
|
|
4558
5091
|
return this.#transaction(async (tx) => {
|
|
4559
|
-
const
|
|
4560
|
-
if (
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
if (
|
|
4565
|
-
const
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
});
|
|
4573
|
-
if ((await tx.execute({
|
|
4574
|
-
sql: `UPDATE "${TABLE_KNOWLEDGE_NODES}" SET mergedInto=?,version=version+1,updatedAt=? WHERE id=? AND type='node' AND version=? AND mergedInto IS NULL`,
|
|
5092
|
+
const row = await this.#livePage(tx, input.id);
|
|
5093
|
+
if (Number(row.isScope) === 1) throw new Error(`Cannot move ${input.id}: it is a scope page`);
|
|
5094
|
+
if (Number(row.version) !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
|
|
5095
|
+
const parent = await this.#livePage(tx, input.parentId);
|
|
5096
|
+
const path = String(row.path);
|
|
5097
|
+
if (String(parent.id) === input.id || String(parent.path).startsWith(path)) throw new Error(`Cannot move ${input.id} into its own subtree`);
|
|
5098
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5099
|
+
const { scopeId } = await this.#rehome(tx, row, parent, {
|
|
5100
|
+
position: input.position,
|
|
5101
|
+
countLimitId: input.id
|
|
5102
|
+
});
|
|
5103
|
+
await tx.execute({
|
|
5104
|
+
sql: `UPDATE ${T.pages} SET version = version + 1, updatedBy = ?, updatedAt = ? WHERE id = ?`,
|
|
4575
5105
|
args: [
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
input.sourceVersion
|
|
5106
|
+
JSON.stringify(input.provenance),
|
|
5107
|
+
now,
|
|
5108
|
+
input.id
|
|
4580
5109
|
]
|
|
4581
|
-
})
|
|
5110
|
+
});
|
|
5111
|
+
await this.#logActivity(tx, "page-moved", {
|
|
5112
|
+
scopeId,
|
|
5113
|
+
pageId: input.id,
|
|
5114
|
+
provenance: input.provenance
|
|
5115
|
+
}, now);
|
|
5116
|
+
return toPage(await this.#pageRow(tx, input.id));
|
|
5117
|
+
});
|
|
5118
|
+
}
|
|
5119
|
+
async mergePages(input) {
|
|
5120
|
+
const core = await loadPagesCore();
|
|
5121
|
+
return this.#transaction(async (tx) => {
|
|
5122
|
+
const source = await this.#livePage(tx, input.sourceId);
|
|
5123
|
+
const target = await this.#livePage(tx, input.targetId);
|
|
5124
|
+
if (Number(source.isScope) === 1 || Number(target.isScope) === 1) throw new Error("Cannot merge a scope page: merging applies to non-scope pages only");
|
|
5125
|
+
if (Number(source.version) !== input.sourceVersion) throw new KnowledgeConflictError(input.sourceId);
|
|
5126
|
+
const sourcePath = String(source.path);
|
|
5127
|
+
if (input.sourceId === input.targetId || String(target.path).startsWith(sourcePath)) throw new Error(`Page ${input.targetId} is inside page ${input.sourceId}; it cannot be the merge target (subtree)`);
|
|
5128
|
+
const within = prefixRange("path", sourcePath);
|
|
5129
|
+
const carried = await this.#query(tx, `SELECT COUNT(*) AS n FROM ${T.pages} WHERE ${within.sql} AND id <> ? AND deletedAt IS NULL`, [...within.args, input.sourceId]);
|
|
5130
|
+
if (Number(carried[0].n) > core.MAX_KNOWLEDGE_MOVE_PAGES) throw new core.KnowledgeMoveTooLargeError(input.sourceId, Number(carried[0].n));
|
|
5131
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4582
5132
|
await tx.execute({
|
|
4583
|
-
sql: `UPDATE
|
|
4584
|
-
args: [
|
|
5133
|
+
sql: `UPDATE ${T.pages} SET deletedAt = ?, deletedBy = ?, mergedInto = ?, scopeId = ?, version = version + 1, updatedBy = ?, updatedAt = ? WHERE id = ?`,
|
|
5134
|
+
args: [
|
|
5135
|
+
now,
|
|
5136
|
+
input.provenance.author,
|
|
5137
|
+
input.targetId,
|
|
5138
|
+
String(target.scopeId),
|
|
5139
|
+
JSON.stringify(input.provenance),
|
|
5140
|
+
now,
|
|
5141
|
+
input.sourceId
|
|
5142
|
+
]
|
|
5143
|
+
});
|
|
5144
|
+
const children = await this.#query(tx, `SELECT * FROM ${T.pages} WHERE parentId = ? AND isScope = 0`, [input.sourceId]);
|
|
5145
|
+
for (const child of children) await this.#rehome(tx, child, target, {
|
|
5146
|
+
position: String(child.position),
|
|
5147
|
+
countLimitId: input.sourceId
|
|
4585
5148
|
});
|
|
4586
5149
|
await tx.execute({
|
|
4587
|
-
sql: `
|
|
4588
|
-
args: [
|
|
5150
|
+
sql: `UPDATE ${T.notes} SET pageId = ? WHERE pageId = ?`,
|
|
5151
|
+
args: [input.targetId, input.sourceId]
|
|
4589
5152
|
});
|
|
4590
5153
|
await tx.execute({
|
|
4591
|
-
sql: `UPDATE
|
|
4592
|
-
args: [
|
|
4593
|
-
});
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
5154
|
+
sql: `UPDATE IGNORE ${T.mentions} SET pageId = ? WHERE pageId = ?`,
|
|
5155
|
+
args: [input.targetId, input.sourceId]
|
|
5156
|
+
});
|
|
5157
|
+
await tx.execute({
|
|
5158
|
+
sql: `DELETE FROM ${T.mentions} WHERE pageId = ?`,
|
|
5159
|
+
args: [input.sourceId]
|
|
5160
|
+
});
|
|
5161
|
+
const pins = await this.#query(tx, `SELECT * FROM ${T.pins} WHERE pageId IN (?, ?)`, [input.sourceId, input.targetId]);
|
|
5162
|
+
const sourcePin = pins.find((pin) => String(pin.pageId) === input.sourceId);
|
|
5163
|
+
if (sourcePin) {
|
|
5164
|
+
const targetPin = pins.find((pin) => String(pin.pageId) === input.targetId);
|
|
5165
|
+
const audience = parseJsonList(sourcePin.audience);
|
|
5166
|
+
if (targetPin) {
|
|
5167
|
+
const merged = [.../* @__PURE__ */ new Set([...parseJsonList(targetPin.audience), ...audience])];
|
|
5168
|
+
await tx.execute({
|
|
5169
|
+
sql: `UPDATE ${T.pins} SET audience = ?, updatedAt = ? WHERE id = ?`,
|
|
5170
|
+
args: [
|
|
5171
|
+
JSON.stringify(merged),
|
|
5172
|
+
now,
|
|
5173
|
+
String(targetPin.id)
|
|
5174
|
+
]
|
|
5175
|
+
});
|
|
5176
|
+
await this.#indexAudience(tx, String(targetPin.id), merged);
|
|
5177
|
+
await this.#indexAudience(tx, String(sourcePin.id), []);
|
|
5178
|
+
await tx.execute({
|
|
5179
|
+
sql: `DELETE FROM ${T.pins} WHERE id = ?`,
|
|
5180
|
+
args: [String(sourcePin.id)]
|
|
5181
|
+
});
|
|
5182
|
+
} else await tx.execute({
|
|
5183
|
+
sql: `UPDATE ${T.pins} SET pageId = ?, updatedAt = ? WHERE id = ?`,
|
|
4601
5184
|
args: [
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
target.version
|
|
5185
|
+
input.targetId,
|
|
5186
|
+
now,
|
|
5187
|
+
String(sourcePin.id)
|
|
4606
5188
|
]
|
|
4607
|
-
})
|
|
4608
|
-
mergedTarget = {
|
|
4609
|
-
...target,
|
|
4610
|
-
description: source.description,
|
|
4611
|
-
version: target.version + 1,
|
|
4612
|
-
updatedAt: adoptedAt
|
|
4613
|
-
};
|
|
4614
|
-
await this.#activity(tx, "node-updated", "node", target.id, target.scope);
|
|
4615
|
-
}
|
|
5189
|
+
});
|
|
4616
5190
|
}
|
|
4617
|
-
await this.#
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
5191
|
+
await this.#logActivity(tx, "page-merged", {
|
|
5192
|
+
scopeId: String(target.scopeId),
|
|
5193
|
+
pageId: input.sourceId,
|
|
5194
|
+
provenance: input.provenance
|
|
5195
|
+
}, now);
|
|
5196
|
+
return toPage(await this.#pageRow(tx, input.targetId));
|
|
4621
5197
|
});
|
|
4622
5198
|
}
|
|
4623
|
-
async
|
|
4624
|
-
await
|
|
4625
|
-
const scope = canonicalizeKnowledgeScope(input.scope);
|
|
4626
|
-
const resolutionScope = canonicalizeKnowledgeScope(input.resolutionScope);
|
|
4627
|
-
const defaultScope = canonicalizeKnowledgeScope(input.defaultScope);
|
|
5199
|
+
async deletePage(input) {
|
|
5200
|
+
const core = await loadPagesCore();
|
|
4628
5201
|
return this.#transaction(async (tx) => {
|
|
4629
|
-
const
|
|
4630
|
-
if (
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
text: input.text,
|
|
4635
|
-
scope,
|
|
4636
|
-
sourceThreadId: input.sourceThreadId,
|
|
4637
|
-
capturedAt: /* @__PURE__ */ new Date(),
|
|
4638
|
-
when: input.when ? new Date(input.when) : void 0,
|
|
4639
|
-
metadata: input.metadata
|
|
4640
|
-
};
|
|
5202
|
+
const row = await this.#livePage(tx, input.id);
|
|
5203
|
+
if (Number(row.isScope) === 1) throw new Error(`Cannot delete ${input.id}: it is a scope page`);
|
|
5204
|
+
if (Number(row.version) !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
|
|
5205
|
+
if ((await this.#query(tx, `SELECT 1 FROM ${T.pages} WHERE parentId = ? AND deletedAt IS NULL LIMIT 1`, [input.id])).length > 0) throw new core.KnowledgePageHasChildrenError(input.id);
|
|
5206
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4641
5207
|
await tx.execute({
|
|
4642
|
-
sql: `
|
|
5208
|
+
sql: `UPDATE ${T.pages} SET deletedAt = ?, deletedBy = ?, version = version + 1, updatedBy = ?, updatedAt = ? WHERE id = ?`,
|
|
4643
5209
|
args: [
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
record.sourceThreadId,
|
|
4650
|
-
record.capturedAt.toISOString(),
|
|
4651
|
-
record.when?.toISOString() ?? null,
|
|
4652
|
-
record.metadata ? JSON.stringify(record.metadata) : null
|
|
5210
|
+
now,
|
|
5211
|
+
input.deletedBy,
|
|
5212
|
+
JSON.stringify({ author: input.deletedBy }),
|
|
5213
|
+
now,
|
|
5214
|
+
input.id
|
|
4653
5215
|
]
|
|
4654
5216
|
});
|
|
4655
|
-
await
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
5217
|
+
await tx.execute({
|
|
5218
|
+
sql: `DELETE FROM ${T.pinAudiences} WHERE pinId IN (SELECT id FROM ${T.pins} WHERE pageId = ?)`,
|
|
5219
|
+
args: [input.id]
|
|
5220
|
+
});
|
|
5221
|
+
await tx.execute({
|
|
5222
|
+
sql: `DELETE FROM ${T.pins} WHERE pageId = ?`,
|
|
5223
|
+
args: [input.id]
|
|
5224
|
+
});
|
|
5225
|
+
await this.#logActivity(tx, "page-deleted", {
|
|
5226
|
+
scopeId: String(row.scopeId),
|
|
5227
|
+
pageId: input.id,
|
|
5228
|
+
provenance: { author: input.deletedBy }
|
|
5229
|
+
}, now);
|
|
5230
|
+
return toPage(await this.#pageRow(tx, input.id));
|
|
4665
5231
|
});
|
|
4666
|
-
return result.rows[0] ? parseKnowledge(result.rows[0]) : null;
|
|
4667
|
-
}
|
|
4668
|
-
async listKnowledgeAbout(input) {
|
|
4669
|
-
return this.#queryKnowledge(input, "about");
|
|
4670
|
-
}
|
|
4671
|
-
async listKnowledgeMentioning(input) {
|
|
4672
|
-
return this.#queryKnowledge(input, "mentioning");
|
|
4673
5232
|
}
|
|
4674
|
-
async
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
const
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
5233
|
+
async #hydrateNotes(executor, rows) {
|
|
5234
|
+
if (rows.length === 0) return [];
|
|
5235
|
+
const ids = jsonList(rows.map((row) => String(row.id)));
|
|
5236
|
+
const stamps = await this.#query(executor, `SELECT noteId, scopeId FROM ${T.stamps} WHERE noteId IN ${JV} ORDER BY scopeId`, [ids]);
|
|
5237
|
+
const mentions = await this.#query(executor, `SELECT noteId, pageId FROM ${T.mentions} WHERE noteId IN ${JV} ORDER BY pageId`, [ids]);
|
|
5238
|
+
return rows.map((row) => toNote(row, stamps.filter((stamp) => String(stamp.noteId) === String(row.id)).map((stamp) => String(stamp.scopeId)), mentions.filter((mention) => String(mention.noteId) === String(row.id)).map((mention) => String(mention.pageId))));
|
|
5239
|
+
}
|
|
5240
|
+
async #noteById(executor, id) {
|
|
5241
|
+
const rows = await this.#query(executor, `SELECT * FROM ${T.notes} WHERE id = ?`, [id]);
|
|
5242
|
+
return (await this.#hydrateNotes(executor, rows))[0] ?? null;
|
|
5243
|
+
}
|
|
5244
|
+
async getNote(input) {
|
|
5245
|
+
const readable = noteReadable("n", input.read);
|
|
5246
|
+
const rows = await this.#query(this.#read, `SELECT n.* FROM ${T.notes} n WHERE n.id = ?${input.includeDeleted ? "" : " AND n.deletedAt IS NULL"} AND ${readable.sql}`, [input.id, ...readable.args]);
|
|
5247
|
+
return (await this.#hydrateNotes(this.#read, rows))[0] ?? null;
|
|
5248
|
+
}
|
|
5249
|
+
async listNotes(input) {
|
|
5250
|
+
const core = await loadPagesCore();
|
|
5251
|
+
const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
5252
|
+
const cursor = decodeCursor(input.cursor, 2);
|
|
5253
|
+
const readable = noteReadable("n", input.read);
|
|
5254
|
+
const args = [input.pageId, ...readable.args];
|
|
5255
|
+
let after = "";
|
|
5256
|
+
if (cursor) {
|
|
5257
|
+
after = ` AND (n.createdAt > ? OR (n.createdAt = ? AND n.id > ?))`;
|
|
5258
|
+
args.push(cursor[0], cursor[0], cursor[1]);
|
|
5259
|
+
}
|
|
4682
5260
|
args.push(limit + 1);
|
|
4683
|
-
const
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
5261
|
+
const rows = await this.#query(this.#read, `SELECT n.* FROM ${T.notes} n WHERE n.pageId = ?${input.includeDeleted ? "" : " AND n.deletedAt IS NULL"} AND ${readable.sql}${after} ORDER BY n.createdAt, n.id LIMIT ?`, args);
|
|
5262
|
+
return this.#noteOutput(rows, limit);
|
|
5263
|
+
}
|
|
5264
|
+
async countNotes(input) {
|
|
5265
|
+
const ids = [...new Set(input.pageIds)];
|
|
5266
|
+
const counts = Object.fromEntries(ids.map((id) => [id, 0]));
|
|
5267
|
+
if (ids.length === 0) return counts;
|
|
5268
|
+
const readable = noteReadable("n", input.read);
|
|
5269
|
+
const rows = await this.#query(this.#read, `SELECT n.pageId AS pageId, COUNT(*) AS total FROM ${T.notes} n WHERE n.pageId IN ${JV} AND n.deletedAt IS NULL AND ${readable.sql} GROUP BY n.pageId`, [jsonList(ids), ...readable.args]);
|
|
5270
|
+
for (const row of rows) counts[String(row.pageId)] = Number(row.total);
|
|
5271
|
+
return counts;
|
|
5272
|
+
}
|
|
5273
|
+
async listNotesMentioning(input) {
|
|
5274
|
+
const core = await loadPagesCore();
|
|
5275
|
+
const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
5276
|
+
const cursor = decodeCursor(input.cursor, 2);
|
|
5277
|
+
const readable = noteReadable("n", input.read);
|
|
5278
|
+
const args = [
|
|
5279
|
+
input.pageId,
|
|
5280
|
+
input.pageId,
|
|
5281
|
+
...readable.args
|
|
5282
|
+
];
|
|
5283
|
+
let after = "";
|
|
5284
|
+
if (cursor) {
|
|
5285
|
+
after = ` AND (n.createdAt < ? OR (n.createdAt = ? AND n.id < ?))`;
|
|
5286
|
+
args.push(cursor[0], cursor[0], cursor[1]);
|
|
5287
|
+
}
|
|
5288
|
+
args.push(limit + 1);
|
|
5289
|
+
const rows = await this.#query(this.#read, `SELECT n.* FROM ${T.notes} n WHERE n.pageId <> ? AND n.deletedAt IS NULL AND EXISTS (SELECT 1 FROM ${T.mentions} m WHERE m.noteId = n.id AND m.pageId = ?) AND ${readable.sql}${after} ORDER BY n.createdAt DESC, n.id DESC LIMIT ?`, args);
|
|
5290
|
+
return this.#noteOutput(rows, limit);
|
|
5291
|
+
}
|
|
5292
|
+
async #noteOutput(rows, limit) {
|
|
5293
|
+
const page = rows.slice(0, limit);
|
|
4687
5294
|
return {
|
|
4688
|
-
|
|
4689
|
-
|
|
5295
|
+
notes: await this.#hydrateNotes(this.#read, page),
|
|
5296
|
+
...rows.length > limit ? { nextCursor: encodeCursor([String(page[page.length - 1].createdAt), String(page[page.length - 1].id)]) } : {}
|
|
4690
5297
|
};
|
|
4691
5298
|
}
|
|
4692
|
-
async
|
|
5299
|
+
async #assertScopes(tx, ids) {
|
|
5300
|
+
if (ids.length === 0) throw new Error("A note needs at least one stamp scope");
|
|
5301
|
+
const found = await this.#query(tx, `SELECT id FROM ${T.pages} WHERE isScope = 1 AND id IN ${JV}`, [jsonList(ids)]);
|
|
5302
|
+
const known = new Set(found.map((row) => String(row.id)));
|
|
5303
|
+
const missing = ids.find((id) => !known.has(id));
|
|
5304
|
+
if (missing) throw new Error(`Note stamp ${missing} is not a scope page`);
|
|
5305
|
+
}
|
|
5306
|
+
async addNote(input) {
|
|
5307
|
+
await assertKnowledgeNoteTextWithinBound(input.text);
|
|
4693
5308
|
return this.#transaction(async (tx) => {
|
|
4694
|
-
const
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
const
|
|
5309
|
+
const page = await this.#livePage(tx, input.pageId);
|
|
5310
|
+
const stamps = [...new Set(input.stampScopeIds)];
|
|
5311
|
+
await this.#assertScopes(tx, stamps);
|
|
5312
|
+
const mentions = [...new Set(input.mentions)];
|
|
5313
|
+
for (const mention of mentions) await this.#livePage(tx, mention);
|
|
5314
|
+
const id = input.id ?? createKnowledgeUlid();
|
|
5315
|
+
if ((await this.#query(tx, `SELECT 1 FROM ${T.notes} WHERE id = ?`, [id])).length > 0) throw new KnowledgeConflictError(id);
|
|
5316
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4698
5317
|
await tx.execute({
|
|
4699
|
-
sql: `
|
|
5318
|
+
sql: `INSERT INTO ${T.notes} (id, pageId, text, ${WHEN}, provenance, version, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
|
|
4700
5319
|
args: [
|
|
4701
|
-
|
|
4702
|
-
input.
|
|
4703
|
-
input.
|
|
5320
|
+
id,
|
|
5321
|
+
input.pageId,
|
|
5322
|
+
input.text,
|
|
5323
|
+
input.when ? input.when.toISOString() : null,
|
|
5324
|
+
JSON.stringify(input.provenance),
|
|
5325
|
+
now,
|
|
5326
|
+
now
|
|
4704
5327
|
]
|
|
4705
5328
|
});
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
5329
|
+
for (const stamp of stamps) await tx.execute({
|
|
5330
|
+
sql: `INSERT INTO ${T.stamps} (noteId, scopeId) VALUES (?, ?)`,
|
|
5331
|
+
args: [id, stamp]
|
|
5332
|
+
});
|
|
5333
|
+
for (const mention of mentions) await tx.execute({
|
|
5334
|
+
sql: `INSERT INTO ${T.mentions} (noteId, pageId) VALUES (?, ?)`,
|
|
5335
|
+
args: [id, mention]
|
|
5336
|
+
});
|
|
5337
|
+
await this.#logActivity(tx, "note-added", {
|
|
5338
|
+
scopeId: String(page.scopeId),
|
|
5339
|
+
pageId: input.pageId,
|
|
5340
|
+
noteId: id,
|
|
5341
|
+
provenance: input.provenance
|
|
5342
|
+
}, now);
|
|
5343
|
+
return await this.#noteById(tx, id);
|
|
4713
5344
|
});
|
|
4714
5345
|
}
|
|
4715
|
-
async
|
|
5346
|
+
async #noteWithPage(tx, id) {
|
|
5347
|
+
const note = (await this.#query(tx, `SELECT * FROM ${T.notes} WHERE id = ?`, [id]))[0];
|
|
5348
|
+
if (!note) throw new KnowledgeNotFoundError("note", id);
|
|
5349
|
+
const page = await this.#pageRow(tx, String(note.pageId));
|
|
5350
|
+
if (!page) throw new KnowledgeNotFoundError("page", String(note.pageId));
|
|
5351
|
+
return {
|
|
5352
|
+
note,
|
|
5353
|
+
page
|
|
5354
|
+
};
|
|
5355
|
+
}
|
|
5356
|
+
restampNote(input) {
|
|
4716
5357
|
return this.#transaction(async (tx) => {
|
|
4717
|
-
const
|
|
4718
|
-
if (
|
|
4719
|
-
if (
|
|
5358
|
+
const { note, page } = await this.#noteWithPage(tx, input.id);
|
|
5359
|
+
if (note.deletedAt) throw new KnowledgeNotFoundError("note", input.id);
|
|
5360
|
+
if (Number(note.version) !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
|
|
5361
|
+
const stamps = [...new Set(input.stampScopeIds)];
|
|
5362
|
+
await this.#assertScopes(tx, stamps);
|
|
5363
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4720
5364
|
await tx.execute({
|
|
4721
|
-
sql: `
|
|
5365
|
+
sql: `DELETE FROM ${T.stamps} WHERE noteId = ?`,
|
|
4722
5366
|
args: [input.id]
|
|
4723
5367
|
});
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
5368
|
+
for (const stamp of stamps) await tx.execute({
|
|
5369
|
+
sql: `INSERT INTO ${T.stamps} (noteId, scopeId) VALUES (?, ?)`,
|
|
5370
|
+
args: [input.id, stamp]
|
|
5371
|
+
});
|
|
5372
|
+
await tx.execute({
|
|
5373
|
+
sql: `UPDATE ${T.notes} SET version = version + 1, updatedAt = ? WHERE id = ?`,
|
|
5374
|
+
args: [now, input.id]
|
|
5375
|
+
});
|
|
5376
|
+
await this.#logActivity(tx, "note-restamped", {
|
|
5377
|
+
scopeId: String(page.scopeId),
|
|
5378
|
+
pageId: String(page.id),
|
|
5379
|
+
noteId: input.id,
|
|
5380
|
+
provenance: input.provenance
|
|
5381
|
+
}, now);
|
|
5382
|
+
return await this.#noteById(tx, input.id);
|
|
4731
5383
|
});
|
|
4732
5384
|
}
|
|
4733
|
-
|
|
4734
|
-
const scope = canonicalizeKnowledgeScope(input.scope);
|
|
5385
|
+
removeNote(input) {
|
|
4735
5386
|
return this.#transaction(async (tx) => {
|
|
4736
|
-
const
|
|
4737
|
-
if (
|
|
5387
|
+
const { note, page } = await this.#noteWithPage(tx, input.id);
|
|
5388
|
+
if (note.deletedAt) throw new KnowledgeNotFoundError("note", input.id);
|
|
5389
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4738
5390
|
await tx.execute({
|
|
4739
|
-
sql: `UPDATE
|
|
5391
|
+
sql: `UPDATE ${T.notes} SET deletedAt = ?, deletedBy = ?, version = version + 1, updatedAt = ? WHERE id = ?`,
|
|
4740
5392
|
args: [
|
|
4741
|
-
|
|
4742
|
-
|
|
5393
|
+
now,
|
|
5394
|
+
input.deletedBy,
|
|
5395
|
+
now,
|
|
4743
5396
|
input.id
|
|
4744
5397
|
]
|
|
4745
5398
|
});
|
|
4746
|
-
await this.#
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
5399
|
+
await this.#logActivity(tx, "note-removed", {
|
|
5400
|
+
scopeId: String(page.scopeId),
|
|
5401
|
+
pageId: String(page.id),
|
|
5402
|
+
noteId: input.id,
|
|
5403
|
+
provenance: input.provenance
|
|
5404
|
+
}, now);
|
|
5405
|
+
return await this.#noteById(tx, input.id);
|
|
4753
5406
|
});
|
|
4754
5407
|
}
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
})
|
|
4772
|
-
type: String(row.type),
|
|
4773
|
-
id: String(row.id),
|
|
4774
|
-
recordId: String(row.id),
|
|
4775
|
-
name: String(row.name),
|
|
4776
|
-
text: [
|
|
4777
|
-
String(row.name),
|
|
4778
|
-
...row.description ? [String(row.description)] : [],
|
|
4779
|
-
...row.content ? [String(row.content)] : []
|
|
4780
|
-
].join("\n"),
|
|
4781
|
-
scope: parseJson$1(row.scopeJson)
|
|
4782
|
-
}));
|
|
4783
|
-
if (results.length < (input.limit ?? 20)) {
|
|
4784
|
-
const records = await this.#client.execute({
|
|
4785
|
-
sql: `SELECT f.*,f.scope AS scopeJson,r.name,r.scope AS parentScopeJson FROM "${TABLE_KNOWLEDGE_RECORDS}" f JOIN "${TABLE_KNOWLEDGE_NODES}" r ON r.id=f.node AND r.type='node' AND r.mergedInto IS NULL WHERE f.deletedAt IS NULL AND ${visibleSql(keys, "f.scopeKey")} AND lower(f.text) LIKE ? ESCAPE '=' ORDER BY f.id DESC LIMIT ?`,
|
|
4786
|
-
args: [
|
|
4787
|
-
...keys,
|
|
4788
|
-
query,
|
|
4789
|
-
(input.limit ?? 20) - results.length
|
|
4790
|
-
]
|
|
4791
|
-
});
|
|
4792
|
-
results.push(...records.rows.map((row) => {
|
|
4793
|
-
const parentVisible = isKnowledgeScopeVisible(parseJson$1(row.parentScopeJson), scope);
|
|
4794
|
-
return {
|
|
4795
|
-
type: "record",
|
|
4796
|
-
id: String(row.id),
|
|
4797
|
-
recordId: parentVisible ? String(row.node) : String(row.id),
|
|
4798
|
-
name: parentVisible ? String(row.name) : "(private node)",
|
|
4799
|
-
text: String(row.text),
|
|
4800
|
-
scope: parseJson$1(row.scopeJson)
|
|
4801
|
-
};
|
|
4802
|
-
}));
|
|
4803
|
-
}
|
|
4804
|
-
return results;
|
|
4805
|
-
}
|
|
4806
|
-
/**
|
|
4807
|
-
* @deprecated Curation cursors were removed. Observation-time curate is the only Knowledge writer and needs no
|
|
4808
|
-
* cursor. Always throws.
|
|
4809
|
-
*/
|
|
4810
|
-
async getCurationCursor(_input) {
|
|
4811
|
-
throw new Error(KNOWLEDGE_CURATION_CURSOR_REMOVED_MESSAGE);
|
|
4812
|
-
}
|
|
4813
|
-
/**
|
|
4814
|
-
* @deprecated Curation cursors were removed. Observation-time curate is the only Knowledge writer and needs no
|
|
4815
|
-
* cursor. Always throws.
|
|
4816
|
-
*/
|
|
4817
|
-
async advanceCurationCursor(_input) {
|
|
4818
|
-
throw new Error(KNOWLEDGE_CURATION_CURSOR_REMOVED_MESSAGE);
|
|
5408
|
+
restoreNote(input) {
|
|
5409
|
+
return this.#transaction(async (tx) => {
|
|
5410
|
+
const { note, page } = await this.#noteWithPage(tx, input.id);
|
|
5411
|
+
if (!note.deletedAt) throw new KnowledgeNotFoundError("note", input.id);
|
|
5412
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5413
|
+
await tx.execute({
|
|
5414
|
+
sql: `UPDATE ${T.notes} SET deletedAt = NULL, deletedBy = NULL, version = version + 1, updatedAt = ? WHERE id = ?`,
|
|
5415
|
+
args: [now, input.id]
|
|
5416
|
+
});
|
|
5417
|
+
await this.#logActivity(tx, "note-restored", {
|
|
5418
|
+
scopeId: String(page.scopeId),
|
|
5419
|
+
pageId: String(page.id),
|
|
5420
|
+
noteId: input.id,
|
|
5421
|
+
provenance: input.provenance
|
|
5422
|
+
}, now);
|
|
5423
|
+
return await this.#noteById(tx, input.id);
|
|
5424
|
+
});
|
|
4819
5425
|
}
|
|
4820
|
-
async
|
|
4821
|
-
const
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
5426
|
+
async searchPages(input) {
|
|
5427
|
+
const words = knowledgeSearchWords(input.query);
|
|
5428
|
+
if (!words.length) return [];
|
|
5429
|
+
const phrase = input.query.trim().toLowerCase().replace(/\s+/g, " ");
|
|
5430
|
+
const limit = Math.max(1, input.limit ?? 20);
|
|
5431
|
+
const likes = words.map((word) => `%${escapeLike(word)}%`);
|
|
5432
|
+
const readable = pagesInReadableScopes("p", input.read);
|
|
5433
|
+
const scopeFilter = input.scopeIds ? ` AND p.scopeId IN ${JV}` : "";
|
|
5434
|
+
const scopeArgs = input.scopeIds ? [jsonList(input.scopeIds)] : [];
|
|
5435
|
+
const pageMatch = words.map(() => `(LOWER(p.name) LIKE ? ESCAPE '=' OR LOWER(COALESCE(p.description, '')) LIKE ? ESCAPE '=' OR LOWER(COALESCE(p.body, '')) LIKE ? ESCAPE '=')`).join(" AND ");
|
|
5436
|
+
const nameMatch = words.map(() => `LOWER(p.name) LIKE ? ESCAPE '='`).join(" AND ");
|
|
5437
|
+
const hits = (await this.#query(this.#read, `SELECT p.* ${readable.sql} WHERE p.deletedAt IS NULL AND p.isScope = 0${scopeFilter} AND ${pageMatch} ORDER BY CASE WHEN LOCATE(?, p.canonicalName) > 0 THEN 0 WHEN ${nameMatch} THEN 1 ELSE 2 END, p.updatedAt DESC, p.id ASC LIMIT ?`, [
|
|
5438
|
+
...readable.args,
|
|
5439
|
+
...scopeArgs,
|
|
5440
|
+
...likes.flatMap((like) => [
|
|
5441
|
+
like,
|
|
5442
|
+
like,
|
|
5443
|
+
like
|
|
5444
|
+
]),
|
|
5445
|
+
phrase,
|
|
5446
|
+
...likes,
|
|
5447
|
+
limit
|
|
5448
|
+
])).map((row) => ({
|
|
5449
|
+
type: "page",
|
|
5450
|
+
page: toPage(row)
|
|
4837
5451
|
}));
|
|
5452
|
+
if (hits.length >= limit) return hits;
|
|
5453
|
+
const noteVisible = noteReadable("n", input.read);
|
|
5454
|
+
const noteMatch = words.map(() => `LOWER(n.text) LIKE ? ESCAPE '='`).join(" AND ");
|
|
5455
|
+
const noteRows = await this.#query(this.#read, `SELECT n.* FROM ${T.notes} n JOIN ${T.pages} p ON p.id = n.pageId WHERE n.deletedAt IS NULL AND p.deletedAt IS NULL${scopeFilter} AND ${noteMatch} AND ${noteVisible.sql} ORDER BY n.updatedAt DESC, n.id DESC LIMIT ?`, [
|
|
5456
|
+
...scopeArgs,
|
|
5457
|
+
...likes,
|
|
5458
|
+
...noteVisible.args,
|
|
5459
|
+
limit - hits.length
|
|
5460
|
+
]);
|
|
5461
|
+
const notes = await this.#hydrateNotes(this.#read, noteRows);
|
|
5462
|
+
for (const note of notes) {
|
|
5463
|
+
const page = await this.#pageRow(this.#read, note.pageId);
|
|
5464
|
+
if (page) hits.push({
|
|
5465
|
+
type: "note",
|
|
5466
|
+
note,
|
|
5467
|
+
page: toPage(page)
|
|
5468
|
+
});
|
|
5469
|
+
}
|
|
5470
|
+
return hits;
|
|
5471
|
+
}
|
|
5472
|
+
async listPageActivity(input) {
|
|
5473
|
+
const core = await loadPagesCore();
|
|
5474
|
+
const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
5475
|
+
const cursor = decodeCursor(input.cursor, 1);
|
|
5476
|
+
const readable = pageReadable("p", input.read);
|
|
5477
|
+
const noteVisible = noteReadable("n", input.read);
|
|
5478
|
+
const args = [...readable.args];
|
|
5479
|
+
let filters = "";
|
|
5480
|
+
if (input.scopeId !== void 0) {
|
|
5481
|
+
filters += ` AND p.scopeId = ?`;
|
|
5482
|
+
args.push(input.scopeId);
|
|
5483
|
+
}
|
|
5484
|
+
if (input.pageId !== void 0) {
|
|
5485
|
+
filters += ` AND e.pageId = ?`;
|
|
5486
|
+
args.push(input.pageId);
|
|
5487
|
+
}
|
|
5488
|
+
if (cursor) {
|
|
5489
|
+
filters += ` AND e.id < ?`;
|
|
5490
|
+
args.push(cursor[0]);
|
|
5491
|
+
}
|
|
5492
|
+
args.push(...noteVisible.args, limit + 1);
|
|
5493
|
+
const rows = await this.#query(this.#read, `SELECT e.* FROM ${T.activity} e JOIN ${T.pages} p ON p.id = e.pageId WHERE ${readable.sql}${filters} AND (e.noteId IS NULL OR EXISTS (SELECT 1 FROM ${T.notes} n WHERE n.id = e.noteId AND ${noteVisible.sql})) ORDER BY e.id DESC LIMIT ?`, args);
|
|
5494
|
+
const page = rows.slice(0, limit);
|
|
5495
|
+
return {
|
|
5496
|
+
events: page.map((row) => ({
|
|
5497
|
+
id: String(row.id),
|
|
5498
|
+
action: String(row.action),
|
|
5499
|
+
scopeId: String(row.scopeId),
|
|
5500
|
+
pageId: String(row.pageId),
|
|
5501
|
+
...row.noteId ? { noteId: String(row.noteId) } : {},
|
|
5502
|
+
provenance: provenance(row.provenance),
|
|
5503
|
+
createdAt: new Date(String(row.createdAt))
|
|
5504
|
+
})),
|
|
5505
|
+
...rows.length > limit ? { nextCursor: encodeCursor([String(page[page.length - 1].id)]) } : {}
|
|
5506
|
+
};
|
|
4838
5507
|
}
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
args.push(...keys);
|
|
4850
|
-
}
|
|
4851
|
-
args.push(input.limit ?? 100);
|
|
4852
|
-
return (await this.#client.execute({
|
|
4853
|
-
sql: `SELECT *,scope AS scopeJson FROM "${TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}"${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY createdAt ASC,id ASC LIMIT ?`,
|
|
4854
|
-
args
|
|
4855
|
-
})).rows.map(parseOutbox);
|
|
5508
|
+
/** Rewrites the audience index rows of one pin to match `audience`. */
|
|
5509
|
+
async #indexAudience(tx, pinId, audience) {
|
|
5510
|
+
await tx.execute({
|
|
5511
|
+
sql: `DELETE FROM ${T.pinAudiences} WHERE pinId = ?`,
|
|
5512
|
+
args: [pinId]
|
|
5513
|
+
});
|
|
5514
|
+
for (const entry of audience) await tx.execute({
|
|
5515
|
+
sql: `INSERT IGNORE INTO ${T.pinAudiences} (audience, pinId) VALUES (?, ?)`,
|
|
5516
|
+
args: [entry, pinId]
|
|
5517
|
+
});
|
|
4856
5518
|
}
|
|
4857
|
-
|
|
4858
|
-
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
4859
|
-
const stale = new Date(now.getTime() - (input.claimTimeoutMs ?? 6e4));
|
|
5519
|
+
addPinAudience(input) {
|
|
4860
5520
|
return this.#transaction(async (tx) => {
|
|
4861
|
-
const
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
input.
|
|
4882
|
-
|
|
4883
|
-
]
|
|
4884
|
-
}
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
5521
|
+
const page = await this.#livePage(tx, input.pageId);
|
|
5522
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5523
|
+
const existing = (await this.#query(tx, `SELECT * FROM ${T.pins} WHERE pageId = ?`, [input.pageId]))[0];
|
|
5524
|
+
if (!existing) {
|
|
5525
|
+
const id = createKnowledgeUlid();
|
|
5526
|
+
await tx.execute({
|
|
5527
|
+
sql: `INSERT INTO ${T.pins} (id, pageId, audience, createdBy, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
5528
|
+
args: [
|
|
5529
|
+
id,
|
|
5530
|
+
input.pageId,
|
|
5531
|
+
JSON.stringify([...new Set(input.audience)]),
|
|
5532
|
+
input.createdBy,
|
|
5533
|
+
now,
|
|
5534
|
+
now
|
|
5535
|
+
]
|
|
5536
|
+
});
|
|
5537
|
+
await this.#indexAudience(tx, id, [...new Set(input.audience)]);
|
|
5538
|
+
await this.#logActivity(tx, "pin-added", {
|
|
5539
|
+
scopeId: String(page.scopeId),
|
|
5540
|
+
pageId: input.pageId,
|
|
5541
|
+
provenance: { author: input.createdBy }
|
|
5542
|
+
}, now);
|
|
5543
|
+
return toPin((await this.#query(tx, `SELECT * FROM ${T.pins} WHERE id = ?`, [id]))[0]);
|
|
5544
|
+
}
|
|
5545
|
+
const current = parseJsonList(existing.audience);
|
|
5546
|
+
const merged = [.../* @__PURE__ */ new Set([...current, ...input.audience])];
|
|
5547
|
+
if (merged.length !== current.length) {
|
|
5548
|
+
await tx.execute({
|
|
5549
|
+
sql: `UPDATE ${T.pins} SET audience = ?, updatedAt = ? WHERE id = ?`,
|
|
5550
|
+
args: [
|
|
5551
|
+
JSON.stringify(merged),
|
|
5552
|
+
now,
|
|
5553
|
+
String(existing.id)
|
|
5554
|
+
]
|
|
5555
|
+
});
|
|
5556
|
+
await this.#indexAudience(tx, String(existing.id), merged);
|
|
5557
|
+
await this.#logActivity(tx, "pin-added", {
|
|
5558
|
+
scopeId: String(page.scopeId),
|
|
5559
|
+
pageId: input.pageId,
|
|
5560
|
+
provenance: { author: input.createdBy }
|
|
5561
|
+
}, now);
|
|
5562
|
+
}
|
|
5563
|
+
return toPin((await this.#query(tx, `SELECT * FROM ${T.pins} WHERE id = ?`, [String(existing.id)]))[0]);
|
|
4890
5564
|
});
|
|
4891
5565
|
}
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
5566
|
+
removePinAudience(input) {
|
|
5567
|
+
return this.#transaction(async (tx) => {
|
|
5568
|
+
const existing = (await this.#query(tx, `SELECT * FROM ${T.pins} WHERE pageId = ?`, [input.pageId]))[0];
|
|
5569
|
+
if (!existing) return null;
|
|
5570
|
+
const current = parseJsonList(existing.audience);
|
|
5571
|
+
const remove = new Set(input.audience);
|
|
5572
|
+
const remaining = current.filter((entry) => !remove.has(entry));
|
|
5573
|
+
if (remaining.length === current.length) return toPin(existing);
|
|
5574
|
+
const page = await this.#pageRow(tx, input.pageId);
|
|
5575
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5576
|
+
if (page) await this.#logActivity(tx, "pin-removed", {
|
|
5577
|
+
scopeId: String(page.scopeId),
|
|
5578
|
+
pageId: input.pageId,
|
|
5579
|
+
provenance: { author: input.removedBy }
|
|
5580
|
+
}, now);
|
|
5581
|
+
if (remaining.length === 0) {
|
|
5582
|
+
await this.#indexAudience(tx, String(existing.id), []);
|
|
5583
|
+
await tx.execute({
|
|
5584
|
+
sql: `DELETE FROM ${T.pins} WHERE id = ?`,
|
|
5585
|
+
args: [String(existing.id)]
|
|
5586
|
+
});
|
|
5587
|
+
return null;
|
|
5588
|
+
}
|
|
5589
|
+
await tx.execute({
|
|
5590
|
+
sql: `UPDATE ${T.pins} SET audience = ?, updatedAt = ? WHERE id = ?`,
|
|
4898
5591
|
args: [
|
|
5592
|
+
JSON.stringify(remaining),
|
|
4899
5593
|
now,
|
|
4900
|
-
id
|
|
4901
|
-
input.workerId
|
|
5594
|
+
String(existing.id)
|
|
4902
5595
|
]
|
|
4903
5596
|
});
|
|
5597
|
+
await this.#indexAudience(tx, String(existing.id), remaining);
|
|
5598
|
+
return toPin((await this.#query(tx, `SELECT * FROM ${T.pins} WHERE id = ?`, [String(existing.id)]))[0]);
|
|
4904
5599
|
});
|
|
4905
5600
|
}
|
|
4906
|
-
async
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
5601
|
+
async listPins(input) {
|
|
5602
|
+
const core = await loadPagesCore();
|
|
5603
|
+
const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
|
|
5604
|
+
const cursor = decodeCursor(input.cursor, 2);
|
|
5605
|
+
const readable = pageReadable("p", input.read);
|
|
5606
|
+
const args = [...readable.args];
|
|
5607
|
+
let filters = "";
|
|
5608
|
+
if (input.audience !== "all") {
|
|
5609
|
+
filters += ` AND pin.id IN (SELECT pa.pinId FROM (SELECT jv.v FROM JSON_TABLE(?, '$[*]' COLUMNS (v VARCHAR(512) ${BIN} PATH '$')) jv) aud STRAIGHT_JOIN ${T.pinAudiences} pa ON pa.audience = aud.v)`;
|
|
5610
|
+
args.push(jsonList(input.audience));
|
|
5611
|
+
}
|
|
5612
|
+
if (input.pageIds) {
|
|
5613
|
+
filters += ` AND pin.pageId IN ${JV}`;
|
|
5614
|
+
args.push(jsonList(input.pageIds));
|
|
5615
|
+
}
|
|
5616
|
+
if (input.excludePageIds?.length) {
|
|
5617
|
+
filters += ` AND pin.pageId NOT IN ${JV}`;
|
|
5618
|
+
args.push(jsonList(input.excludePageIds));
|
|
5619
|
+
}
|
|
5620
|
+
if (input.withBody) filters += ` AND REGEXP_LIKE(p.body, '[^[:space:]]')`;
|
|
5621
|
+
const from = `FROM ${T.pins} pin JOIN ${T.pages} p ON p.id = pin.pageId WHERE p.deletedAt IS NULL AND ${readable.sql}${filters}`;
|
|
5622
|
+
let total;
|
|
5623
|
+
if (input.count) total = Number((await this.#query(this.#read, `SELECT COUNT(*) AS n ${from}`, args))[0].n);
|
|
5624
|
+
const pageArgs = [...args];
|
|
5625
|
+
let cursorFilter = "";
|
|
5626
|
+
if (cursor) {
|
|
5627
|
+
cursorFilter = ` AND (pin.createdAt < ? OR (pin.createdAt = ? AND pin.id < ?))`;
|
|
5628
|
+
pageArgs.push(cursor[0], cursor[0], cursor[1]);
|
|
5629
|
+
}
|
|
5630
|
+
pageArgs.push(limit + 1);
|
|
5631
|
+
const rows = await this.#query(this.#read, `SELECT pin.*, p.name AS pageName ${from}${cursorFilter} ORDER BY pin.createdAt DESC, pin.id DESC LIMIT ?`, pageArgs);
|
|
5632
|
+
const page = rows.slice(0, limit);
|
|
5633
|
+
return {
|
|
5634
|
+
pins: page.map(toListedPin),
|
|
5635
|
+
...rows.length > limit ? { nextCursor: encodeCursor([String(page[page.length - 1].createdAt), String(page[page.length - 1].id)]) } : {},
|
|
5636
|
+
...total === void 0 ? {} : { total }
|
|
5637
|
+
};
|
|
5638
|
+
}
|
|
5639
|
+
};
|
|
5640
|
+
/** mysql2 returns JSON columns already parsed; accept the raw string form too. */
|
|
5641
|
+
function parseJsonList(value) {
|
|
5642
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
5643
|
+
}
|
|
5644
|
+
// src/storage/domains/knowledge/index.ts
|
|
5645
|
+
var KnowledgeMySQL = class extends KnowledgeStorage {
|
|
5646
|
+
static getExportDDL() {
|
|
5647
|
+
return KNOWLEDGE_PAGE_DDL.map((statement) => `${statement};`);
|
|
5648
|
+
}
|
|
5649
|
+
#pool;
|
|
5650
|
+
#pages;
|
|
5651
|
+
constructor({ pool }) {
|
|
5652
|
+
super();
|
|
5653
|
+
this.#pool = pool;
|
|
5654
|
+
const rawExecutor = (client) => ({ async execute(statement) {
|
|
5655
|
+
const [result] = await client.query(statement.sql, statement.args ?? []);
|
|
5656
|
+
return { rows: Array.isArray(result) ? result : [] };
|
|
5657
|
+
} });
|
|
5658
|
+
this.#pages = new MySQLKnowledgePages({
|
|
5659
|
+
read: rawExecutor(pool),
|
|
5660
|
+
transaction: async (operation) => {
|
|
5661
|
+
const connection = await pool.getConnection();
|
|
5662
|
+
try {
|
|
5663
|
+
await connection.beginTransaction();
|
|
5664
|
+
const result = await operation(rawExecutor(connection));
|
|
5665
|
+
await connection.commit();
|
|
5666
|
+
return result;
|
|
5667
|
+
} catch (error) {
|
|
5668
|
+
await connection.rollback();
|
|
5669
|
+
throw error;
|
|
5670
|
+
} finally {
|
|
5671
|
+
connection.release();
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
4917
5674
|
});
|
|
4918
5675
|
}
|
|
4919
|
-
|
|
5676
|
+
getCapabilities() {
|
|
5677
|
+
return {
|
|
5678
|
+
contractVersion: 4,
|
|
5679
|
+
schemaVersion: 4,
|
|
5680
|
+
supportsSchemaInspection: true,
|
|
5681
|
+
supportsExplicitReset: true
|
|
5682
|
+
};
|
|
5683
|
+
}
|
|
5684
|
+
async #inspectPages(client = this.#pool) {
|
|
5685
|
+
return inspectKnowledgePagesSchema(await this.#pagesSnapshot(client));
|
|
5686
|
+
}
|
|
5687
|
+
async #pagesSnapshot(client) {
|
|
5688
|
+
const exec = async (sql) => {
|
|
5689
|
+
const [rows] = await client.query(sql);
|
|
5690
|
+
return rows;
|
|
5691
|
+
};
|
|
5692
|
+
const tableNames = (await exec("SELECT table_name AS name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE 'mastra\\_knowledge\\_%'")).map((row) => String(row.name));
|
|
5693
|
+
let schemaVersion;
|
|
5694
|
+
if (tableNames.includes("mastra_knowledge_access_state")) {
|
|
5695
|
+
const state = await exec(`SELECT schemaVersion FROM \`${TABLE_KNOWLEDGE_PAGES_STATE}\` WHERE id = 'knowledge'`);
|
|
5696
|
+
if (state[0]) schemaVersion = Number(state[0].schemaVersion);
|
|
5697
|
+
}
|
|
5698
|
+
return {
|
|
5699
|
+
tableNames,
|
|
5700
|
+
schemaVersion
|
|
5701
|
+
};
|
|
5702
|
+
}
|
|
5703
|
+
async inspectSchema() {
|
|
5704
|
+
return this.#inspectPages();
|
|
5705
|
+
}
|
|
5706
|
+
/** Runs the earlier-alpha upgrades under a named lock so processes starting together don't race their ALTERs. */
|
|
5707
|
+
async #upgrade() {
|
|
4920
5708
|
const connection = await this.#pool.getConnection();
|
|
4921
5709
|
try {
|
|
4922
|
-
await connection.
|
|
4923
|
-
const
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
await connection.rollback();
|
|
4928
|
-
throw error;
|
|
5710
|
+
await connection.query("SELECT GET_LOCK('mastra_knowledge_upgrade', 60)");
|
|
5711
|
+
for (const upgrade of KNOWLEDGE_PAGE_UPGRADES) {
|
|
5712
|
+
const [rows] = await connection.query(upgrade.needed, upgrade.args);
|
|
5713
|
+
if (rows.length > 0) for (const statement of upgrade.apply) await connection.query(statement);
|
|
5714
|
+
}
|
|
4929
5715
|
} finally {
|
|
5716
|
+
await connection.query("SELECT RELEASE_LOCK('mastra_knowledge_upgrade')").catch(() => {});
|
|
4930
5717
|
connection.release();
|
|
4931
5718
|
}
|
|
4932
5719
|
}
|
|
4933
|
-
async
|
|
4934
|
-
const
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
}
|
|
4947
|
-
async #resolveNode(executor, name, scope) {
|
|
4948
|
-
const keys = visibleScopeKeys(scope);
|
|
4949
|
-
const candidates = (await executor.execute({
|
|
4950
|
-
sql: `SELECT *,scope AS scopeJson FROM "${TABLE_KNOWLEDGE_NODES}" WHERE type='node' AND canonicalName=? AND (scopeKey IN (${keys.map(() => "?").join(",")}) OR mergedInto IS NOT NULL)`,
|
|
4951
|
-
args: [canonicalName(name), ...keys]
|
|
4952
|
-
})).rows.map(parseNode).sort((left, right) => right.scope.length - left.scope.length);
|
|
4953
|
-
for (const candidate of candidates) {
|
|
4954
|
-
const terminal = await this.#resolveTerminalNode(executor, candidate.id);
|
|
4955
|
-
if (terminal && isKnowledgeScopeVisible(terminal.scope, scope)) return terminal;
|
|
5720
|
+
async init() {
|
|
5721
|
+
const connection = await this.#pool.getConnection();
|
|
5722
|
+
try {
|
|
5723
|
+
const [locked] = await connection.query("SELECT GET_LOCK('mastra_knowledge_init', 60) AS ok");
|
|
5724
|
+
if (Number(locked[0]?.ok) !== 1) throw new Error("Timed out waiting for the Knowledge schema lock");
|
|
5725
|
+
for (const table of legacyKnowledgeTablesToDrop(await this.#pagesSnapshot(connection))) await connection.query(`DROP TABLE IF EXISTS \`${table}\``);
|
|
5726
|
+
const inspection = await this.#inspectPages(connection);
|
|
5727
|
+
if (inspection.status === "incompatible-reset-required") throw new KnowledgeSchemaResetRequiredError(inspection);
|
|
5728
|
+
for (const statement of KNOWLEDGE_PAGE_DDL) await connection.query(statement);
|
|
5729
|
+
await connection.query(`INSERT IGNORE INTO \`${TABLE_KNOWLEDGE_PAGES_STATE}\` (id, schemaVersion) VALUES ('knowledge', ?)`, [4]);
|
|
5730
|
+
} finally {
|
|
5731
|
+
await connection.query("SELECT RELEASE_LOCK('mastra_knowledge_init')").catch(() => {});
|
|
5732
|
+
connection.release();
|
|
4956
5733
|
}
|
|
4957
|
-
|
|
5734
|
+
await this.#upgrade();
|
|
4958
5735
|
}
|
|
4959
|
-
async
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
while (node?.mergedInto) {
|
|
4963
|
-
if (seen.has(node.id)) throw new Error(`Knowledge merge cycle detected at ${node.id}`);
|
|
4964
|
-
seen.add(node.id);
|
|
4965
|
-
node = await this.#getNode(executor, node.mergedInto);
|
|
4966
|
-
}
|
|
4967
|
-
return node;
|
|
5736
|
+
async dangerouslyReset() {
|
|
5737
|
+
for (const table of [...KNOWLEDGE_RESETTABLE_TABLE_NAMES, "mastra_knowledge_pin_audiences"]) await this.#pool.query(`DROP TABLE IF EXISTS \`${table}\``);
|
|
5738
|
+
await this.init();
|
|
4968
5739
|
}
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
sql: `SELECT *,scope AS scopeJson FROM "${TABLE_KNOWLEDGE_RECORDS}" WHERE id=?${includeDeleted ? "" : " AND deletedAt IS NULL"}`,
|
|
4972
|
-
args: [id]
|
|
4973
|
-
});
|
|
4974
|
-
return result.rows[0] ? parseKnowledge(result.rows[0]) : null;
|
|
5740
|
+
materializeScope(input) {
|
|
5741
|
+
return this.#pages.materializeScope(input);
|
|
4975
5742
|
}
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
const node = await this.#resolveTerminalNode(this.#client, nodeReferenceId(input.node));
|
|
4979
|
-
if (!node) return { records: [] };
|
|
4980
|
-
const keys = visibleScopeKeys(scope);
|
|
4981
|
-
const args = [
|
|
4982
|
-
node.id,
|
|
4983
|
-
...relationship === "related" ? [node.id] : [],
|
|
4984
|
-
...keys
|
|
4985
|
-
];
|
|
4986
|
-
if (input.after) args.push(input.after);
|
|
4987
|
-
args.push((input.limit ?? 100) + 1);
|
|
4988
|
-
const records = (await this.#client.execute({
|
|
4989
|
-
sql: `SELECT DISTINCT f.*,f.scope AS scopeJson FROM "${TABLE_KNOWLEDGE_RECORDS}" f${relationship === "about" ? "" : ` LEFT JOIN "${TABLE_KNOWLEDGE_MENTIONS}" m ON m.sourceType='record' AND m.sourceId=f.id`} WHERE ${relationship === "about" ? "f.node=?" : relationship === "mentioning" ? "m.recordId=?" : "(f.node=? OR m.recordId=?)"} AND ${visibleSql(keys, "f.scopeKey")}${input.includeDeleted ? "" : " AND f.deletedAt IS NULL"}${input.after ? " AND f.id < ?" : ""} ORDER BY f.id DESC LIMIT ?`,
|
|
4990
|
-
args
|
|
4991
|
-
})).rows.map(parseKnowledge);
|
|
4992
|
-
const limit = input.limit ?? 100;
|
|
4993
|
-
return {
|
|
4994
|
-
records: records.slice(0, limit),
|
|
4995
|
-
nextCursor: records.length > limit ? records[limit - 1]?.id : void 0
|
|
4996
|
-
};
|
|
5743
|
+
listScopeDeclarations() {
|
|
5744
|
+
return this.#pages.listScopeDeclarations();
|
|
4997
5745
|
}
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
sql: `DELETE FROM "${TABLE_KNOWLEDGE_MENTIONS}" WHERE sourceType=? AND sourceId=?`,
|
|
5001
|
-
args: [sourceType, sourceId]
|
|
5002
|
-
});
|
|
5003
|
-
for (const name of parseKnowledgeWikilinks(text)) {
|
|
5004
|
-
let node = await this.#resolveNode(tx, name, resolutionScope);
|
|
5005
|
-
if (!node) {
|
|
5006
|
-
node = await this.#getNodeByName(tx, name, defaultScope);
|
|
5007
|
-
if (node) node = await this.#resolveTerminalNode(tx, node.id);
|
|
5008
|
-
if (!node) {
|
|
5009
|
-
const now = /* @__PURE__ */ new Date();
|
|
5010
|
-
node = {
|
|
5011
|
-
id: crypto.randomUUID(),
|
|
5012
|
-
type: "node",
|
|
5013
|
-
name,
|
|
5014
|
-
kind: "node",
|
|
5015
|
-
scope: defaultScope,
|
|
5016
|
-
version: 1,
|
|
5017
|
-
createdAt: now,
|
|
5018
|
-
updatedAt: now
|
|
5019
|
-
};
|
|
5020
|
-
await tx.execute({
|
|
5021
|
-
sql: `INSERT INTO "${TABLE_KNOWLEDGE_NODES}" (id,type,name,canonicalName,kind,content,scope,scopeKey,version,mergedInto,createdAt,updatedAt) VALUES (?,?,?,?,?,NULL,jsonb(?),?,?,NULL,?,?)`,
|
|
5022
|
-
args: [
|
|
5023
|
-
node.id,
|
|
5024
|
-
"node",
|
|
5025
|
-
node.name,
|
|
5026
|
-
canonicalName(node.name),
|
|
5027
|
-
node.kind,
|
|
5028
|
-
JSON.stringify(defaultScope),
|
|
5029
|
-
knowledgeScopeKey(defaultScope),
|
|
5030
|
-
1,
|
|
5031
|
-
now.toISOString()
|
|
5032
|
-
]
|
|
5033
|
-
});
|
|
5034
|
-
await this.#activity(tx, "node-created", "node", node.id, defaultScope);
|
|
5035
|
-
await this.#outbox(tx, "node", node.id, "upsert", 1, defaultScope);
|
|
5036
|
-
}
|
|
5037
|
-
}
|
|
5038
|
-
await tx.execute({
|
|
5039
|
-
sql: `INSERT OR IGNORE INTO "${TABLE_KNOWLEDGE_MENTIONS}" (sourceType,sourceId,recordId) VALUES (?,?,?)`,
|
|
5040
|
-
args: [
|
|
5041
|
-
sourceType,
|
|
5042
|
-
sourceId,
|
|
5043
|
-
node.id
|
|
5044
|
-
]
|
|
5045
|
-
});
|
|
5046
|
-
}
|
|
5746
|
+
renameScopeDeclaration(input) {
|
|
5747
|
+
return this.#pages.renameScopeDeclaration(input);
|
|
5047
5748
|
}
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
await executor.execute({
|
|
5051
|
-
sql: `INSERT INTO "${TABLE_KNOWLEDGE_ACTIVITY}" (id,action,recordType,recordId,scope,scopeKey,sourceThreadId,createdAt) VALUES (?,?,?,?,jsonb(?),?,?,?)`,
|
|
5052
|
-
args: [
|
|
5053
|
-
createKnowledgeUlid(),
|
|
5054
|
-
action,
|
|
5055
|
-
recordType,
|
|
5056
|
-
recordId,
|
|
5057
|
-
JSON.stringify(scope),
|
|
5058
|
-
knowledgeScopeKey(scope),
|
|
5059
|
-
sourceThreadId ?? null,
|
|
5060
|
-
now.toISOString()
|
|
5061
|
-
]
|
|
5062
|
-
});
|
|
5749
|
+
getScopesByAddress(input) {
|
|
5750
|
+
return this.#pages.getScopesByAddress(input);
|
|
5063
5751
|
}
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5752
|
+
listChildScopes(input) {
|
|
5753
|
+
return this.#pages.listChildScopes(input);
|
|
5754
|
+
}
|
|
5755
|
+
getPage(input) {
|
|
5756
|
+
return this.#pages.getPage(input);
|
|
5757
|
+
}
|
|
5758
|
+
getPageByPath(input) {
|
|
5759
|
+
return this.#pages.getPageByPath(input);
|
|
5760
|
+
}
|
|
5761
|
+
findPagesByName(input) {
|
|
5762
|
+
return this.#pages.findPagesByName(input);
|
|
5763
|
+
}
|
|
5764
|
+
listChildPages(input) {
|
|
5765
|
+
return this.#pages.listChildPages(input);
|
|
5766
|
+
}
|
|
5767
|
+
listPages(input) {
|
|
5768
|
+
return this.#pages.listPages(input);
|
|
5769
|
+
}
|
|
5770
|
+
createPage(input) {
|
|
5771
|
+
return this.#pages.createPage(input);
|
|
5772
|
+
}
|
|
5773
|
+
updatePage(input) {
|
|
5774
|
+
return this.#pages.updatePage(input);
|
|
5775
|
+
}
|
|
5776
|
+
movePage(input) {
|
|
5777
|
+
return this.#pages.movePage(input);
|
|
5778
|
+
}
|
|
5779
|
+
mergePages(input) {
|
|
5780
|
+
return this.#pages.mergePages(input);
|
|
5781
|
+
}
|
|
5782
|
+
deletePage(input) {
|
|
5783
|
+
return this.#pages.deletePage(input);
|
|
5784
|
+
}
|
|
5785
|
+
getNote(input) {
|
|
5786
|
+
return this.#pages.getNote(input);
|
|
5787
|
+
}
|
|
5788
|
+
listNotes(input) {
|
|
5789
|
+
return this.#pages.listNotes(input);
|
|
5790
|
+
}
|
|
5791
|
+
countNotes(input) {
|
|
5792
|
+
return this.#pages.countNotes(input);
|
|
5793
|
+
}
|
|
5794
|
+
listNotesMentioning(input) {
|
|
5795
|
+
return this.#pages.listNotesMentioning(input);
|
|
5796
|
+
}
|
|
5797
|
+
addNote(input) {
|
|
5798
|
+
return this.#pages.addNote(input);
|
|
5799
|
+
}
|
|
5800
|
+
restampNote(input) {
|
|
5801
|
+
return this.#pages.restampNote(input);
|
|
5802
|
+
}
|
|
5803
|
+
removeNote(input) {
|
|
5804
|
+
return this.#pages.removeNote(input);
|
|
5805
|
+
}
|
|
5806
|
+
restoreNote(input) {
|
|
5807
|
+
return this.#pages.restoreNote(input);
|
|
5808
|
+
}
|
|
5809
|
+
searchPages(input) {
|
|
5810
|
+
return this.#pages.searchPages(input);
|
|
5811
|
+
}
|
|
5812
|
+
listPageActivity(input) {
|
|
5813
|
+
return this.#pages.listPageActivity(input);
|
|
5814
|
+
}
|
|
5815
|
+
addPinAudience(input) {
|
|
5816
|
+
return this.#pages.addPinAudience(input);
|
|
5817
|
+
}
|
|
5818
|
+
removePinAudience(input) {
|
|
5819
|
+
return this.#pages.removePinAudience(input);
|
|
5820
|
+
}
|
|
5821
|
+
getPages(input) {
|
|
5822
|
+
return this.#pages.getPages(input);
|
|
5823
|
+
}
|
|
5824
|
+
listPins(input) {
|
|
5825
|
+
return this.#pages.listPins(input);
|
|
5826
|
+
}
|
|
5827
|
+
async dangerouslyClearAll() {
|
|
5828
|
+
await this.#pages.clearAll();
|
|
5082
5829
|
}
|
|
5083
5830
|
};
|
|
5084
5831
|
// src/storage/domains/mcp-clients/index.ts
|