Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions src/appsscript/sheet/getSchema.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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() ?? "");

Expand Down
13 changes: 11 additions & 2 deletions src/appsscript/sheet/insertSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/appsscript/sheet/removeSchema.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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;
}

Expand Down
27 changes: 26 additions & 1 deletion test/appsscript/sheet/getSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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: "{}" }]);

Expand Down
16 changes: 14 additions & 2 deletions test/appsscript/sheet/insertSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
]);
});

Expand Down
15 changes: 14 additions & 1 deletion test/appsscript/sheet/removeSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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" }]);

Expand Down