From 822a59babb2612f8155d6377a1732a0e98065319 Mon Sep 17 00:00:00 2001 From: "Aaron C. Meadows" Date: Wed, 3 Jun 2026 10:52:54 -0500 Subject: [PATCH 1/7] Updated libSmartAttributes to 0.0.4, added support for setWithWorker and noCreate to setSheetItem() usage --- .types/index.d.ts | 6 +- .../0.0.4/libSmartAttributes.js | 75 ++++++++ libSmartAttributes/script.json | 3 +- libSmartAttributes/src/index.ts | 19 +- libSmartAttributes/tests/index.test.ts | 176 +++++++++++++++--- 5 files changed, 239 insertions(+), 40 deletions(-) create mode 100644 libSmartAttributes/0.0.4/libSmartAttributes.js diff --git a/.types/index.d.ts b/.types/index.d.ts index 5c1559643..df420dcfa 100644 --- a/.types/index.d.ts +++ b/.types/index.d.ts @@ -554,7 +554,11 @@ declare function getAllObjs(): Roll20Object[]; */ declare function getAttrByName(character_id: string, attribute_name: string, value_type?: "current" | "max"): string; -type SheetItemOptions = { allowThrow?: boolean }; +type SheetItemOptions = { + allowThrow?: boolean, + createAttr?: boolean, + withWorker?: boolean +}; type SheetItemValueTYpe = "current" | "max"; /** diff --git a/libSmartAttributes/0.0.4/libSmartAttributes.js b/libSmartAttributes/0.0.4/libSmartAttributes.js new file mode 100644 index 000000000..5c5547ec0 --- /dev/null +++ b/libSmartAttributes/0.0.4/libSmartAttributes.js @@ -0,0 +1,75 @@ +// libSmartAttributes v0.0.4 by GUD Team | libSmartAttributes provides an interface for managing beacon attributes in a slightly smarter way. +var libSmartAttributes = (function () { + 'use strict'; + + async function getAttribute(characterId, name, type = "current") { + // Try for a legacy attribute or beacon computed + const attr = await getSheetItem(characterId, name, type); + if (attr !== null && attr !== undefined) { + return attr; + } + // Then try for the user attribute + const userAttr = await getSheetItem(characterId, `user.${name}`, type); + if (userAttr !== null && userAttr !== undefined) { + return userAttr; + } + log(`Attribute ${name} not found on character ${characterId}`); + return undefined; + } + async function setAttribute(characterId, name, value, type = "current", options) { + try { + await setSheetItem(characterId, name, value, type, { + allowThrow: true, + createAttr: options?.noCreate === undefined ? true : !options.noCreate, + withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker + }); + return; + } + catch { + // throw will happen on beacon sheets if the computed doesn't exist or is read-only + } + // Then default to a user attribute + setSheetItem(characterId, `user.${name}`, value, type, { + allowThrow: false, + createAttr: options?.noCreate === undefined ? true : !options.noCreate, + withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker + }); + return; + } + async function deleteAttribute(characterId, name, type = "current") { + // Try for legacy attribute first + const legacyAttr = findObjs({ + _type: "attribute", + _characterid: characterId, + name: name, + })[0]; + if (legacyAttr) { + legacyAttr.remove(); + return; + } + // Then try for the beacon computed + const beaconAttr = await getSheetItem(characterId, name, type); + if (beaconAttr !== null && beaconAttr !== undefined) { + log(`Cannot delete beacon computed attribute ${name} on character ${characterId}. Setting to undefined instead`); + setSheetItem(characterId, name, undefined, type); + return; + } + // Then try for the user attribute + const userAttr = await getSheetItem(characterId, `user.${name}`, type); + if (userAttr !== null && userAttr !== undefined) { + log(`Deleting user attribute ${name} on character ${characterId}`); + setSheetItem(characterId, `user.${name}`, undefined, type); + return; + } + log(`Attribute ${type} not found on character ${characterId}, nothing to delete`); + return; + } + var index = { + getAttribute, + setAttribute, + deleteAttribute, + }; + + return index; + +})(); diff --git a/libSmartAttributes/script.json b/libSmartAttributes/script.json index 9e156eeb2..66de2c135 100644 --- a/libSmartAttributes/script.json +++ b/libSmartAttributes/script.json @@ -1,6 +1,6 @@ { "name": "libSmartAttributes", - "version": "0.0.3", + "version": "0.0.4", "description": "libSmartAttributes provides an interface for managing beacon attributes in a slightly smarter way.", "authors": "GUD Team", "roll20userid": "8705027", @@ -10,6 +10,7 @@ "script": "libSmartAttributes.js", "useroptions": [], "previousversions": [ + "0.0.3", "0.0.2", "0.0.1" ] diff --git a/libSmartAttributes/src/index.ts b/libSmartAttributes/src/index.ts index 37bcefc17..9d5347d7a 100644 --- a/libSmartAttributes/src/index.ts +++ b/libSmartAttributes/src/index.ts @@ -22,6 +22,7 @@ async function getAttribute( }; type SetOptions = { + setWithWorker?: boolean; noCreate?: boolean; }; @@ -34,20 +35,22 @@ async function setAttribute( ) { try { - await setSheetItem(characterId, name, value, type, {allowThrow: true}); + await setSheetItem(characterId, name, value, type, { + allowThrow: true, + createAttr: options?.noCreate === undefined ? true : !options.noCreate, + withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker + }); return; } catch { // throw will happen on beacon sheets if the computed doesn't exist or is read-only } - // Guard against creating user attributes if noCreate is set - if (options?.noCreate) { - log(`Attribute ${name} not found on character ${characterId}, and noCreate option is set. Skipping creation.`); - return; - } - // Then default to a user attribute - setSheetItem(characterId, `user.${name}`, value, type); + setSheetItem(characterId, `user.${name}`, value, type, { + allowThrow: false, + createAttr: options?.noCreate === undefined ? true : !options.noCreate, + withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker + }); return; }; diff --git a/libSmartAttributes/tests/index.test.ts b/libSmartAttributes/tests/index.test.ts index b3f2e5fc8..5ce35f5fc 100644 --- a/libSmartAttributes/tests/index.test.ts +++ b/libSmartAttributes/tests/index.test.ts @@ -1,17 +1,26 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import SmartAttributes from "../src/index"; -// Mock Roll20 API functions const mockGetSheetItem = vi.fn(); const mockSetSheetItem = vi.fn(); const mockLog = vi.fn(); - -// Setup global mocks vi.stubGlobal("getSheetItem", mockGetSheetItem); vi.stubGlobal("setSheetItem", mockSetSheetItem); vi.stubGlobal("log", mockLog); +/** Matches default setSheetItem options from setAttribute */ +const sheetOpts = (overrides: { + allowThrow?: boolean; + createAttr?: boolean; + withWorker?: boolean; +} = {}) => ({ + allowThrow: true, + createAttr: true, + withWorker: true, + ...overrides, +}); + describe("SmartAttributes", () => { beforeEach(() => { vi.clearAllMocks(); @@ -50,21 +59,21 @@ describe("SmartAttributes", () => { }); it("should handle falsy beacon values correctly", async () => { - mockGetSheetItem.mockResolvedValueOnce(0); // 0 is now treated as valid + mockGetSheetItem.mockResolvedValueOnce(0); const result = await SmartAttributes.getAttribute(characterId, attributeName); - expect(result).toBe(0); // 0 is returned as valid beacon value + expect(result).toBe(0); expect(mockGetSheetItem).toHaveBeenCalledTimes(1); expect(mockGetSheetItem).toHaveBeenCalledWith(characterId, attributeName, "current"); }); it("should handle empty string beacon values correctly", async () => { - mockGetSheetItem.mockResolvedValueOnce(""); // '' is now treated as valid + mockGetSheetItem.mockResolvedValueOnce(""); const result = await SmartAttributes.getAttribute(characterId, attributeName); - expect(result).toBe(""); // Empty string is returned as valid beacon value + expect(result).toBe(""); expect(mockGetSheetItem).toHaveBeenCalledTimes(1); expect(mockGetSheetItem).toHaveBeenCalledWith(characterId, attributeName, "current"); }); @@ -75,61 +84,158 @@ describe("SmartAttributes", () => { const attributeName = "strength"; const value = "18"; - it("should set beacon computed attribute when no legacy attribute but beacon exists", async () => { + it("should set beacon computed attribute when setSheetItem succeeds", async () => { mockSetSheetItem.mockResolvedValue("updated-value"); const result = await SmartAttributes.setAttribute(characterId, attributeName, value); - expect(mockSetSheetItem).toHaveBeenCalledWith(characterId, attributeName, value, "current", {allowThrow: true}); + expect(mockSetSheetItem).toHaveBeenCalledTimes(1); + expect(mockSetSheetItem).toHaveBeenCalledWith( + characterId, + attributeName, + value, + "current", + sheetOpts({ allowThrow: true }) + ); expect(result).toBeUndefined(); }); - it("should default to user attribute when no legacy or beacon attribute exists", async () => { + it("should default to user attribute when primary setSheetItem throws", async () => { mockSetSheetItem - .mockImplementationOnce(()=>{throw new Error("missing computed");}) + .mockRejectedValueOnce(new Error("missing computed")) .mockResolvedValue("user-value"); const result = await SmartAttributes.setAttribute(characterId, attributeName, value); - expect(mockSetSheetItem).toHaveBeenCalledWith(characterId, `user.${attributeName}`, value, "current"); + expect(mockSetSheetItem).toHaveBeenCalledTimes(2); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 1, + characterId, + attributeName, + value, + "current", + sheetOpts({ allowThrow: true }) + ); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 2, + characterId, + `user.${attributeName}`, + value, + "current", + sheetOpts({ allowThrow: false }) + ); expect(result).toBeUndefined(); }); + it("should pass createAttr false when noCreate is set", async () => { + mockSetSheetItem.mockRejectedValueOnce(new Error("missing computed")); + + await SmartAttributes.setAttribute(characterId, attributeName, value, "current", { + noCreate: true, + }); + + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 1, + characterId, + attributeName, + value, + "current", + sheetOpts({ allowThrow: true, createAttr: false }) + ); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 2, + characterId, + `user.${attributeName}`, + value, + "current", + sheetOpts({ allowThrow: false, createAttr: false }) + ); + }); + + it("should pass withWorker false when setWithWorker is false", async () => { + mockSetSheetItem.mockResolvedValue("ok"); + + await SmartAttributes.setAttribute(characterId, attributeName, value, "current", { + setWithWorker: false, + }); + + expect(mockSetSheetItem).toHaveBeenCalledWith( + characterId, + attributeName, + value, + "current", + sheetOpts({ allowThrow: true, withWorker: false }) + ); + }); + it("should handle complex values correctly", async () => { const complexValue = { nested: { value: 42 } }; mockSetSheetItem - .mockImplementationOnce(()=>{throw new Error("missing computed");}) + .mockRejectedValueOnce(new Error("missing computed")) .mockResolvedValue(complexValue); const result = await SmartAttributes.setAttribute(characterId, attributeName, complexValue); expect(mockSetSheetItem).toHaveBeenCalledTimes(2); - expect(mockSetSheetItem).toHaveBeenCalledWith(characterId, attributeName, complexValue, "current", {allowThrow:true}); - expect(mockSetSheetItem).toHaveBeenCalledWith(characterId, `user.${attributeName}`, complexValue, "current"); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 1, + characterId, + attributeName, + complexValue, + "current", + sheetOpts({ allowThrow: true }) + ); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 2, + characterId, + `user.${attributeName}`, + complexValue, + "current", + sheetOpts({ allowThrow: false }) + ); expect(result).toBeUndefined(); }); it("should handle null and undefined values", async () => { - mockSetSheetItem.mockResolvedValue(null); mockSetSheetItem - .mockImplementationOnce(()=>{throw new Error("missing computed");}) + .mockRejectedValueOnce(new Error("missing computed")) .mockResolvedValue(null); const result = await SmartAttributes.setAttribute(characterId, attributeName, null); expect(mockSetSheetItem).toHaveBeenCalledTimes(2); - expect(mockSetSheetItem).toHaveBeenCalledWith(characterId, attributeName, null, "current",{allowThrow:true}); - expect(mockSetSheetItem).toHaveBeenCalledWith(characterId, `user.${attributeName}`, null, "current"); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 1, + characterId, + attributeName, + null, + "current", + sheetOpts({ allowThrow: true }) + ); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 2, + characterId, + `user.${attributeName}`, + null, + "current", + sheetOpts({ allowThrow: false }) + ); expect(result).toBeUndefined(); }); - it("should handle falsy beacon values correctly for setting", async () => { - mockGetSheetItem.mockResolvedValue(0); // 0 is now treated as valid existing beacon value + it("should succeed on first setSheetItem without fallback", async () => { mockSetSheetItem.mockResolvedValue("updated"); const result = await SmartAttributes.setAttribute(characterId, attributeName, value); - expect(mockSetSheetItem).toHaveBeenCalledWith(characterId, attributeName, value,"current",{allowThrow:true}); + expect(mockSetSheetItem).toHaveBeenCalledTimes(1); + expect(mockSetSheetItem).toHaveBeenCalledWith( + characterId, + attributeName, + value, + "current", + sheetOpts({ allowThrow: true }) + ); expect(result).toBeUndefined(); }); }); @@ -149,7 +255,7 @@ describe("SmartAttributes", () => { }); it("should handle boolean values in attributes", async () => { - mockGetSheetItem.mockResolvedValueOnce(false); // Test with false to show falsy values are valid + mockGetSheetItem.mockResolvedValueOnce(false); const result = await SmartAttributes.getAttribute(characterId, attributeName); @@ -165,11 +271,9 @@ describe("SmartAttributes", () => { mockGetSheetItem.mockResolvedValue("beacon-10"); mockSetSheetItem.mockResolvedValue("beacon-15"); - // Get current value const currentValue = await SmartAttributes.getAttribute(characterId, attributeName); expect(currentValue).toBe("beacon-10"); - // Set new value const result = await SmartAttributes.setAttribute(characterId, attributeName, "beacon-15"); expect(result).toBeUndefined(); }); @@ -177,20 +281,32 @@ describe("SmartAttributes", () => { it("should handle get returning undefined but set still working", async () => { mockGetSheetItem.mockResolvedValue(null); mockSetSheetItem - .mockImplementationOnce(()=>{throw new Error("missing computed");}) + .mockRejectedValueOnce(new Error("missing computed")) .mockResolvedValue("new-value"); - // Get returns undefined const currentValue = await SmartAttributes.getAttribute(characterId, attributeName); expect(currentValue).toBeUndefined(); - // But set still works by creating user attribute const result = await SmartAttributes.setAttribute(characterId, attributeName, "new-value"); expect(result).toBeUndefined(); expect(mockSetSheetItem).toHaveBeenCalledTimes(2); - expect(mockSetSheetItem).toHaveBeenCalledWith(characterId, attributeName, "new-value", "current",{allowThrow:true}); - expect(mockSetSheetItem).toHaveBeenCalledWith(characterId, `user.${attributeName}`, "new-value", "current"); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 1, + characterId, + attributeName, + "new-value", + "current", + sheetOpts({ allowThrow: true }) + ); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 2, + characterId, + `user.${attributeName}`, + "new-value", + "current", + sheetOpts({ allowThrow: false }) + ); }); }); }); From 0d3d090da42a271e8b8f936432be97fe7fd0320d Mon Sep 17 00:00:00 2001 From: "Aaron C. Meadows" Date: Wed, 3 Jun 2026 13:32:14 -0500 Subject: [PATCH 2/7] Added handling for readonly computeds. --- .../0.0.4/.libSmartAttributes.js.swp | Bin 0 -> 12288 bytes .../0.0.4/libSmartAttributes.js | 7 ++- libSmartAttributes/src/.index.ts.swp | Bin 0 -> 12288 bytes libSmartAttributes/src/index.ts | 13 ++++- libSmartAttributes/tests/index.test.ts | 45 ++++++++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 libSmartAttributes/0.0.4/.libSmartAttributes.js.swp create mode 100644 libSmartAttributes/src/.index.ts.swp diff --git a/libSmartAttributes/0.0.4/.libSmartAttributes.js.swp b/libSmartAttributes/0.0.4/.libSmartAttributes.js.swp new file mode 100644 index 0000000000000000000000000000000000000000..a9d953656364553000eaa19a06273a84aab35f85 GIT binary patch literal 12288 zcmeHN&2QX96ra)nEu~PE&r>zoM$JZAyxW3GNfd>uQL6|lP?NL=K9X5`cJ0J#FZMXw zRakoC!i7HoaX~6v;KGRuM^4&2riw5{I8CQTgtxXwqBV_71&hwLYb!umDe zbGy@MI1HxkL2mJGY_b`md_y5EH|37?| zvGc$ba2oja8OFW>1YiI!0LOsek2Cf;unJrN4B*eF8T%Rd3Ge{{aNyr#jNJw<1LuJX z@G@`&_~t3bz61i`EN~L|@hD?W;5u*_coX>N2y_Ftflq-o;0ka7I0f8&lCe9$Z@~A! zCQt*udxEhp@IG)2mk zQ$L!4hmL{4Om$`swTUN?DhfRAc$U1G4h`nw8Je|lOGnA)kEB2S-uQ5q;OOk;3p*_Ki*DcPwOG}rig6w1Ju+V(_84%TZNmdKXlNvp=m zz``n|Cw5|yXd`3jRW62Cn_zRS;k(K8mqmxM=ok>n?D zN&$!Abf=3OWY+&<+~CVn#nD3QWMm?1LnVb(jz_XoM*c7&rQuA?-k205?;Vwdk1=9V zVt~bw7%LdAYzwnb4jF;$_*;@%!|-*=z+9l{%wXod#s|3|y+Fp!#y|6pK;vs!IXTvB zUcpONNsQc_>GZtDT7(=cWK>l!9dHo zk&W|t)OVIPd;ygfNuEjX7G|V#_?UqdNsrE?7Yxt86c6vGb~3dh6U@0Ly~1_<-ijUg zy;7aa*9Op`yP z{vC4UEOn=>8Cbpyy=leeO$RLzy~_!)0|@IC-*fvcV^Jv6E?AkzmL$sYrLl61Y3xD9 zne;!1Sd-pjyeaYWaL2Rp-m!n+lPcLK+_YmPC0Z&x=%cO^U-}(Ybp?IJ;;Rh{Xum4V zt0-?zdyW=l@|Z`9;tnt>E3#cSU+&S_xZ@7G+cE0TyG|q7K-Rv7p`qkST6ZjSANyU} z)&<|w*iq1W=T}x2`HB=B{_!ZeZs2b@mJGR|gEI{xB@V!gNsA*HJA_jmOjNZTOsGL`Pci|i32-W*s$BnE2?AbkL$b<+yYA#fY2Tr0=|~`4!9!7Uo!VTJ2Xdd3fm4g52uopLl^o9&t3?@ZZGh=D3K#{3 zP+%u}YVyfA)gQ}^(tY>7IK(?I9d0!9I&fKk9GU=%P47zK<1MggOMQNSp09V)@u}{Ej;1GBo%z(e4@n7H~_ye2==fKzC3!wJkQ(!(u0i%FXAXcC=#fWU&l9EE- zjVhoi6y3X0>UpW}S**7FK;peRk6cOXjK%W=LPmk7W_10f{Gy(a#@6LpOLWNdCDr}N zv&r|!YVd$tQUvJMKUeKeK@Dp--V#YaHEnTxiec6cAnL9Xn>p7ILRO_{=6fb7&|(yd zpg6Yf@uui376^-qy5kAEK*|#0|WXII2vR{Zi-wkpGjoI2&zyUJl2 ztyIX1T$lFji6_*J+JbQiMr8S3D7zyn#M|7FO6Vay(r%PWG~W;&$zToY1|%C|z?CZ% zPx-y2Wc#_N_A&_^stFEn;8C-Mt9AC3Nm!&gA!GTm-C84ZppM{nSKwq3t!5_iK(n5p zSLB{C+smBzzNNSW{$5^PS1fUBP1#!vMAJVi@;X+!UCK(tLTCKdiG?I0)$(PEjX(#{ z5gpZzUGC;g)mB-FZe?hhay*OLoN7;EtGf(U2(fR^)|SJ(iaixEX|N=NdbX_Y1fgU+ zT=OidN1i1eZ0$gW=uNdUg}77cEg@}V)3c|yLx$FAv`9|Qk%T(uXC{#1-8!~Z^ayLSHDS--cDaaBytEWf=zG((yuvAM^ z+d+|O@Ky_{k1a}rGF0U-R3%B30iBYzFGBBeNn+JO)j^SPLKISNKjC}s8m#Y-#iiAV z&FL?(*`^Q{y!P92Z8wIfcWYEkq#G8O*3EY)$@vZYq6#YVGbtrmAeL3VnoXh-!fg}g zYW;ZLGQxGm^V^B|&6>m08l$I}EFOhH($lPRAbxLN#8m#OCaxau4qz8Klfl-@37 zYmfCv$NrPMn^CG3Z8*b~)kB`)mpQQYQK#mjqttdXJyD_o@z_ tv_^BmPzI4kR6o^8=-w-lSZUujXiq{RhdE(=h-5 literal 0 HcmV?d00001 diff --git a/libSmartAttributes/src/index.ts b/libSmartAttributes/src/index.ts index 9d5347d7a..f1a6872e6 100644 --- a/libSmartAttributes/src/index.ts +++ b/libSmartAttributes/src/index.ts @@ -26,6 +26,12 @@ type SetOptions = { noCreate?: boolean; }; +type SheetItemError = Error & { + type: string; + details?: Record; +}; + + async function setAttribute( characterId: string, name: string, @@ -41,8 +47,13 @@ async function setAttribute( withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker }); return; - } catch { + } catch (e) { // throw will happen on beacon sheets if the computed doesn't exist or is read-only + switch((e as SheetItemError).type){ + // for read only computeds, we don't want to make a shadow "user." version. + case "COMPUTED_READONLY": + return; + } } // Then default to a user attribute diff --git a/libSmartAttributes/tests/index.test.ts b/libSmartAttributes/tests/index.test.ts index 5ce35f5fc..6ef0629bc 100644 --- a/libSmartAttributes/tests/index.test.ts +++ b/libSmartAttributes/tests/index.test.ts @@ -21,6 +21,13 @@ const sheetOpts = (overrides: { ...overrides, }); +/** Mimics setSheetItem errors from displayErrorMessage(..., true, errorType, details) */ +const sheetItemError = (type: string, message = "setSheetItem failed") => { + const err = new Error(message) as Error & { type: string; details?: Record }; + err.type = type; + return err; +}; + describe("SmartAttributes", () => { beforeEach(() => { vi.clearAllMocks(); @@ -127,6 +134,44 @@ describe("SmartAttributes", () => { expect(result).toBeUndefined(); }); + it("should not create user attribute when computed is read-only", async () => { + mockSetSheetItem.mockRejectedValueOnce( + sheetItemError("COMPUTED_READONLY", 'ERROR: Readonly Property "strength".') + ); + + const result = await SmartAttributes.setAttribute(characterId, attributeName, value); + + expect(mockSetSheetItem).toHaveBeenCalledTimes(1); + expect(mockSetSheetItem).toHaveBeenCalledWith( + characterId, + attributeName, + value, + "current", + sheetOpts({ allowThrow: true }) + ); + expect(result).toBeUndefined(); + }); + + it("should still fall through to user attribute for non-readonly setSheetItem errors", async () => { + mockSetSheetItem + .mockRejectedValueOnce( + sheetItemError("COMPUTED_INVALID", 'ERROR: Property "strength" doesn\'t exist.') + ) + .mockResolvedValue("user-value"); + + await SmartAttributes.setAttribute(characterId, attributeName, value); + + expect(mockSetSheetItem).toHaveBeenCalledTimes(2); + expect(mockSetSheetItem).toHaveBeenNthCalledWith( + 2, + characterId, + `user.${attributeName}`, + value, + "current", + sheetOpts({ allowThrow: false }) + ); + }); + it("should pass createAttr false when noCreate is set", async () => { mockSetSheetItem.mockRejectedValueOnce(new Error("missing computed")); From 3032fea1e191d05356366df297e73f301816551e Mon Sep 17 00:00:00 2001 From: "Aaron C. Meadows" Date: Wed, 3 Jun 2026 14:12:19 -0500 Subject: [PATCH 3/7] Adding reporting on success with set and delete --- .../0.0.4/libSmartAttributes.js | 54 ++++-- libSmartAttributes/src/index.ts | 51 +++-- libSmartAttributes/src/types.d.ts | 4 +- libSmartAttributes/tests/index.test.ts | 179 ++++++++++++------ 4 files changed, 197 insertions(+), 91 deletions(-) diff --git a/libSmartAttributes/0.0.4/libSmartAttributes.js b/libSmartAttributes/0.0.4/libSmartAttributes.js index a947031e3..238538337 100644 --- a/libSmartAttributes/0.0.4/libSmartAttributes.js +++ b/libSmartAttributes/0.0.4/libSmartAttributes.js @@ -23,23 +23,28 @@ var libSmartAttributes = (function () { createAttr: options?.noCreate === undefined ? true : !options.noCreate, withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker }); - return; + return true; } catch (e) { // throw will happen on beacon sheets if the computed doesn't exist or is read-only switch (e.type) { // for read only computeds, we don't want to make a shadow "user." version. case "COMPUTED_READONLY": - return; + return false; } } // Then default to a user attribute - setSheetItem(characterId, `user.${name}`, value, type, { - allowThrow: false, - createAttr: options?.noCreate === undefined ? true : !options.noCreate, - withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker - }); - return; + try { + await setSheetItem(characterId, `user.${name}`, value, type, { + allowThrow: true, + createAttr: options?.noCreate === undefined ? true : !options.noCreate, + withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker + }); + return true; + } + catch { + return false; + } } async function deleteAttribute(characterId, name, type = "current") { // Try for legacy attribute first @@ -50,24 +55,39 @@ var libSmartAttributes = (function () { })[0]; if (legacyAttr) { legacyAttr.remove(); - return; + return true; } // Then try for the beacon computed const beaconAttr = await getSheetItem(characterId, name, type); if (beaconAttr !== null && beaconAttr !== undefined) { - log(`Cannot delete beacon computed attribute ${name} on character ${characterId}. Setting to undefined instead`); - setSheetItem(characterId, name, undefined, type); - return; + // Cannot delete beacon computed attributes. Setting to undefined instead. + try { + await setSheetItem(characterId, name, undefined, type, { allowThrow: true }); + return true; + } + catch (e) { + switch (e.type) { + // for read only computeds, we don't want to fall through to a "user." version. + case "COMPUTED_READONLY": + return false; + } + } } // Then try for the user attribute const userAttr = await getSheetItem(characterId, `user.${name}`, type); if (userAttr !== null && userAttr !== undefined) { - log(`Deleting user attribute ${name} on character ${characterId}`); - setSheetItem(characterId, `user.${name}`, undefined, type); - return; + try { + await setSheetItem(characterId, `user.${name}`, undefined, type, { + allowThrow: true, + createAttr: false + }); + return true; + } + catch { + return false; + } } - log(`Attribute ${type} not found on character ${characterId}, nothing to delete`); - return; + return false; } var index = { getAttribute, diff --git a/libSmartAttributes/src/index.ts b/libSmartAttributes/src/index.ts index f1a6872e6..17e8eb638 100644 --- a/libSmartAttributes/src/index.ts +++ b/libSmartAttributes/src/index.ts @@ -46,23 +46,27 @@ async function setAttribute( createAttr: options?.noCreate === undefined ? true : !options.noCreate, withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker }); - return; + return true; } catch (e) { // throw will happen on beacon sheets if the computed doesn't exist or is read-only switch((e as SheetItemError).type){ // for read only computeds, we don't want to make a shadow "user." version. case "COMPUTED_READONLY": - return; + return false; } } // Then default to a user attribute - setSheetItem(characterId, `user.${name}`, value, type, { - allowThrow: false, - createAttr: options?.noCreate === undefined ? true : !options.noCreate, - withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker - }); - return; + try { + await setSheetItem(characterId, `user.${name}`, value, type, { + allowThrow: true, + createAttr: options?.noCreate === undefined ? true : !options.noCreate, + withWorker: options?.setWithWorker === undefined ? true : options.setWithWorker + }); + return true; + } catch { + return false; + } }; async function deleteAttribute(characterId: string, name: string, type: AttributeType = "current") { @@ -75,27 +79,40 @@ async function deleteAttribute(characterId: string, name: string, type: Attribut if (legacyAttr) { legacyAttr.remove(); - return; + return true; } // Then try for the beacon computed const beaconAttr = await getSheetItem(characterId, name, type); if (beaconAttr !== null && beaconAttr !== undefined) { - log(`Cannot delete beacon computed attribute ${name} on character ${characterId}. Setting to undefined instead`); - setSheetItem(characterId, name, undefined, type); - return; + // Cannot delete beacon computed attributes. Setting to undefined instead. + try { + await setSheetItem(characterId, name, undefined, type, { allowThrow: true }); + return true; + } catch (e) { + switch((e as SheetItemError).type){ + // for read only computeds, we don't want to fall through to a "user." version. + case "COMPUTED_READONLY": + return false; + } + } } // Then try for the user attribute const userAttr = await getSheetItem(characterId, `user.${name}`, type); if (userAttr !== null && userAttr !== undefined) { - log(`Deleting user attribute ${name} on character ${characterId}`); - setSheetItem(characterId, `user.${name}`, undefined, type); - return; + try { + await setSheetItem(characterId, `user.${name}`, undefined, type, { + allowThrow: true, + createAttr: false + }); + return true; + } catch { + return false; + } } - log(`Attribute ${type} not found on character ${characterId}, nothing to delete`); - return; + return false; }; export default { diff --git a/libSmartAttributes/src/types.d.ts b/libSmartAttributes/src/types.d.ts index b881d3d2a..d76b339e2 100644 --- a/libSmartAttributes/src/types.d.ts +++ b/libSmartAttributes/src/types.d.ts @@ -1,5 +1,5 @@ declare namespace SmartAttributes { function getAttribute(characterId: string, name: string, type?: "current" | "max"): Promise; - function setAttribute(characterId: string, name: string, value: unknown, type?: "current" | "max", options?: { setWithWorker?: boolean, noCreate?: boolean }): Promise; - function deleteAttribute(characterId: string, name: string, type?: "current" | "max", options?: { setWithWorker?: boolean }): Promise; + function setAttribute(characterId: string, name: string, value: unknown, type?: "current" | "max", options?: { setWithWorker?: boolean, noCreate?: boolean }): Promise; + function deleteAttribute(characterId: string, name: string, type?: "current" | "max"): Promise; } \ No newline at end of file diff --git a/libSmartAttributes/tests/index.test.ts b/libSmartAttributes/tests/index.test.ts index 6ef0629bc..437c6ad03 100644 --- a/libSmartAttributes/tests/index.test.ts +++ b/libSmartAttributes/tests/index.test.ts @@ -1,10 +1,12 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import SmartAttributes from "../src/index"; +const mockFindObjs = vi.fn(); const mockGetSheetItem = vi.fn(); const mockSetSheetItem = vi.fn(); const mockLog = vi.fn(); +vi.stubGlobal("findObjs", mockFindObjs); vi.stubGlobal("getSheetItem", mockGetSheetItem); vi.stubGlobal("setSheetItem", mockSetSheetItem); vi.stubGlobal("log", mockLog); @@ -31,6 +33,7 @@ const sheetItemError = (type: string, message = "setSheetItem failed") => { describe("SmartAttributes", () => { beforeEach(() => { vi.clearAllMocks(); + mockFindObjs.mockReturnValue([]); }); describe("getAttribute", () => { @@ -91,7 +94,7 @@ describe("SmartAttributes", () => { const attributeName = "strength"; const value = "18"; - it("should set beacon computed attribute when setSheetItem succeeds", async () => { + it("should return true when setSheetItem succeeds on computed", async () => { mockSetSheetItem.mockResolvedValue("updated-value"); const result = await SmartAttributes.setAttribute(characterId, attributeName, value); @@ -104,10 +107,10 @@ describe("SmartAttributes", () => { "current", sheetOpts({ allowThrow: true }) ); - expect(result).toBeUndefined(); + expect(result).toBe(true); }); - it("should default to user attribute when primary setSheetItem throws", async () => { + it("should return true when falling through to user attribute", async () => { mockSetSheetItem .mockRejectedValueOnce(new Error("missing computed")) .mockResolvedValue("user-value"); @@ -129,12 +132,12 @@ describe("SmartAttributes", () => { `user.${attributeName}`, value, "current", - sheetOpts({ allowThrow: false }) + sheetOpts({ allowThrow: true }) ); - expect(result).toBeUndefined(); + expect(result).toBe(true); }); - it("should not create user attribute when computed is read-only", async () => { + it("should return false and not create user attribute when computed is read-only", async () => { mockSetSheetItem.mockRejectedValueOnce( sheetItemError("COMPUTED_READONLY", 'ERROR: Readonly Property "strength".') ); @@ -149,17 +152,17 @@ describe("SmartAttributes", () => { "current", sheetOpts({ allowThrow: true }) ); - expect(result).toBeUndefined(); + expect(result).toBe(false); }); - it("should still fall through to user attribute for non-readonly setSheetItem errors", async () => { + it("should return true when falling through for non-readonly setSheetItem errors", async () => { mockSetSheetItem .mockRejectedValueOnce( sheetItemError("COMPUTED_INVALID", 'ERROR: Property "strength" doesn\'t exist.') ) .mockResolvedValue("user-value"); - await SmartAttributes.setAttribute(characterId, attributeName, value); + const result = await SmartAttributes.setAttribute(characterId, attributeName, value); expect(mockSetSheetItem).toHaveBeenCalledTimes(2); expect(mockSetSheetItem).toHaveBeenNthCalledWith( @@ -168,14 +171,28 @@ describe("SmartAttributes", () => { `user.${attributeName}`, value, "current", - sheetOpts({ allowThrow: false }) + sheetOpts({ allowThrow: true }) ); + expect(result).toBe(true); + }); + + it("should return false when user attribute fallback also fails", async () => { + mockSetSheetItem + .mockRejectedValueOnce(new Error("missing computed")) + .mockRejectedValueOnce(new Error("user set failed")); + + const result = await SmartAttributes.setAttribute(characterId, attributeName, value); + + expect(mockSetSheetItem).toHaveBeenCalledTimes(2); + expect(result).toBe(false); }); it("should pass createAttr false when noCreate is set", async () => { - mockSetSheetItem.mockRejectedValueOnce(new Error("missing computed")); + mockSetSheetItem + .mockRejectedValueOnce(new Error("missing computed")) + .mockResolvedValue("user-value"); - await SmartAttributes.setAttribute(characterId, attributeName, value, "current", { + const result = await SmartAttributes.setAttribute(characterId, attributeName, value, "current", { noCreate: true, }); @@ -193,14 +210,15 @@ describe("SmartAttributes", () => { `user.${attributeName}`, value, "current", - sheetOpts({ allowThrow: false, createAttr: false }) + sheetOpts({ allowThrow: true, createAttr: false }) ); + expect(result).toBe(true); }); it("should pass withWorker false when setWithWorker is false", async () => { mockSetSheetItem.mockResolvedValue("ok"); - await SmartAttributes.setAttribute(characterId, attributeName, value, "current", { + const result = await SmartAttributes.setAttribute(characterId, attributeName, value, "current", { setWithWorker: false, }); @@ -211,9 +229,10 @@ describe("SmartAttributes", () => { "current", sheetOpts({ allowThrow: true, withWorker: false }) ); + expect(result).toBe(true); }); - it("should handle complex values correctly", async () => { + it("should return true for complex values via user fallback", async () => { const complexValue = { nested: { value: 42 } }; mockSetSheetItem .mockRejectedValueOnce(new Error("missing computed")) @@ -222,26 +241,10 @@ describe("SmartAttributes", () => { const result = await SmartAttributes.setAttribute(characterId, attributeName, complexValue); expect(mockSetSheetItem).toHaveBeenCalledTimes(2); - expect(mockSetSheetItem).toHaveBeenNthCalledWith( - 1, - characterId, - attributeName, - complexValue, - "current", - sheetOpts({ allowThrow: true }) - ); - expect(mockSetSheetItem).toHaveBeenNthCalledWith( - 2, - characterId, - `user.${attributeName}`, - complexValue, - "current", - sheetOpts({ allowThrow: false }) - ); - expect(result).toBeUndefined(); + expect(result).toBe(true); }); - it("should handle null and undefined values", async () => { + it("should return true when setting null via user fallback", async () => { mockSetSheetItem .mockRejectedValueOnce(new Error("missing computed")) .mockResolvedValue(null); @@ -249,39 +252,105 @@ describe("SmartAttributes", () => { const result = await SmartAttributes.setAttribute(characterId, attributeName, null); expect(mockSetSheetItem).toHaveBeenCalledTimes(2); - expect(mockSetSheetItem).toHaveBeenNthCalledWith( - 1, + expect(result).toBe(true); + }); + }); + + describe("deleteAttribute", () => { + const characterId = "char123"; + const attributeName = "strength"; + + it("should return true when removing a legacy attribute", async () => { + const mockRemove = vi.fn(); + mockFindObjs.mockReturnValue([{ remove: mockRemove }]); + + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + + expect(mockFindObjs).toHaveBeenCalledWith({ + _type: "attribute", + _characterid: characterId, + name: attributeName, + }); + expect(mockRemove).toHaveBeenCalled(); + expect(mockGetSheetItem).not.toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it("should return true when clearing a writable beacon computed", async () => { + mockGetSheetItem.mockResolvedValueOnce("10"); + mockSetSheetItem.mockResolvedValue(true); + + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + + expect(mockGetSheetItem).toHaveBeenCalledWith(characterId, attributeName, "current"); + expect(mockSetSheetItem).toHaveBeenCalledWith( characterId, attributeName, - null, + undefined, "current", - sheetOpts({ allowThrow: true }) + { allowThrow: true } ); - expect(mockSetSheetItem).toHaveBeenNthCalledWith( - 2, + expect(result).toBe(true); + }); + + it("should return false and not touch user attribute when beacon computed is read-only", async () => { + mockGetSheetItem.mockResolvedValueOnce("10"); + mockSetSheetItem.mockRejectedValueOnce( + sheetItemError("COMPUTED_READONLY", 'ERROR: Readonly Property "strength".') + ); + + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + + expect(mockSetSheetItem).toHaveBeenCalledTimes(1); + expect(mockSetSheetItem).toHaveBeenCalledWith( characterId, - `user.${attributeName}`, - null, + attributeName, + undefined, "current", - sheetOpts({ allowThrow: false }) + { allowThrow: true } ); - expect(result).toBeUndefined(); + expect(mockGetSheetItem).toHaveBeenCalledTimes(1); + expect(result).toBe(false); }); - it("should succeed on first setSheetItem without fallback", async () => { - mockSetSheetItem.mockResolvedValue("updated"); + it("should return true when deleting an existing user attribute", async () => { + mockGetSheetItem + .mockResolvedValueOnce(null) + .mockResolvedValueOnce("user-value"); + mockSetSheetItem.mockResolvedValue(true); - const result = await SmartAttributes.setAttribute(characterId, attributeName, value); + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); - expect(mockSetSheetItem).toHaveBeenCalledTimes(1); + expect(mockGetSheetItem).toHaveBeenNthCalledWith(1, characterId, attributeName, "current"); + expect(mockGetSheetItem).toHaveBeenNthCalledWith(2, characterId, `user.${attributeName}`, "current"); expect(mockSetSheetItem).toHaveBeenCalledWith( characterId, - attributeName, - value, + `user.${attributeName}`, + undefined, "current", - sheetOpts({ allowThrow: true }) + { allowThrow: true, createAttr: false } ); - expect(result).toBeUndefined(); + expect(result).toBe(true); + }); + + it("should return false when no attribute exists", async () => { + mockGetSheetItem.mockResolvedValue(null); + + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + + expect(mockSetSheetItem).not.toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it("should return false when user attribute delete fails", async () => { + mockGetSheetItem + .mockResolvedValueOnce(null) + .mockResolvedValueOnce("user-value"); + mockSetSheetItem.mockRejectedValueOnce(new Error("delete failed")); + + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + + expect(result).toBe(false); }); }); @@ -320,10 +389,10 @@ describe("SmartAttributes", () => { expect(currentValue).toBe("beacon-10"); const result = await SmartAttributes.setAttribute(characterId, attributeName, "beacon-15"); - expect(result).toBeUndefined(); + expect(result).toBe(true); }); - it("should handle get returning undefined but set still working", async () => { + it("should return true when set falls through to user attribute", async () => { mockGetSheetItem.mockResolvedValue(null); mockSetSheetItem .mockRejectedValueOnce(new Error("missing computed")) @@ -333,7 +402,7 @@ describe("SmartAttributes", () => { expect(currentValue).toBeUndefined(); const result = await SmartAttributes.setAttribute(characterId, attributeName, "new-value"); - expect(result).toBeUndefined(); + expect(result).toBe(true); expect(mockSetSheetItem).toHaveBeenCalledTimes(2); expect(mockSetSheetItem).toHaveBeenNthCalledWith( @@ -350,7 +419,7 @@ describe("SmartAttributes", () => { `user.${attributeName}`, "new-value", "current", - sheetOpts({ allowThrow: false }) + sheetOpts({ allowThrow: true }) ); }); }); From ac4895809eb84a10602431a4fc7cbba7f3d8aa1e Mon Sep 17 00:00:00 2001 From: "Aaron C. Meadows" Date: Thu, 4 Jun 2026 12:59:23 -0500 Subject: [PATCH 4/7] Adding Legacy sheet detection to deleteAttribute --- .types/index.d.ts | 5 +++- .../0.0.4/libSmartAttributes.js | 25 ++++++++++++------- libSmartAttributes/src/index.ts | 25 ++++++++++++------- libSmartAttributes/tests/index.test.ts | 22 ++++++++++++++++ 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/.types/index.d.ts b/.types/index.d.ts index df420dcfa..40c2fe931 100644 --- a/.types/index.d.ts +++ b/.types/index.d.ts @@ -218,7 +218,10 @@ type CharacterProperties = { _defaulttoken: string; }; -declare type Roll20Character = Prettify>; +declare type Roll20Character = Prettify & { + /** Experimental: legacy Roll20 attributes vs Beacon sheet */ + sheetEnvironment?: "legacy" | "beacon"; +}>; // Attribute type with proper properties type AttributeProperties = { diff --git a/libSmartAttributes/0.0.4/libSmartAttributes.js b/libSmartAttributes/0.0.4/libSmartAttributes.js index 238538337..bdeb24b9f 100644 --- a/libSmartAttributes/0.0.4/libSmartAttributes.js +++ b/libSmartAttributes/0.0.4/libSmartAttributes.js @@ -47,15 +47,22 @@ var libSmartAttributes = (function () { } } async function deleteAttribute(characterId, name, type = "current") { - // Try for legacy attribute first - const legacyAttr = findObjs({ - _type: "attribute", - _characterid: characterId, - name: name, - })[0]; - if (legacyAttr) { - legacyAttr.remove(); - return true; + const character = getObj("character", characterId); + if (!character) { + return false; + } + if (character?.sheetEnvironment === "legacy") { + // Try for legacy attribute first + const legacyAttr = findObjs({ + _type: "attribute", + _characterid: characterId, + name: name, + })[0]; + if (legacyAttr) { + legacyAttr.remove(); + return true; + } + return false; } // Then try for the beacon computed const beaconAttr = await getSheetItem(characterId, name, type); diff --git a/libSmartAttributes/src/index.ts b/libSmartAttributes/src/index.ts index 17e8eb638..0b2bd5a1f 100644 --- a/libSmartAttributes/src/index.ts +++ b/libSmartAttributes/src/index.ts @@ -70,16 +70,23 @@ async function setAttribute( }; async function deleteAttribute(characterId: string, name: string, type: AttributeType = "current") { - // Try for legacy attribute first - const legacyAttr = findObjs({ - _type: "attribute", - _characterid: characterId, - name: name, - })[0]; + const character = getObj("character",characterId); + if(!character) { + return false; + } + if( character?.sheetEnvironment === "legacy"){ + // Try for legacy attribute first + const legacyAttr = findObjs({ + _type: "attribute", + _characterid: characterId, + name: name, + })[0]; - if (legacyAttr) { - legacyAttr.remove(); - return true; + if (legacyAttr) { + legacyAttr.remove(); + return true; + } + return false; } // Then try for the beacon computed diff --git a/libSmartAttributes/tests/index.test.ts b/libSmartAttributes/tests/index.test.ts index 437c6ad03..1a2726d5c 100644 --- a/libSmartAttributes/tests/index.test.ts +++ b/libSmartAttributes/tests/index.test.ts @@ -2,11 +2,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import SmartAttributes from "../src/index"; const mockFindObjs = vi.fn(); +const mockGetObj = vi.fn(); const mockGetSheetItem = vi.fn(); const mockSetSheetItem = vi.fn(); const mockLog = vi.fn(); vi.stubGlobal("findObjs", mockFindObjs); +vi.stubGlobal("getObj", mockGetObj); vi.stubGlobal("getSheetItem", mockGetSheetItem); vi.stubGlobal("setSheetItem", mockSetSheetItem); vi.stubGlobal("log", mockLog); @@ -34,6 +36,7 @@ describe("SmartAttributes", () => { beforeEach(() => { vi.clearAllMocks(); mockFindObjs.mockReturnValue([]); + mockGetObj.mockReturnValue({ sheetEnvironment: "beacon" }); }); describe("getAttribute", () => { @@ -262,6 +265,7 @@ describe("SmartAttributes", () => { it("should return true when removing a legacy attribute", async () => { const mockRemove = vi.fn(); + mockGetObj.mockReturnValue({ sheetEnvironment: "legacy" }); mockFindObjs.mockReturnValue([{ remove: mockRemove }]); const result = await SmartAttributes.deleteAttribute(characterId, attributeName); @@ -276,6 +280,24 @@ describe("SmartAttributes", () => { expect(result).toBe(true); }); + it("should return false when legacy character has no matching attribute", async () => { + mockGetObj.mockReturnValue({ sheetEnvironment: "legacy" }); + mockFindObjs.mockReturnValue([]); + + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + + expect(mockGetSheetItem).not.toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it("should return false when character is not found", async () => { + mockGetObj.mockReturnValue(null); + + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + + expect(result).toBe(false); + }); + it("should return true when clearing a writable beacon computed", async () => { mockGetSheetItem.mockResolvedValueOnce("10"); mockSetSheetItem.mockResolvedValue(true); From 1697d4518fa8f96af71ea67788390b26a4244c24 Mon Sep 17 00:00:00 2001 From: "Aaron C. Meadows" Date: Fri, 5 Jun 2026 11:31:40 -0500 Subject: [PATCH 5/7] Make delete on a computed fail. --- .../0.0.4/libSmartAttributes.js | 14 ++------ libSmartAttributes/src/.index.ts.swo | Bin 0 -> 16384 bytes libSmartAttributes/src/index.ts | 13 ++----- libSmartAttributes/tests/index.test.ts | 32 ++---------------- 4 files changed, 7 insertions(+), 52 deletions(-) create mode 100644 libSmartAttributes/src/.index.ts.swo diff --git a/libSmartAttributes/0.0.4/libSmartAttributes.js b/libSmartAttributes/0.0.4/libSmartAttributes.js index bdeb24b9f..79bf5693d 100644 --- a/libSmartAttributes/0.0.4/libSmartAttributes.js +++ b/libSmartAttributes/0.0.4/libSmartAttributes.js @@ -67,18 +67,8 @@ var libSmartAttributes = (function () { // Then try for the beacon computed const beaconAttr = await getSheetItem(characterId, name, type); if (beaconAttr !== null && beaconAttr !== undefined) { - // Cannot delete beacon computed attributes. Setting to undefined instead. - try { - await setSheetItem(characterId, name, undefined, type, { allowThrow: true }); - return true; - } - catch (e) { - switch (e.type) { - // for read only computeds, we don't want to fall through to a "user." version. - case "COMPUTED_READONLY": - return false; - } - } + // Cannot delete beacon computed attributes. + return false; } // Then try for the user attribute const userAttr = await getSheetItem(characterId, `user.${name}`, type); diff --git a/libSmartAttributes/src/.index.ts.swo b/libSmartAttributes/src/.index.ts.swo new file mode 100644 index 0000000000000000000000000000000000000000..c03ccdc4f21b658f0dc5df5265119476c7274812 GIT binary patch literal 16384 zcmeI2TWB0r7{^c3i<(-kR($@lYTUqfH?3IhrZL8(4?$~6np$eD&Ft>k-AQ(5(wW(8 zLR^bJ_*n6Vh~Psjf-fS754MPCi!UvB1M3A}#QLIw`XC6F`2Xg%x5=f&)>6*EFPk}Y z=FB<&`Og2$IWw8w$3}M1_GGWXwo!-?nbwfu*;Sez>CNa+Nh)xf$QAd>mN&%&SQa~x76i^B%1(X6xfonn|1(X6x0i}RaKq;UUPzopolmbctrGQdE zDR31kpylw|ACvpjYfw47{}1N>=WZ6_Yw!U$23`cypaiyq3pWYzIrs>i1Sh~N;9+n( zSOqR@5aJ(j0=xx|gJa+*u)#s_IM@%igC4L3tO5co+$hA4;0!nk-UiPB6HI|gkO4j5 z7I1345M?02I2Z=McA{;-0%KqY*ao(OEnq!Z4L-d=he4oa!So19C#C=-_)`EAj{}wn7j)51zGvF|I5*z?mW+QARvk;#c#-|;Q zZWO3uON+GN!w$N0-q6ajMy5g8qGoACdY9Dy0&2ce`hXi4}YclGhEH z>!Eo;lr;_8spse?t*Yq`6{NGLD5W#v$a35*KkG44zQZ!za207LohgvSpv( z6yCD<`~^Q(g$93KsrJT(MtAMrJHB)H(Adtw;n7DQ*&jSR2@V_H5Re>)meg|SO{F>XiSjhQoyyq>s!Gb4#@!B8HNzptA97D{$ice2HJT#{x}*q3%%mTC1Q-A=>Z5220FMMjECuS0U0AL5Qx z2_xKIv_rBDBXylM)7^XuxHWW!2siE(OZLK?touoa|aV-b!3-31R zFbYV-?c-kNogdmt5Kt}2;pvTT(~xI*zww&qyG(SDv^1q|A8k68WI@Z$aix=%ESobj z4(syTQ$NI2MPjYzK<`05O2lepz~X|j=O6cT>b zgFUZe;VI*&C#n(4RxC>zPRwn2joa+mZ>!Y?vC7qrh|-!XCLcoV7x$Ugl(f=hPIGPT zfh0nBUzkq)jaEVf2-S+DFn4oi!DT{0bWulg?ZbQnNy8j+lNT$n%fEh@(Yu92=*VVu z96h<3b}#OwFW%2u2fi7tR6)mMNfT}-3p!gcz+s%#Dkax7v}H%QP|l`t%^lU2Ft3ZU zuCg`-Ga|dF<;-fdZLQR_a9#GmDsD9Fu&7N>V?akp`aRL+M+cj2f(Q4&PT{1|W-pbq zI-WUTxw`GT?q+sf5oLvK9r#)ZG4Ba$@j&w&Z;|j4o{h&-vxJ>`%5O>Q> z!`G(_vubdP=Wf15U_Ugs5tv($+~Pf`edr=_JodS!k&}+5m+XNwjmfNO<#q(N<6rCn zb{4OS-jP1Rdo80Q49OiOshfS+EjZ}oC^=)A_|)0pl2C`bpEMrw3JrBZfk8ojJcXkY z7vnx;RO(epEP{77v-936q1G+!#Y1B6G!6aHockhzRL^`y;l(5FMXD}R3g%_P+!g$MQ zNl#I%tj)%#Pw@PI26OfA0MGw}_xpcgj{hEb4IBX#kOAZ1L68R9z`fuu5CdIcBM{(s zl=p*Q?zi~;2Al)00$v~Z3G;QeDFu`QN&%&SQa~x76i^B%1(X7pQ2|UzgEt!=H?8!0 zE34srf#=qZ^JLy{oM0q`oSil;UXk?{ikfGsk#kd?_S9%ab5pGSnNE@49AO#Ps~S&N zgQ|bhQ1u|~R7BcoH_e~l^U879zNoaQHb>=)N{c!-O0N~1TZ_mOl_n$d$@Y4*U~Ec}UH>~;HR_->|g0lob_XSn3ixa1qWVfh!C6FD{j literal 0 HcmV?d00001 diff --git a/libSmartAttributes/src/index.ts b/libSmartAttributes/src/index.ts index 0b2bd5a1f..c964fe6cf 100644 --- a/libSmartAttributes/src/index.ts +++ b/libSmartAttributes/src/index.ts @@ -92,17 +92,8 @@ async function deleteAttribute(characterId: string, name: string, type: Attribut // Then try for the beacon computed const beaconAttr = await getSheetItem(characterId, name, type); if (beaconAttr !== null && beaconAttr !== undefined) { - // Cannot delete beacon computed attributes. Setting to undefined instead. - try { - await setSheetItem(characterId, name, undefined, type, { allowThrow: true }); - return true; - } catch (e) { - switch((e as SheetItemError).type){ - // for read only computeds, we don't want to fall through to a "user." version. - case "COMPUTED_READONLY": - return false; - } - } + // Cannot delete beacon computed attributes. + return false; } // Then try for the user attribute diff --git a/libSmartAttributes/tests/index.test.ts b/libSmartAttributes/tests/index.test.ts index 1a2726d5c..645796473 100644 --- a/libSmartAttributes/tests/index.test.ts +++ b/libSmartAttributes/tests/index.test.ts @@ -298,40 +298,14 @@ describe("SmartAttributes", () => { expect(result).toBe(false); }); - it("should return true when clearing a writable beacon computed", async () => { + it("should return false when a beacon computed exists", async () => { mockGetSheetItem.mockResolvedValueOnce("10"); - mockSetSheetItem.mockResolvedValue(true); const result = await SmartAttributes.deleteAttribute(characterId, attributeName); - expect(mockGetSheetItem).toHaveBeenCalledWith(characterId, attributeName, "current"); - expect(mockSetSheetItem).toHaveBeenCalledWith( - characterId, - attributeName, - undefined, - "current", - { allowThrow: true } - ); - expect(result).toBe(true); - }); - - it("should return false and not touch user attribute when beacon computed is read-only", async () => { - mockGetSheetItem.mockResolvedValueOnce("10"); - mockSetSheetItem.mockRejectedValueOnce( - sheetItemError("COMPUTED_READONLY", 'ERROR: Readonly Property "strength".') - ); - - const result = await SmartAttributes.deleteAttribute(characterId, attributeName); - - expect(mockSetSheetItem).toHaveBeenCalledTimes(1); - expect(mockSetSheetItem).toHaveBeenCalledWith( - characterId, - attributeName, - undefined, - "current", - { allowThrow: true } - ); expect(mockGetSheetItem).toHaveBeenCalledTimes(1); + expect(mockGetSheetItem).toHaveBeenCalledWith(characterId, attributeName, "current"); + expect(mockSetSheetItem).not.toHaveBeenCalled(); expect(result).toBe(false); }); From dc0913efd0cb9b705ccf04a4264467645ad6ab9e Mon Sep 17 00:00:00 2001 From: "Aaron C. Meadows" Date: Fri, 5 Jun 2026 11:43:31 -0500 Subject: [PATCH 6/7] Corrections --- .../0.0.4/libSmartAttributes.js | 4 +--- libSmartAttributes/src/index.ts | 7 +++--- libSmartAttributes/tests/index.test.ts | 24 ++++++++++++++++++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/libSmartAttributes/0.0.4/libSmartAttributes.js b/libSmartAttributes/0.0.4/libSmartAttributes.js index 79bf5693d..ac1d3d823 100644 --- a/libSmartAttributes/0.0.4/libSmartAttributes.js +++ b/libSmartAttributes/0.0.4/libSmartAttributes.js @@ -52,7 +52,6 @@ var libSmartAttributes = (function () { return false; } if (character?.sheetEnvironment === "legacy") { - // Try for legacy attribute first const legacyAttr = findObjs({ _type: "attribute", _characterid: characterId, @@ -64,10 +63,9 @@ var libSmartAttributes = (function () { } return false; } - // Then try for the beacon computed + // Beacon computeds cannot be deleted (no change to the computed value). const beaconAttr = await getSheetItem(characterId, name, type); if (beaconAttr !== null && beaconAttr !== undefined) { - // Cannot delete beacon computed attributes. return false; } // Then try for the user attribute diff --git a/libSmartAttributes/src/index.ts b/libSmartAttributes/src/index.ts index c964fe6cf..93a1a7246 100644 --- a/libSmartAttributes/src/index.ts +++ b/libSmartAttributes/src/index.ts @@ -74,8 +74,8 @@ async function deleteAttribute(characterId: string, name: string, type: Attribut if(!character) { return false; } - if( character?.sheetEnvironment === "legacy"){ - // Try for legacy attribute first + + if (character?.sheetEnvironment === "legacy") { const legacyAttr = findObjs({ _type: "attribute", _characterid: characterId, @@ -89,10 +89,9 @@ async function deleteAttribute(characterId: string, name: string, type: Attribut return false; } - // Then try for the beacon computed + // Beacon computeds cannot be deleted (no change to the computed value). const beaconAttr = await getSheetItem(characterId, name, type); if (beaconAttr !== null && beaconAttr !== undefined) { - // Cannot delete beacon computed attributes. return false; } diff --git a/libSmartAttributes/tests/index.test.ts b/libSmartAttributes/tests/index.test.ts index 645796473..95f7de0b6 100644 --- a/libSmartAttributes/tests/index.test.ts +++ b/libSmartAttributes/tests/index.test.ts @@ -298,17 +298,39 @@ describe("SmartAttributes", () => { expect(result).toBe(false); }); - it("should return false when a beacon computed exists", async () => { + it("should return false when a beacon computed exists and no legacy attribute exists", async () => { mockGetSheetItem.mockResolvedValueOnce("10"); const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + expect(mockFindObjs).toHaveBeenCalledTimes(0); expect(mockGetSheetItem).toHaveBeenCalledTimes(1); expect(mockGetSheetItem).toHaveBeenCalledWith(characterId, attributeName, "current"); expect(mockSetSheetItem).not.toHaveBeenCalled(); expect(result).toBe(false); }); + it("should return false for falsy beacon computed values without calling setSheetItem", async () => { + mockGetSheetItem.mockResolvedValueOnce(0); + + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + + expect(mockSetSheetItem).not.toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it("should ignore legacy attribute on beacon character before checking computed", async () => { + const mockRemove = vi.fn(); + mockGetObj.mockReturnValue({ sheetEnvironment: "beacon" }); + mockFindObjs.mockReturnValue([{ remove: mockRemove }]); + + const result = await SmartAttributes.deleteAttribute(characterId, attributeName); + + expect(mockRemove).not.toHaveBeenCalled(); + expect(mockGetSheetItem).toHaveBeenCalledTimes(2); + expect(result).toBe(false); + }); + it("should return true when deleting an existing user attribute", async () => { mockGetSheetItem .mockResolvedValueOnce(null) From 298a95038754ec93e03016e51bc9ba29d04a1042 Mon Sep 17 00:00:00 2001 From: "Aaron C. Meadows" Date: Tue, 16 Jun 2026 10:59:39 -0500 Subject: [PATCH 7/7] Fixed legacy detection in default --- libSmartAttributes/0.0.4/libSmartAttributes.js | 2 +- libSmartAttributes/src/.index.ts.swp | Bin 12288 -> 0 bytes libSmartAttributes/src/index.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 libSmartAttributes/src/.index.ts.swp diff --git a/libSmartAttributes/0.0.4/libSmartAttributes.js b/libSmartAttributes/0.0.4/libSmartAttributes.js index ac1d3d823..a05379fb7 100644 --- a/libSmartAttributes/0.0.4/libSmartAttributes.js +++ b/libSmartAttributes/0.0.4/libSmartAttributes.js @@ -51,7 +51,7 @@ var libSmartAttributes = (function () { if (!character) { return false; } - if (character?.sheetEnvironment === "legacy") { + if (character?.sheetEnvironment === "legacy" || character?.sheetEnvironment === undefined) { const legacyAttr = findObjs({ _type: "attribute", _characterid: characterId, diff --git a/libSmartAttributes/src/.index.ts.swp b/libSmartAttributes/src/.index.ts.swp deleted file mode 100644 index e7009e29c239c94caa13c82150bf47623f2f483d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2&u<$=6vrq0E>I{xB@V!gNsA*HJA_jmOjNZTOsGL`Pci|i32-W*s$BnE2?AbkL$b<+yYA#fY2Tr0=|~`4!9!7Uo!VTJ2Xdd3fm4g52uopLl^o9&t3?@ZZGh=D3K#{3 zP+%u}YVyfA)gQ}^(tY>7IK(?I9d0!9I&fKk9GU=%P47zK<1MggOMQNSp09V)@u}{Ej;1GBo%z(e4@n7H~_ye2==fKzC3!wJkQ(!(u0i%FXAXcC=#fWU&l9EE- zjVhoi6y3X0>UpW}S**7FK;peRk6cOXjK%W=LPmk7W_10f{Gy(a#@6LpOLWNdCDr}N zv&r|!YVd$tQUvJMKUeKeK@Dp--V#YaHEnTxiec6cAnL9Xn>p7ILRO_{=6fb7&|(yd zpg6Yf@uui376^-qy5kAEK*|#0|WXII2vR{Zi-wkpGjoI2&zyUJl2 ztyIX1T$lFji6_*J+JbQiMr8S3D7zyn#M|7FO6Vay(r%PWG~W;&$zToY1|%C|z?CZ% zPx-y2Wc#_N_A&_^stFEn;8C-Mt9AC3Nm!&gA!GTm-C84ZppM{nSKwq3t!5_iK(n5p zSLB{C+smBzzNNSW{$5^PS1fUBP1#!vMAJVi@;X+!UCK(tLTCKdiG?I0)$(PEjX(#{ z5gpZzUGC;g)mB-FZe?hhay*OLoN7;EtGf(U2(fR^)|SJ(iaixEX|N=NdbX_Y1fgU+ zT=OidN1i1eZ0$gW=uNdUg}77cEg@}V)3c|yLx$FAv`9|Qk%T(uXC{#1-8!~Z^ayLSHDS--cDaaBytEWf=zG((yuvAM^ z+d+|O@Ky_{k1a}rGF0U-R3%B30iBYzFGBBeNn+JO)j^SPLKISNKjC}s8m#Y-#iiAV z&FL?(*`^Q{y!P92Z8wIfcWYEkq#G8O*3EY)$@vZYq6#YVGbtrmAeL3VnoXh-!fg}g zYW;ZLGQxGm^V^B|&6>m08l$I}EFOhH($lPRAbxLN#8m#OCaxau4qz8Klfl-@37 zYmfCv$NrPMn^CG3Z8*b~)kB`)mpQQYQK#mjqttdXJyD_o@z_ tv_^BmPzI4kR6o^8=-w-lSZUujXiq{RhdE(=h-5 diff --git a/libSmartAttributes/src/index.ts b/libSmartAttributes/src/index.ts index 93a1a7246..f17837576 100644 --- a/libSmartAttributes/src/index.ts +++ b/libSmartAttributes/src/index.ts @@ -75,7 +75,7 @@ async function deleteAttribute(characterId: string, name: string, type: Attribut return false; } - if (character?.sheetEnvironment === "legacy") { + if (character?.sheetEnvironment === "legacy" || character?.sheetEnvironment === undefined) { const legacyAttr = findObjs({ _type: "attribute", _characterid: characterId,