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
3 changes: 2 additions & 1 deletion bundle/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { v4 as uuid } from "uuid";
import { jrefTypeOf } from "@hyperjump/browser/jref";
import * as JsonPointer from "@hyperjump/json-pointer";
import { resolveIri, toAbsoluteIri } from "@hyperjump/uri";
import { hasSchema } from "../lib/index.js";
import { getSchema, toSchema, getKeywordName } from "../lib/experimental.js";


Expand Down Expand Up @@ -35,7 +36,7 @@ const doBundling = async (schemaUri, bundled, fullOptions, contextSchema, visite
const schema = await getSchema(schemaUri, contextSchema);
for (const reference of allReferences(schema.document.root)) {
const uri = toAbsoluteIri(resolveIri(reference.href, schema.document.baseUri));
if (visited.has(uri) || fullOptions.externalSchemas.includes(uri) || (uri in schema.document.embedded && !(uri in schema._cache))) {
if (visited.has(uri) || fullOptions.externalSchemas.includes(uri) || (uri in schema.document.embedded && !(uri in schema._cache) && !hasSchema(uri))) {
continue;
}

Expand Down
119 changes: 119 additions & 0 deletions lib/schema-cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { get as browserGet, RetrievalError } from "@hyperjump/browser";
import { MockAgent, setGlobalDispatcher } from "undici";
import { registerSchema, unregisterSchema } from "./index.js";
import { getSchema } from "./experimental.js";
import "../v1/index.js";


describe("Schema registry browser lookup", () => {
const domain = "https://cache.hyperjump.io";
const rootUri = `${domain}/root`;
const targetUri = `${domain}/target`;
const dialect = "https://json-schema.org/v1";
let mockAgent: MockAgent;

beforeEach(() => {
mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
registerSchema({ $schema: dialect }, rootUri);
});

afterEach(async () => {
unregisterSchema(rootUri);
unregisterSchema(targetUri);
await mockAgent.close();
});

const mockSchema = (type: string) => {
mockAgent.get(domain)
.intercept({ method: "GET", path: "/target" })
.reply(200, { $schema: dialect, type }, { headers: { "content-type": "application/schema+json" } });
};

it("doesn't find unregistered schemas in existing browsers", async () => {
registerSchema({ $schema: dialect, type: "string" }, targetUri);
const browser = await getSchema(rootUri);
expect((await getSchema(targetUri, browser)).document.root).to.eql({ type: "string" });
unregisterSchema(targetUri);

await expect(getSchema(targetUri, browser)).rejects.to.be.instanceof(RetrievalError);
});

it("finds replaced schemas in existing browsers", async () => {
registerSchema({ $schema: dialect, type: "string" }, targetUri);
const browser = await getSchema(rootUri);
expect((await getSchema(targetUri, browser)).document.root).to.eql({ type: "string" });
unregisterSchema(targetUri);
registerSchema({ $schema: dialect, type: "number" }, targetUri);

expect((await getSchema(targetUri, browser)).document.root).to.eql({ type: "number" });
});

it("finds schemas registered after a browser was created", async () => {
const browser = await getSchema(rootUri);
registerSchema({ $schema: dialect, type: "string" }, targetUri);

expect((await browserGet(targetUri, { ...browser })).document.root).to.eql({ type: "string" });
});

it("keeps fetched documents local to each independent browser", async () => {
const first = await getSchema(rootUri);
const second = await getSchema(rootUri);
mockSchema("string");
expect((await getSchema(targetUri, first)).document.root).to.eql({ type: "string" });
mockSchema("number");
expect((await getSchema(targetUri, second)).document.root).to.eql({ type: "number" });
expect((await getSchema(targetUri, first)).document.root).to.eql({ type: "string" });
mockAgent.assertNoPendingInterceptors();
});

it("preserves fetched documents when the same URI is subsequently registered", async () => {
mockSchema("string");
const browser = await getSchema(targetUri);
registerSchema({ $schema: dialect, type: "number" }, targetUri);

expect((await getSchema(targetUri, browser)).document.root).to.eql({ type: "string" });
expect((await getSchema(targetUri)).document.root).to.eql({ type: "number" });
mockAgent.assertNoPendingInterceptors();
});

it("prefers fetched documents over registered schemas in a browser created outside getSchema", async () => {
mockSchema("string");
const browser = await browserGet(targetUri);
registerSchema({ $schema: dialect, type: "number" }, targetUri);

expect((await getSchema(rootUri, browser)).document.root).to.eql({});
expect((await getSchema(targetUri, browser)).document.root).to.eql({ type: "string" });
mockAgent.assertNoPendingInterceptors();
});

it("finds schemas registered after a browser created outside getSchema was first used", async () => {
mockSchema("string");
const browser = await browserGet(targetUri);
await getSchema(rootUri, browser);
const laterUri = `${domain}/later`;
registerSchema({ $schema: dialect, type: "number" }, laterUri);

try {
expect((await getSchema(laterUri, browser)).document.root).to.eql({ type: "number" });
} finally {
unregisterSchema(laterUri);
}
});

it("follows relative references and anchors to registered schemas", async () => {
unregisterSchema(rootUri);
registerSchema({ $schema: dialect, $ref: "target#value" }, rootUri);
registerSchema({
$schema: dialect,
$defs: { value: { $anchor: "value", type: "string" } }
}, targetUri);
const browser = await getSchema(rootUri);

const referenced = await browserGet("#/$ref", { ...browser });
expect(referenced.document.baseUri).to.equal(targetUri);
expect(referenced.cursor).to.equal("/$defs/value");
});
});
14 changes: 2 additions & 12 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,8 @@ export const schemaPlugin = {

const schemaRegistry = {};

export const getSchema = async (uri, browser = undefined) => {
if (!browser) {
browser = { _cache: {} };
}

for (const uri in schemaRegistry) {
if (!(uri in browser._cache)) {
browser._cache[uri] = schemaRegistry[uri];
}
}

const schema = await browserGet(uri, { ...browser });
export const getSchema = async (uri, browser = { _cache: {} }) => {
const schema = await browserGet(uri, { ...browser, _registry: schemaRegistry });
if (typeof schema.document.dialectId !== "string") {
throw Error(`The document at ${schema.document.baseUri} is not a schema.`);
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"uuid": "^14.0.0"
},
"peerDependencies": {
"@hyperjump/browser": "^1.1.0"
"@hyperjump/browser": "^1.6.0"
},
"allowScripts": {
"unrs-resolver@1.12.2": true
Expand Down
Loading