@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/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(