@openparachute/vault 0.3.3 → 0.4.3
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/.parachute/module.json +15 -0
- package/README.md +133 -0
- package/core/src/core.test.ts +2990 -92
- package/core/src/links.ts +1 -1
- package/core/src/mcp.ts +413 -68
- package/core/src/notes.ts +693 -42
- package/core/src/obsidian.ts +3 -3
- package/core/src/paths.ts +1 -1
- package/core/src/query-operators.ts +23 -7
- package/core/src/schema-defaults.ts +331 -0
- package/core/src/schema.ts +467 -11
- package/core/src/store.ts +262 -8
- package/core/src/tag-hierarchy.ts +171 -0
- package/core/src/tag-schemas.ts +242 -42
- package/core/src/types.ts +96 -7
- package/core/src/vault-projection.ts +309 -0
- package/core/src/wikilinks.ts +3 -3
- package/package.json +13 -3
- package/src/admin-spa.test.ts +161 -0
- package/src/admin-spa.ts +161 -0
- package/src/auth-hub-jwt.test.ts +360 -0
- package/src/auth-status.ts +84 -0
- package/src/auth.test.ts +135 -23
- package/src/auth.ts +173 -15
- package/src/backup.ts +4 -7
- package/src/cli.ts +322 -57
- package/src/config.test.ts +44 -0
- package/src/config.ts +68 -40
- package/src/hub-jwt.test.ts +307 -0
- package/src/hub-jwt.ts +88 -0
- package/src/init.test.ts +216 -0
- package/src/mcp-http.ts +33 -29
- package/src/mcp-install.ts +1 -1
- package/src/mcp-tools.ts +318 -19
- package/src/module-config.ts +1 -1
- package/src/oauth.test.ts +345 -0
- package/src/oauth.ts +85 -14
- package/src/owner-auth.ts +57 -1
- package/src/prompt.ts +6 -5
- package/src/routes.ts +796 -61
- package/src/routing.test.ts +466 -1
- package/src/routing.ts +106 -24
- package/src/scopes.test.ts +66 -8
- package/src/scopes.ts +163 -37
- package/src/server.ts +24 -2
- package/src/services-manifest.test.ts +20 -0
- package/src/services-manifest.ts +9 -2
- package/src/stop-signal.test.ts +85 -0
- package/src/storage.test.ts +92 -0
- package/src/tag-scope.ts +118 -0
- package/src/token-store.test.ts +47 -0
- package/src/token-store.ts +128 -13
- package/src/tokens-routes.test.ts +727 -0
- package/src/tokens-routes.ts +392 -0
- package/src/transcription-worker.test.ts +5 -0
- package/src/triggers.ts +1 -1
- package/src/two-factor.ts +2 -2
- package/src/vault-create.test.ts +193 -0
- package/src/vault-name.test.ts +123 -0
- package/src/vault-name.ts +80 -0
- package/src/vault.test.ts +1626 -183
- package/tsconfig.json +8 -1
- package/.claude/settings.local.json +0 -8
- package/.dockerignore +0 -8
- package/.env.example +0 -9
- package/CHANGELOG.md +0 -175
- package/CLAUDE.md +0 -125
- package/Caddyfile +0 -3
- package/Dockerfile +0 -22
- package/bun.lock +0 -219
- package/bunfig.toml +0 -2
- package/deploy/parachute-vault.service +0 -20
- package/docker-compose.yml +0 -50
- package/docs/HTTP_API.md +0 -434
- package/docs/auth-model.md +0 -340
- package/fly.toml +0 -24
- package/package/package.json +0 -32
- package/railway.json +0 -14
- package/scripts/migrate-audio-to-opus.test.ts +0 -237
- package/scripts/migrate-audio-to-opus.ts +0 -499
package/core/src/tag-schemas.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tag
|
|
2
|
+
* Tag record CRUD — DB-backed storage for the per-tag identity row.
|
|
3
3
|
*
|
|
4
|
-
* Each tag
|
|
5
|
-
* fields
|
|
6
|
-
* and
|
|
4
|
+
* Each tag row carries: human-readable description, indexed metadata field
|
|
5
|
+
* declarations (`fields`), typed-link relationship declarations
|
|
6
|
+
* (`relationships`), and the hierarchy parent list (`parent_names`).
|
|
7
|
+
* See parachute-patterns/patterns/tag-data-model.md.
|
|
8
|
+
*
|
|
9
|
+
* This module retains the historical `tag-schemas` filename and exports
|
|
10
|
+
* (`TagSchema`, `listTagSchemas`, `getTagSchema`, `upsertTagSchema`,
|
|
11
|
+
* `deleteTagSchema`, `getTagSchemaMap`) as a thin schema-only facade —
|
|
12
|
+
* callers that only care about `description + fields` keep working without
|
|
13
|
+
* change. New surface (`TagRecord`, `listTagRecords`, `getTagRecord`,
|
|
14
|
+
* `upsertTagRecord`) returns the full row including relationships and
|
|
15
|
+
* parent_names.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
import { Database } from "bun:sqlite";
|
|
@@ -23,87 +32,278 @@ export interface TagFieldSchema {
|
|
|
23
32
|
indexed?: boolean;
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Cardinality vocabulary for typed relationships. Names rather than
|
|
37
|
+
* algebra so AI clients reading `list-tags` can reason about intent
|
|
38
|
+
* directly. Phase 1 is informational — declarations are not enforced
|
|
39
|
+
* at write time. See patterns/tag-data-model.md §Typed relationships.
|
|
40
|
+
*/
|
|
41
|
+
export type TagRelCardinality = "one" | "optional" | "many" | "many-required";
|
|
42
|
+
|
|
43
|
+
export const TAG_REL_CARDINALITIES: readonly TagRelCardinality[] = [
|
|
44
|
+
"one",
|
|
45
|
+
"optional",
|
|
46
|
+
"many",
|
|
47
|
+
"many-required",
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
export interface TagRelationship {
|
|
51
|
+
target_tag: string;
|
|
52
|
+
cardinality: TagRelCardinality;
|
|
53
|
+
description?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Schema-only view of a tag — the historical shape. Backwards-compatible
|
|
58
|
+
* with v13-and-earlier callers.
|
|
59
|
+
*/
|
|
26
60
|
export interface TagSchema {
|
|
27
61
|
tag: string;
|
|
28
62
|
description?: string;
|
|
29
63
|
fields?: Record<string, TagFieldSchema>;
|
|
30
64
|
}
|
|
31
65
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Full tag record — schema + typed relationships + hierarchy parents.
|
|
68
|
+
*/
|
|
69
|
+
export interface TagRecord extends TagSchema {
|
|
70
|
+
relationships?: Record<string, TagRelationship>;
|
|
71
|
+
parent_names?: string[];
|
|
72
|
+
created_at?: string;
|
|
73
|
+
updated_at?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// DB row shape (post-v14 `tags` table).
|
|
77
|
+
interface TagRow {
|
|
78
|
+
name: string;
|
|
35
79
|
description: string | null;
|
|
36
80
|
fields: string | null;
|
|
81
|
+
relationships: string | null;
|
|
82
|
+
parent_names: string | null;
|
|
83
|
+
created_at: string | null;
|
|
84
|
+
updated_at: string | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// CRUD — full record
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/** List all tag records, sorted by name. */
|
|
92
|
+
export function listTagRecords(db: Database): TagRecord[] {
|
|
93
|
+
const rows = db.prepare(
|
|
94
|
+
"SELECT name, description, fields, relationships, parent_names, created_at, updated_at FROM tags ORDER BY name",
|
|
95
|
+
).all() as TagRow[];
|
|
96
|
+
return rows.map(rowToRecord);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Get a single tag record, or null if the tag doesn't exist. */
|
|
100
|
+
export function getTagRecord(db: Database, tag: string): TagRecord | null {
|
|
101
|
+
const row = db.prepare(
|
|
102
|
+
"SELECT name, description, fields, relationships, parent_names, created_at, updated_at FROM tags WHERE name = ?",
|
|
103
|
+
).get(tag) as TagRow | undefined;
|
|
104
|
+
return row ? rowToRecord(row) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Upsert a tag record — partial update. Any field left `undefined` is
|
|
109
|
+
* preserved. Pass `null` explicitly to clear a column. Always touches
|
|
110
|
+
* `updated_at`; sets `created_at` on first insert.
|
|
111
|
+
*/
|
|
112
|
+
export function upsertTagRecord(
|
|
113
|
+
db: Database,
|
|
114
|
+
tag: string,
|
|
115
|
+
patch: {
|
|
116
|
+
description?: string | null;
|
|
117
|
+
fields?: Record<string, TagFieldSchema> | null;
|
|
118
|
+
relationships?: Record<string, TagRelationship> | null;
|
|
119
|
+
parent_names?: string[] | null;
|
|
120
|
+
},
|
|
121
|
+
): TagRecord {
|
|
122
|
+
const now = new Date().toISOString();
|
|
123
|
+
db.prepare(
|
|
124
|
+
"INSERT OR IGNORE INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)",
|
|
125
|
+
).run(tag, now, now);
|
|
126
|
+
|
|
127
|
+
const existing = getTagRecord(db, tag);
|
|
128
|
+
|
|
129
|
+
const description =
|
|
130
|
+
patch.description === undefined ? (existing?.description ?? null) : patch.description;
|
|
131
|
+
const fields =
|
|
132
|
+
patch.fields === undefined
|
|
133
|
+
? jsonOrNull(existing?.fields)
|
|
134
|
+
: jsonOrNull(patch.fields);
|
|
135
|
+
const relationships =
|
|
136
|
+
patch.relationships === undefined
|
|
137
|
+
? jsonOrNull(existing?.relationships)
|
|
138
|
+
: jsonOrNull(patch.relationships);
|
|
139
|
+
const parent_names =
|
|
140
|
+
patch.parent_names === undefined
|
|
141
|
+
? jsonOrNull(existing?.parent_names)
|
|
142
|
+
: jsonOrNull(patch.parent_names);
|
|
143
|
+
|
|
144
|
+
db.prepare(
|
|
145
|
+
`UPDATE tags
|
|
146
|
+
SET description = ?, fields = ?, relationships = ?, parent_names = ?, updated_at = ?
|
|
147
|
+
WHERE name = ?`,
|
|
148
|
+
).run(description, fields, relationships, parent_names, now, tag);
|
|
149
|
+
|
|
150
|
+
return getTagRecord(db, tag)!;
|
|
37
151
|
}
|
|
38
152
|
|
|
39
153
|
// ---------------------------------------------------------------------------
|
|
40
|
-
// CRUD
|
|
154
|
+
// CRUD — schema-only facade (back-compat)
|
|
41
155
|
// ---------------------------------------------------------------------------
|
|
42
156
|
|
|
43
|
-
/** List
|
|
157
|
+
/** List schema-only views for tags that have a description or fields set. */
|
|
44
158
|
export function listTagSchemas(db: Database): TagSchema[] {
|
|
45
|
-
const rows = db.prepare(
|
|
46
|
-
|
|
159
|
+
const rows = db.prepare(
|
|
160
|
+
"SELECT name, description, fields FROM tags WHERE description IS NOT NULL OR fields IS NOT NULL ORDER BY name",
|
|
161
|
+
).all() as { name: string; description: string | null; fields: string | null }[];
|
|
162
|
+
return rows.map((r) => ({
|
|
163
|
+
tag: r.name,
|
|
164
|
+
description: r.description ?? undefined,
|
|
165
|
+
fields: parseJson<Record<string, TagFieldSchema>>(r.fields),
|
|
166
|
+
}));
|
|
47
167
|
}
|
|
48
168
|
|
|
49
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* Schema-only view for a single tag. Returns null if the tag has neither
|
|
171
|
+
* a description nor fields (matches v13 behavior, where the absence of a
|
|
172
|
+
* `tag_schemas` row meant "no schema").
|
|
173
|
+
*/
|
|
50
174
|
export function getTagSchema(db: Database, tag: string): TagSchema | null {
|
|
51
|
-
const row = db.prepare(
|
|
52
|
-
|
|
175
|
+
const row = db.prepare(
|
|
176
|
+
"SELECT name, description, fields FROM tags WHERE name = ?",
|
|
177
|
+
).get(tag) as { name: string; description: string | null; fields: string | null } | undefined;
|
|
178
|
+
if (!row) return null;
|
|
179
|
+
if (row.description === null && row.fields === null) return null;
|
|
180
|
+
return {
|
|
181
|
+
tag: row.name,
|
|
182
|
+
description: row.description ?? undefined,
|
|
183
|
+
fields: parseJson<Record<string, TagFieldSchema>>(row.fields),
|
|
184
|
+
};
|
|
53
185
|
}
|
|
54
186
|
|
|
55
187
|
/** Get all schemas as a lookup map (tag → schema). Used by schema effects. */
|
|
56
|
-
export function getTagSchemaMap(
|
|
57
|
-
|
|
188
|
+
export function getTagSchemaMap(
|
|
189
|
+
db: Database,
|
|
190
|
+
): Record<string, { description?: string; fields?: Record<string, TagFieldSchema> }> {
|
|
58
191
|
const map: Record<string, { description?: string; fields?: Record<string, TagFieldSchema> }> = {};
|
|
59
|
-
for (const s of
|
|
192
|
+
for (const s of listTagSchemas(db)) {
|
|
60
193
|
map[s.tag] = { description: s.description, fields: s.fields };
|
|
61
194
|
}
|
|
62
195
|
return map;
|
|
63
196
|
}
|
|
64
197
|
|
|
65
198
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
199
|
+
* Set or replace a tag's schema (description + fields). Other columns
|
|
200
|
+
* (`relationships`, `parent_names`) are left untouched. Idempotent.
|
|
68
201
|
*/
|
|
69
202
|
export function upsertTagSchema(
|
|
70
203
|
db: Database,
|
|
71
204
|
tag: string,
|
|
72
205
|
schema: { description?: string; fields?: Record<string, TagFieldSchema> },
|
|
73
206
|
): TagSchema {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
db.
|
|
79
|
-
INSERT INTO tag_schemas (tag_name, description, fields)
|
|
80
|
-
VALUES (?, ?, ?)
|
|
81
|
-
ON CONFLICT(tag_name) DO UPDATE SET
|
|
82
|
-
description = excluded.description,
|
|
83
|
-
fields = excluded.fields
|
|
84
|
-
`).run(tag, schema.description ?? null, fieldsJson);
|
|
85
|
-
|
|
86
|
-
return getTagSchema(db, tag)!;
|
|
207
|
+
upsertTagRecord(db, tag, {
|
|
208
|
+
description: schema.description ?? null,
|
|
209
|
+
fields: schema.fields ?? null,
|
|
210
|
+
});
|
|
211
|
+
return getTagSchema(db, tag) ?? { tag, description: schema.description, fields: schema.fields };
|
|
87
212
|
}
|
|
88
213
|
|
|
89
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Clear a tag's schema (description + fields). Returns true if anything
|
|
216
|
+
* was cleared. Other columns and the tag row itself are preserved — to
|
|
217
|
+
* delete the tag entirely, use `noteOps.deleteTag`.
|
|
218
|
+
*/
|
|
90
219
|
export function deleteTagSchema(db: Database, tag: string): boolean {
|
|
91
|
-
const
|
|
92
|
-
|
|
220
|
+
const before = getTagSchema(db, tag);
|
|
221
|
+
if (!before) return false;
|
|
222
|
+
db.prepare(
|
|
223
|
+
"UPDATE tags SET description = NULL, fields = NULL, updated_at = ? WHERE name = ?",
|
|
224
|
+
).run(new Date().toISOString(), tag);
|
|
225
|
+
return true;
|
|
93
226
|
}
|
|
94
227
|
|
|
95
228
|
// ---------------------------------------------------------------------------
|
|
96
|
-
//
|
|
229
|
+
// Validation — typed relationships
|
|
97
230
|
// ---------------------------------------------------------------------------
|
|
98
231
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Validate a `relationships` payload before persisting. Returns the
|
|
234
|
+
* canonicalized object on success; throws Error with a user-readable
|
|
235
|
+
* message on the first violation. Rules:
|
|
236
|
+
*
|
|
237
|
+
* - Each value must declare `target_tag` (non-empty string) and
|
|
238
|
+
* `cardinality` from the named vocabulary.
|
|
239
|
+
* - `description` is optional, must be a string when present.
|
|
240
|
+
* - Relationship keys must be non-empty strings.
|
|
241
|
+
*/
|
|
242
|
+
export function validateRelationships(
|
|
243
|
+
raw: unknown,
|
|
244
|
+
): Record<string, TagRelationship> {
|
|
245
|
+
if (raw === null || raw === undefined) {
|
|
246
|
+
throw new Error("relationships: expected an object, got null/undefined");
|
|
247
|
+
}
|
|
248
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
249
|
+
throw new Error("relationships: expected an object mapping rel name → declaration");
|
|
103
250
|
}
|
|
251
|
+
const out: Record<string, TagRelationship> = {};
|
|
252
|
+
for (const [rel, decl] of Object.entries(raw as Record<string, unknown>)) {
|
|
253
|
+
if (!rel || typeof rel !== "string") {
|
|
254
|
+
throw new Error("relationships: keys must be non-empty strings");
|
|
255
|
+
}
|
|
256
|
+
if (!decl || typeof decl !== "object" || Array.isArray(decl)) {
|
|
257
|
+
throw new Error(`relationships["${rel}"]: declaration must be an object`);
|
|
258
|
+
}
|
|
259
|
+
const d = decl as Record<string, unknown>;
|
|
260
|
+
if (typeof d.target_tag !== "string" || d.target_tag.length === 0) {
|
|
261
|
+
throw new Error(`relationships["${rel}"]: target_tag must be a non-empty string`);
|
|
262
|
+
}
|
|
263
|
+
const card = d.cardinality;
|
|
264
|
+
if (typeof card !== "string" || !TAG_REL_CARDINALITIES.includes(card as TagRelCardinality)) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`relationships["${rel}"]: cardinality must be one of ${TAG_REL_CARDINALITIES.join(" | ")}; got ${JSON.stringify(card)}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (d.description !== undefined && typeof d.description !== "string") {
|
|
270
|
+
throw new Error(`relationships["${rel}"]: description must be a string when set`);
|
|
271
|
+
}
|
|
272
|
+
out[rel] = {
|
|
273
|
+
target_tag: d.target_tag,
|
|
274
|
+
cardinality: card as TagRelCardinality,
|
|
275
|
+
...(d.description !== undefined ? { description: d.description as string } : {}),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Helpers
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
function rowToRecord(row: TagRow): TagRecord {
|
|
104
286
|
return {
|
|
105
|
-
tag: row.
|
|
287
|
+
tag: row.name,
|
|
106
288
|
description: row.description ?? undefined,
|
|
107
|
-
fields,
|
|
289
|
+
fields: parseJson<Record<string, TagFieldSchema>>(row.fields),
|
|
290
|
+
relationships: parseJson<Record<string, TagRelationship>>(row.relationships),
|
|
291
|
+
parent_names: parseJson<string[]>(row.parent_names),
|
|
292
|
+
created_at: row.created_at ?? undefined,
|
|
293
|
+
updated_at: row.updated_at ?? undefined,
|
|
108
294
|
};
|
|
109
295
|
}
|
|
296
|
+
|
|
297
|
+
function parseJson<T>(raw: string | null): T | undefined {
|
|
298
|
+
if (raw === null || raw === undefined) return undefined;
|
|
299
|
+
try { return JSON.parse(raw) as T; } catch { return undefined; }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function jsonOrNull(value: unknown): string | null {
|
|
303
|
+
if (value === null || value === undefined) return null;
|
|
304
|
+
if (typeof value === "string") {
|
|
305
|
+
// Already-encoded payload (e.g. when copying from an existing row).
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
return JSON.stringify(value);
|
|
309
|
+
}
|
package/core/src/types.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import type { TagFieldSchema, TagRelationship, TagRecord } from "./tag-schemas.js";
|
|
2
|
+
|
|
3
|
+
// ---- Re-exports ----
|
|
4
|
+
|
|
5
|
+
export type { TagFieldSchema, TagRelationship, TagRecord } from "./tag-schemas.js";
|
|
6
|
+
|
|
1
7
|
// ---- Note ----
|
|
2
8
|
|
|
3
9
|
export interface Note {
|
|
@@ -41,6 +47,7 @@ export interface VaultStats {
|
|
|
41
47
|
notesByMonth: { month: string; count: number }[];
|
|
42
48
|
topTags: { tag: string; count: number }[];
|
|
43
49
|
tagCount: number;
|
|
50
|
+
attachmentCount: number;
|
|
44
51
|
linkCount: number;
|
|
45
52
|
}
|
|
46
53
|
|
|
@@ -57,14 +64,35 @@ export interface QueryOpts {
|
|
|
57
64
|
hasLinks?: boolean;
|
|
58
65
|
path?: string; // exact path match (case-insensitive)
|
|
59
66
|
pathPrefix?: string; // e.g., "Projects/Parachute" matches "Projects/Parachute/README"
|
|
67
|
+
// Restrict results to a specific set of note IDs. The MCP `near` query uses
|
|
68
|
+
// this to push graph-neighborhood scoping into the SQL WHERE clause so that
|
|
69
|
+
// LIMIT and ORDER BY apply to the filtered set, not the whole notes table.
|
|
70
|
+
// Empty array → no rows match (avoids `IN ()` syntax error).
|
|
71
|
+
ids?: string[];
|
|
60
72
|
// Per-field metadata filter. Each value is either a primitive (exact
|
|
61
73
|
// match, today's behavior) or an operator object — `{ eq, ne, gt, gte, lt,
|
|
62
74
|
// lte, in, not_in, exists }` — which routes through the generated column
|
|
63
75
|
// for the field. Operator queries require the field to be declared
|
|
64
76
|
// `indexed: true` in a tag schema; undeclared fields error loudly.
|
|
65
77
|
metadata?: Record<string, unknown>;
|
|
78
|
+
// Legacy shorthand: filters on `n.created_at` (vault ingestion time).
|
|
79
|
+
// Equivalent to `dateFilter: { field: "created_at", from, to }`. Kept
|
|
80
|
+
// as the common path; specifying both this and `dateFilter` rejects.
|
|
66
81
|
dateFrom?: string; // ISO date
|
|
67
82
|
dateTo?: string; // ISO date
|
|
83
|
+
// Generalized date range. `field` defaults to `created_at`; `updated_at`
|
|
84
|
+
// is also a recognized real column (the incremental-rebuild path —
|
|
85
|
+
// vault#285 1.5). Any other field must be declared `indexed: true` in a
|
|
86
|
+
// tag schema (so the SQL hits a real B-tree index, same contract as
|
|
87
|
+
// `metadata` operator queries and `orderBy`). Use this to filter on a
|
|
88
|
+
// *content* date — an email's received date, a meeting's scheduled
|
|
89
|
+
// date — rather than the ingestion timestamp, or on `updated_at` to ask
|
|
90
|
+
// "what changed since X."
|
|
91
|
+
dateFilter?: {
|
|
92
|
+
field?: string;
|
|
93
|
+
from?: string;
|
|
94
|
+
to?: string;
|
|
95
|
+
};
|
|
68
96
|
sort?: "asc" | "desc";
|
|
69
97
|
// Sort by an indexed metadata field instead of `created_at`. Must be
|
|
70
98
|
// declared `indexed: true`; errors loudly otherwise. Direction is taken
|
|
@@ -114,7 +142,7 @@ export interface Store {
|
|
|
114
142
|
getNote(id: string): Promise<Note | null>;
|
|
115
143
|
getNoteByPath(path: string): Promise<Note | null>;
|
|
116
144
|
getNotes(ids: string[]): Promise<Note[]>;
|
|
117
|
-
updateNote(id: string, updates: { content?: string; path?: string; metadata?: Record<string, unknown>; created_at?: string; skipUpdatedAt?: boolean; if_updated_at?: string }): Promise<Note>;
|
|
145
|
+
updateNote(id: string, updates: { content?: string; append?: string; prepend?: string; path?: string; metadata?: Record<string, unknown>; created_at?: string; skipUpdatedAt?: boolean; if_updated_at?: string }): Promise<Note>;
|
|
118
146
|
deleteNote(id: string): Promise<void>;
|
|
119
147
|
queryNotes(opts: QueryOpts): Promise<Note[]>;
|
|
120
148
|
searchNotes(query: string, opts?: { tags?: string[]; limit?: number }): Promise<Note[]>;
|
|
@@ -122,12 +150,31 @@ export interface Store {
|
|
|
122
150
|
// Tags
|
|
123
151
|
tagNote(noteId: string, tags: string[]): Promise<void>;
|
|
124
152
|
untagNote(noteId: string, tags: string[]): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* Expand a set of tag names to the union of `{tag} ∪ descendants(tag)` for
|
|
155
|
+
* each input, using the `_tags/<name>` config-note hierarchy. Always
|
|
156
|
+
* includes each input tag in the result. Used by tag-scoped tokens to
|
|
157
|
+
* compute the effective allowlisted tag-set at auth time.
|
|
158
|
+
*/
|
|
159
|
+
expandTagsWithDescendants(tags: string[]): Promise<Set<string>>;
|
|
125
160
|
listTags(): Promise<{ name: string; count: number }[]>;
|
|
126
161
|
deleteTag(name: string): Promise<{ deleted: boolean; notes_untagged: number }>;
|
|
127
162
|
renameTag(
|
|
128
163
|
oldName: string,
|
|
129
164
|
newName: string,
|
|
130
|
-
): Promise<
|
|
165
|
+
): Promise<
|
|
166
|
+
| {
|
|
167
|
+
renamed: number;
|
|
168
|
+
sub_tags_renamed: number;
|
|
169
|
+
parent_refs_updated: number;
|
|
170
|
+
tokens_updated: number;
|
|
171
|
+
indexed_field_declarers_updated: number;
|
|
172
|
+
notes_rewritten: number;
|
|
173
|
+
paths_renamed: number;
|
|
174
|
+
}
|
|
175
|
+
| { error: "not_found" }
|
|
176
|
+
| { error: "target_exists"; conflicting: string[] }
|
|
177
|
+
>;
|
|
131
178
|
mergeTags(
|
|
132
179
|
sources: string[],
|
|
133
180
|
target: string,
|
|
@@ -151,12 +198,54 @@ export interface Store {
|
|
|
151
198
|
traverseLinks(noteId: string, opts?: { max_depth?: number; relationship?: string }): Promise<{ noteId: string; depth: number; relationship: string; direction: "outbound" | "inbound" }[]>;
|
|
152
199
|
findPath(sourceId: string, targetId: string, opts?: { max_depth?: number }): Promise<{ path: string[]; relationships: string[] } | null>;
|
|
153
200
|
|
|
154
|
-
// Tag schemas
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
201
|
+
// Tag schemas — schema-only facade (description + fields). Back-compat
|
|
202
|
+
// surface for v13-and-earlier callers; reads/writes route through the
|
|
203
|
+
// post-v14 `tags` row directly.
|
|
204
|
+
listTagSchemas(): Promise<{ tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> }[]>;
|
|
205
|
+
getTagSchema(tag: string): Promise<{ tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> } | null>;
|
|
206
|
+
upsertTagSchema(tag: string, schema: { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> }): Promise<{ tag: string; description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> }>;
|
|
158
207
|
deleteTagSchema(tag: string): Promise<boolean>;
|
|
159
|
-
getTagSchemaMap(): Promise<Record<string, { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[] }> }>>;
|
|
208
|
+
getTagSchemaMap(): Promise<Record<string, { description?: string; fields?: Record<string, { type: string; description?: string; enum?: string[]; indexed?: boolean }> }>>;
|
|
209
|
+
|
|
210
|
+
// Tag records — full v14 identity row (description + fields + typed
|
|
211
|
+
// relationships + parent_names + timestamps). See
|
|
212
|
+
// parachute-patterns/patterns/tag-data-model.md.
|
|
213
|
+
listTagRecords(): Promise<TagRecord[]>;
|
|
214
|
+
getTagRecord(tag: string): Promise<TagRecord | null>;
|
|
215
|
+
/**
|
|
216
|
+
* Partial upsert. Any patch field left undefined is preserved; pass
|
|
217
|
+
* null to clear. Touching `parent_names` invalidates the tag-hierarchy
|
|
218
|
+
* cache. Returns the post-write row.
|
|
219
|
+
*/
|
|
220
|
+
upsertTagRecord(
|
|
221
|
+
tag: string,
|
|
222
|
+
patch: {
|
|
223
|
+
description?: string | null;
|
|
224
|
+
fields?: Record<string, TagFieldSchema> | null;
|
|
225
|
+
relationships?: Record<string, TagRelationship> | null;
|
|
226
|
+
parent_names?: string[] | null;
|
|
227
|
+
},
|
|
228
|
+
): Promise<TagRecord>;
|
|
229
|
+
|
|
230
|
+
// Schema validation (post-v17: backed by `tags.fields` only — the
|
|
231
|
+
// standalone note_schemas + schema_mappings subsystem retired in v17, see
|
|
232
|
+
// vault#267). Post vault#270 the resolver walks `parent_names` so a note's
|
|
233
|
+
// effective fields include all ancestors' declarations (first-in-walk wins
|
|
234
|
+
// on conflict, surfaced as `schema_conflict` warnings); a tag named
|
|
235
|
+
// `_default` is the implicit universal parent. Returns null when no
|
|
236
|
+
// ancestor declares any fields. The underlying resolver is in-memory after
|
|
237
|
+
// the first lazy load.
|
|
238
|
+
validateNoteAgainstSchemas(note: { path?: string | null; tags?: string[]; metadata?: Record<string, unknown> }): {
|
|
239
|
+
schemas: string[];
|
|
240
|
+
warnings: {
|
|
241
|
+
field: string;
|
|
242
|
+
schema: string;
|
|
243
|
+
reason: "type_mismatch" | "enum_mismatch" | "schema_conflict";
|
|
244
|
+
message: string;
|
|
245
|
+
/** Set only on `schema_conflict` — the tag whose declaration was overridden. */
|
|
246
|
+
loser_schema?: string;
|
|
247
|
+
}[];
|
|
248
|
+
} | null;
|
|
160
249
|
|
|
161
250
|
// Attachments
|
|
162
251
|
addAttachment(noteId: string, path: string, mimeType: string, metadata?: Record<string, unknown>): Promise<Attachment>;
|