diff --git a/src/appsscript/sheet/getSchema.ts b/src/appsscript/sheet/getSchema.ts index aae152fb..c68d6915 100644 --- a/src/appsscript/sheet/getSchema.ts +++ b/src/appsscript/sheet/getSchema.ts @@ -1,5 +1,5 @@ import { isNil } from "../../lang"; -import { SCHEMA_METADATA_KEY } from "./insertSchema"; +import { LEGACY_SCHEMA_METADATA_KEYS, SCHEMA_METADATA_KEY } from "./insertSchema"; import { requireSheet } from "./requireSheet"; import { type SheetColumnSchema, type SheetColumnType, type SheetSchema } from "./types"; @@ -10,11 +10,23 @@ import { type SheetColumnSchema, type SheetColumnType, type SheetSchema } from " * @returns {SheetSchema | null} The stored schema, or `null` when there is none or it cannot be read. */ function readStoredSchema(sheet: GoogleAppsScript.Spreadsheet.Sheet): SheetSchema | null { - for (const metadata of sheet.getDeveloperMetadata()) { - if (metadata.getKey() !== SCHEMA_METADATA_KEY) { - continue; - } - + // A schema under the current key wins over one left under a legacy key. + const entries: GoogleAppsScript.Spreadsheet.DeveloperMetadata[] = sheet + .getDeveloperMetadata() + .filter( + (metadata: GoogleAppsScript.Spreadsheet.DeveloperMetadata): boolean => + metadata.getKey() === SCHEMA_METADATA_KEY || + LEGACY_SCHEMA_METADATA_KEYS.includes(metadata.getKey()) + ) + .sort( + ( + a: GoogleAppsScript.Spreadsheet.DeveloperMetadata, + b: GoogleAppsScript.Spreadsheet.DeveloperMetadata + ): number => + Number(b.getKey() === SCHEMA_METADATA_KEY) - Number(a.getKey() === SCHEMA_METADATA_KEY) + ); + + for (const metadata of entries) { try { const parsed: unknown = JSON.parse(metadata.getValue() ?? ""); diff --git a/src/appsscript/sheet/insertSchema.ts b/src/appsscript/sheet/insertSchema.ts index 9dba52c6..cdbfc86c 100644 --- a/src/appsscript/sheet/insertSchema.ts +++ b/src/appsscript/sheet/insertSchema.ts @@ -6,7 +6,14 @@ import { type SheetColumnSchema, type SheetSchema } from "./types"; /** * The developer-metadata key a sheet's schema is stored under. */ -export const SCHEMA_METADATA_KEY: string = "apps-script-utils:schema"; +export const SCHEMA_METADATA_KEY: string = "bootgs-utils:schema"; + +/** + * Keys earlier versions stored the schema under (`apps-script-utils` 2.x and older). They are + * still read and removed, and replaced by {@link SCHEMA_METADATA_KEY} the next time a schema is + * written, so spreadsheets migrate on their own. + */ +export const LEGACY_SCHEMA_METADATA_KEYS: readonly string[] = ["apps-script-utils:schema"]; /** * Rejects anything that is not a usable schema. @@ -51,7 +58,9 @@ function requireSchema(schema: unknown): SheetSchema { */ function removeStoredSchema(sheet: GoogleAppsScript.Spreadsheet.Sheet): void { for (const metadata of sheet.getDeveloperMetadata()) { - if (metadata.getKey() === SCHEMA_METADATA_KEY) { + const key: string = metadata.getKey(); + + if (key === SCHEMA_METADATA_KEY || LEGACY_SCHEMA_METADATA_KEYS.includes(key)) { metadata.remove(); } } diff --git a/src/appsscript/sheet/removeSchema.ts b/src/appsscript/sheet/removeSchema.ts index f5e3d2ee..90601e06 100644 --- a/src/appsscript/sheet/removeSchema.ts +++ b/src/appsscript/sheet/removeSchema.ts @@ -1,5 +1,5 @@ import { isNil } from "../../lang"; -import { SCHEMA_METADATA_KEY } from "./insertSchema"; +import { LEGACY_SCHEMA_METADATA_KEYS, SCHEMA_METADATA_KEY } from "./insertSchema"; import { requireSheet } from "./requireSheet"; import { type SheetSchema } from "./types"; @@ -75,7 +75,9 @@ export function removeSchema( let removed: boolean = false; for (const metadata of sheet.getDeveloperMetadata()) { - if (metadata.getKey() !== SCHEMA_METADATA_KEY) { + const key: string = metadata.getKey(); + + if (key !== SCHEMA_METADATA_KEY && !LEGACY_SCHEMA_METADATA_KEYS.includes(key)) { continue; } diff --git a/test/appsscript/sheet/getSchema.test.ts b/test/appsscript/sheet/getSchema.test.ts index 58516564..d0d166e8 100644 --- a/test/appsscript/sheet/getSchema.test.ts +++ b/test/appsscript/sheet/getSchema.test.ts @@ -28,7 +28,9 @@ function sheetMock( } as unknown as GoogleAppsScript.Spreadsheet.Sheet; } -const KEY = "apps-script-utils:schema"; +const KEY = "bootgs-utils:schema"; + +const LEGACY_KEY = "apps-script-utils:schema"; describe("getSchema", () => { describe("A stored schema", () => { @@ -52,6 +54,29 @@ describe("getSchema", () => { expect(getSchema(sheet)?.columns).toStrictEqual([{ name: "Declared" }]); }); + it("should read a schema stored under the legacy key", () => { + const stored = { version: 1, headerRow: 1, columns: [{ name: "Legacy" }] }; + + const sheet = sheetMock([["Inferred"]], [{ key: LEGACY_KEY, value: JSON.stringify(stored) }]); + + expect(getSchema(sheet)).toStrictEqual(stored); + }); + + it("should prefer the current key over the legacy key", () => { + const legacy = { version: 1, headerRow: 1, columns: [{ name: "Legacy" }] }; + const current = { version: 1, headerRow: 1, columns: [{ name: "Current" }] }; + + const sheet = sheetMock( + [["Inferred"]], + [ + { key: LEGACY_KEY, value: JSON.stringify(legacy) }, + { key: KEY, value: JSON.stringify(current) } + ] + ); + + expect(getSchema(sheet)).toStrictEqual(current); + }); + it("should ignore metadata under other keys", () => { const sheet = sheetMock([["Id"], [1]], [{ key: "other", value: "{}" }]); diff --git a/test/appsscript/sheet/insertSchema.test.ts b/test/appsscript/sheet/insertSchema.test.ts index 17d19b0a..5c103325 100644 --- a/test/appsscript/sheet/insertSchema.test.ts +++ b/test/appsscript/sheet/insertSchema.test.ts @@ -204,7 +204,7 @@ describe("insertSchema", () => { insertSchema(mock.sheet, { version: 1, headerRow: 1, columns: [{ name: "Id" }] }); expect(mock.metadata).toHaveLength(1); - expect(mock.metadata[0].key).toBe("apps-script-utils:schema"); + expect(mock.metadata[0].key).toBe("bootgs-utils:schema"); expect(JSON.parse(mock.metadata[0].value)).toStrictEqual({ version: 1, headerRow: 1, @@ -231,7 +231,19 @@ describe("insertSchema", () => { expect(mock.metadata.map((m: { key: string }) => m.key)).toStrictEqual([ "other", - "apps-script-utils:schema" + "bootgs-utils:schema" + ]); + }); + + it("should migrate a schema stored under the legacy key", () => { + const mock = sheetMock(); + + mock.sheet.addDeveloperMetadata("apps-script-utils:schema", JSON.stringify({ version: 1 })); + + insertSchema(mock.sheet, { version: 1, headerRow: 1, columns: [{ name: "Id" }] }); + + expect(mock.metadata.map((m: { key: string }) => m.key)).toStrictEqual([ + "bootgs-utils:schema" ]); }); diff --git a/test/appsscript/sheet/removeSchema.test.ts b/test/appsscript/sheet/removeSchema.test.ts index 396fa0f5..f4c036d3 100644 --- a/test/appsscript/sheet/removeSchema.test.ts +++ b/test/appsscript/sheet/removeSchema.test.ts @@ -21,7 +21,9 @@ interface SheetMock { calls: RangeCall[]; } -const KEY = "apps-script-utils:schema"; +const KEY = "bootgs-utils:schema"; + +const LEGACY_KEY = "apps-script-utils:schema"; /** * A stand-in sheet carrying the given metadata, recording range calls. @@ -104,6 +106,17 @@ describe("removeSchema", () => { expect(mock.metadata).toStrictEqual([{ key: "other", value: "keep me" }]); }); + it("should remove a schema stored under the legacy key", () => { + const mock = sheetMock([ + { key: "other", value: "keep me" }, + { key: LEGACY_KEY, value: schema }, + { key: KEY, value: schema } + ]); + + expect(removeSchema(mock.sheet)).toBe(true); + expect(mock.metadata).toStrictEqual([{ key: "other", value: "keep me" }]); + }); + it("should remove metadata that will not parse", () => { const mock = sheetMock([{ key: KEY, value: "not json" }]);