@openparachute/vault 0.6.4-rc.3 → 0.6.4-rc.5
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/core/src/core.test.ts +126 -0
- package/core/src/mcp.ts +8 -4
- package/core/src/migrate-tag-field.test.ts +471 -0
- package/core/src/migrate-tag-field.ts +638 -0
- package/core/src/notes.ts +37 -0
- package/core/src/store.ts +47 -18
- package/package.json +1 -1
- package/src/cli.ts +164 -3
- package/src/module-config.ts +46 -80
- package/src/routes.ts +28 -9
- package/src/routing.test.ts +42 -25
- package/src/vault.test.ts +52 -0
package/src/vault.test.ts
CHANGED
|
@@ -4012,6 +4012,36 @@ describe("HTTP PATCH /notes/:idOrPath (update)", async () => {
|
|
|
4012
4012
|
expect(body.metadata).toEqual({ a: 1, b: 2 });
|
|
4013
4013
|
});
|
|
4014
4014
|
|
|
4015
|
+
test("PATCH metadata null DELETES the key (RFC 7386), not a literal null (vault#478/#479)", async () => {
|
|
4016
|
+
await store.createNote("doc", { id: "x", metadata: { keep: "yes", drop: "old", n: 3 } });
|
|
4017
|
+
const res = await handleNotes(
|
|
4018
|
+
mkReq("PATCH", "/notes/x", { metadata: { drop: null }, force: true }),
|
|
4019
|
+
store,
|
|
4020
|
+
"/x",
|
|
4021
|
+
);
|
|
4022
|
+
expect(res.status).toBe(200);
|
|
4023
|
+
const body = await res.json() as any;
|
|
4024
|
+
// Key removed entirely — must NOT survive as a literal JSON null.
|
|
4025
|
+
expect(body.metadata).not.toHaveProperty("drop");
|
|
4026
|
+
expect(body.metadata).toEqual({ keep: "yes", n: 3 });
|
|
4027
|
+
// Persisted state matches the response (round-trips).
|
|
4028
|
+
const fresh = await store.getNote("x");
|
|
4029
|
+
expect(fresh!.metadata).not.toHaveProperty("drop");
|
|
4030
|
+
expect(fresh!.metadata).toEqual({ keep: "yes", n: 3 });
|
|
4031
|
+
});
|
|
4032
|
+
|
|
4033
|
+
test("PATCH metadata key-rename in one call: set new, null-delete old (vault#478)", async () => {
|
|
4034
|
+
await store.createNote("doc", { id: "x", metadata: { "old-key": "v", stable: true } });
|
|
4035
|
+
const res = await handleNotes(
|
|
4036
|
+
mkReq("PATCH", "/notes/x", { metadata: { new_key: "v", "old-key": null }, force: true }),
|
|
4037
|
+
store,
|
|
4038
|
+
"/x",
|
|
4039
|
+
);
|
|
4040
|
+
const body = await res.json() as any;
|
|
4041
|
+
expect(body.metadata).not.toHaveProperty("old-key");
|
|
4042
|
+
expect(body.metadata).toEqual({ new_key: "v", stable: true });
|
|
4043
|
+
});
|
|
4044
|
+
|
|
4015
4045
|
test("PATCH adds/removes tags", async () => {
|
|
4016
4046
|
await store.createNote("x", { id: "x", tags: ["old"] });
|
|
4017
4047
|
const res = await handleNotes(
|
|
@@ -4789,6 +4819,28 @@ describe("HTTP /tags", async () => {
|
|
|
4789
4819
|
.filter((n) => n.startsWith("meta_"));
|
|
4790
4820
|
}
|
|
4791
4821
|
|
|
4822
|
+
test("PUT /tags/:name with a bad indexed-field name returns 400 + leaves schema unchanged (vault#478)", async () => {
|
|
4823
|
+
// kebab-case indexed field violates [A-Za-z0-9_]. Pre-fix this persisted
|
|
4824
|
+
// the declaration then 500'd on index creation, leaving a tag claiming an
|
|
4825
|
+
// index the engine couldn't build (the "lying schema" loop).
|
|
4826
|
+
const res = await handleTags(
|
|
4827
|
+
mkReq("PUT", "/tags/meeting", { fields: { "meeting-type": { type: "string", indexed: true } } }),
|
|
4828
|
+
store,
|
|
4829
|
+
"/meeting",
|
|
4830
|
+
);
|
|
4831
|
+
expect(res.status).toBe(400);
|
|
4832
|
+
const body = await res.json() as any;
|
|
4833
|
+
expect(body.error_type).toBe("invalid_indexed_field");
|
|
4834
|
+
expect(body.error).toMatch(/invalid field name/);
|
|
4835
|
+
|
|
4836
|
+
// Schema is untouched — no poisoned field declared.
|
|
4837
|
+
const record = await store.getTagRecord("meeting");
|
|
4838
|
+
expect(record?.fields?.["meeting-type"]).toBeUndefined();
|
|
4839
|
+
// No orphan/lying index: neither the generated column nor an indexed_fields row.
|
|
4840
|
+
expect(notesMetaCols()).not.toContain("meta_meeting-type");
|
|
4841
|
+
expect(buildVaultProjection(db).indexed_fields.map((f) => f.name)).not.toContain("meeting-type");
|
|
4842
|
+
});
|
|
4843
|
+
|
|
4792
4844
|
test("PUT /tags/:name {fields:null} drops the orphaned generated column", async () => {
|
|
4793
4845
|
// Declare an indexed field via REST PUT — column materializes.
|
|
4794
4846
|
await handleTags(
|