Merge pull request 'fix(core,fs): phase 3 cleanup — drop legacy Store, sync bootstrap, dedup VarStore' (#49) from fix/47-phase3-cleanup into main
This commit was merged in pull request #49.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import type { Hash, ListOptions, OcasStore } from "@ocas/core";
|
import type { Hash, ListOptions, Store } from "@ocas/core";
|
||||||
import {
|
import {
|
||||||
CasNodeNotFoundError,
|
CasNodeNotFoundError,
|
||||||
computeHash,
|
computeHash,
|
||||||
@@ -108,7 +108,7 @@ const compact = flags.json === true;
|
|||||||
|
|
||||||
const inlineRender = flags.render === true || flags.r === true;
|
const inlineRender = flags.render === true || flags.r === true;
|
||||||
|
|
||||||
async function out(data: unknown, store?: OcasStore): Promise<void> {
|
async function out(data: unknown, store?: Store): Promise<void> {
|
||||||
if (
|
if (
|
||||||
inlineRender &&
|
inlineRender &&
|
||||||
typeof data === "object" &&
|
typeof data === "object" &&
|
||||||
@@ -156,10 +156,10 @@ async function readStdinJson(): Promise<unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the filesystem-backed OcasStore. Automatically creates directory and
|
* Open the filesystem-backed Store. Automatically creates directory and
|
||||||
* bootstraps if needed.
|
* bootstraps if needed.
|
||||||
*/
|
*/
|
||||||
async function openStore(): Promise<OcasStore> {
|
async function openStore(): Promise<Store> {
|
||||||
const fullPath = resolve(storePath);
|
const fullPath = resolve(storePath);
|
||||||
return await openFsStore(fullPath);
|
return await openFsStore(fullPath);
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ function isHash(input: string): boolean {
|
|||||||
* Otherwise, query the store's var sub-store for a variable with that exact
|
* Otherwise, query the store's var sub-store for a variable with that exact
|
||||||
* name and return the first match's value.
|
* name and return the first match's value.
|
||||||
*/
|
*/
|
||||||
function resolveHash(input: string, store: OcasStore): Hash {
|
function resolveHash(input: string, store: Store): Hash {
|
||||||
if (isHash(input)) {
|
if (isHash(input)) {
|
||||||
return input as Hash;
|
return input as Hash;
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ async function cmdPut(args: string[]): Promise<void> {
|
|||||||
const metaHash = resolveHash("@ocas/schema", store);
|
const metaHash = resolveHash("@ocas/schema", store);
|
||||||
if (typeHash === metaHash) {
|
if (typeHash === metaHash) {
|
||||||
try {
|
try {
|
||||||
const hash = await putSchema(store, payload as Record<string, unknown>);
|
const hash = putSchema(store, payload as Record<string, unknown>);
|
||||||
await out(await wrapEnvelope(store, "@ocas/output/put", hash), store);
|
await out(await wrapEnvelope(store, "@ocas/output/put", hash), store);
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe("usage.md doc cleanup (D)", () => {
|
|||||||
expect(content).not.toContain("createVariableStore");
|
expect(content).not.toContain("createVariableStore");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("D1. usage.md references openStore returning OcasStore", () => {
|
test("D1. usage.md references openStore returning Store", () => {
|
||||||
const content = readFileSync(usagePath, "utf8");
|
const content = readFileSync(usagePath, "utf8");
|
||||||
expect(content).toContain("openStore");
|
expect(content).toContain("openStore");
|
||||||
expect(content).toMatch(/store\.cas|store\.var|store\.tag/);
|
expect(content).toMatch(/store\.cas|store\.var|store\.tag/);
|
||||||
|
|||||||
@@ -70,10 +70,7 @@ describe("Phase 7: Edge Cases", () => {
|
|||||||
const { openStore: openFsStore } = await import("@ocas/fs");
|
const { openStore: openFsStore } = await import("@ocas/fs");
|
||||||
const { putSchema } = await import("@ocas/core");
|
const { putSchema } = await import("@ocas/core");
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
typeHash = await putSchema(
|
typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8")));
|
||||||
store,
|
|
||||||
JSON.parse(readFileSync(schemaFile, "utf-8")),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeFile = join(tmpStore, "test-node.json");
|
const nodeFile = join(tmpStore, "test-node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||||
@@ -178,10 +175,7 @@ describe("Phase 3: Variable System", () => {
|
|||||||
const { openStore: openFsStore } = await import("@ocas/fs");
|
const { openStore: openFsStore } = await import("@ocas/fs");
|
||||||
const { putSchema } = await import("@ocas/core");
|
const { putSchema } = await import("@ocas/core");
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
typeHash = await putSchema(
|
typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8")));
|
||||||
store,
|
|
||||||
JSON.parse(readFileSync(schemaFile, "utf-8")),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeFile = join(tmpStore, "test-node.json");
|
const nodeFile = join(tmpStore, "test-node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||||
@@ -369,10 +363,7 @@ describe("Phase 4: Template System", () => {
|
|||||||
const { openStore: openFsStore } = await import("@ocas/fs");
|
const { openStore: openFsStore } = await import("@ocas/fs");
|
||||||
const { putSchema } = await import("@ocas/core");
|
const { putSchema } = await import("@ocas/core");
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
typeHash = await putSchema(
|
typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8")));
|
||||||
store,
|
|
||||||
JSON.parse(readFileSync(schemaFile, "utf-8")),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ beforeAll(async () => {
|
|||||||
const { openStore: openFsStore } = await import("@ocas/fs");
|
const { openStore: openFsStore } = await import("@ocas/fs");
|
||||||
const { putSchema } = await import("@ocas/core");
|
const { putSchema } = await import("@ocas/core");
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
typeHash = await putSchema(
|
typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8")));
|
||||||
store,
|
|
||||||
JSON.parse(readFileSync(schemaFile, "utf-8")),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeFile = join(tmpStore, "test-node.json");
|
const nodeFile = join(tmpStore, "test-node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export async function putSchemaFile(
|
|||||||
const schema = JSON.parse(
|
const schema = JSON.parse(
|
||||||
readFileSync(schemaFilePath, "utf-8"),
|
readFileSync(schemaFilePath, "utf-8"),
|
||||||
) as JSONSchema;
|
) as JSONSchema;
|
||||||
const hash = await putSchema(store, schema);
|
const hash = putSchema(store, schema);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ beforeAll(async () => {
|
|||||||
const { openStore: openFsStore } = await import("@ocas/fs");
|
const { openStore: openFsStore } = await import("@ocas/fs");
|
||||||
const { putSchema } = await import("@ocas/core");
|
const { putSchema } = await import("@ocas/core");
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
typeHash = await putSchema(
|
typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8")));
|
||||||
store,
|
|
||||||
JSON.parse(readFileSync(schemaFile, "utf-8")),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeFile = join(tmpStore, "test-node.json");
|
const nodeFile = join(tmpStore, "test-node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||||
|
|||||||
@@ -29,10 +29,7 @@ beforeAll(async () => {
|
|||||||
const { openStore: openFsStore } = await import("@ocas/fs");
|
const { openStore: openFsStore } = await import("@ocas/fs");
|
||||||
const { putSchema } = await import("@ocas/core");
|
const { putSchema } = await import("@ocas/core");
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
typeHash = await putSchema(
|
typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8")));
|
||||||
store,
|
|
||||||
JSON.parse(readFileSync(schemaFile, "utf-8")),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeFile = join(tmpStore, "test-node.json");
|
const nodeFile = join(tmpStore, "test-node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||||
|
|||||||
@@ -103,10 +103,7 @@ describe("Phase 5: Render", () => {
|
|||||||
const { openStore: openFsStore } = await import("@ocas/fs");
|
const { openStore: openFsStore } = await import("@ocas/fs");
|
||||||
const { putSchema } = await import("@ocas/core");
|
const { putSchema } = await import("@ocas/core");
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
typeHash = await putSchema(
|
typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8")));
|
||||||
store,
|
|
||||||
JSON.parse(readFileSync(schemaFile, "utf-8")),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeFile = join(tmpStore, "test-node.json");
|
const nodeFile = join(tmpStore, "test-node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||||
@@ -422,7 +419,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
|
|
||||||
// Get @ocas/string type hash via bootstrap
|
// Get @ocas/string type hash via bootstrap
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
const types = await bootstrap(store);
|
const types = bootstrap(store);
|
||||||
const stringType = types["@ocas/string"];
|
const stringType = types["@ocas/string"];
|
||||||
|
|
||||||
// Create and store a simple string node
|
// Create and store a simple string node
|
||||||
@@ -457,7 +454,7 @@ describe("Suite 6: CLI Integration with Templates", () => {
|
|||||||
|
|
||||||
// Get @ocas/string type hash via bootstrap
|
// Get @ocas/string type hash via bootstrap
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
const types = await bootstrap(store);
|
const types = bootstrap(store);
|
||||||
const stringType = types["@ocas/string"];
|
const stringType = types["@ocas/string"];
|
||||||
|
|
||||||
// Create envelope and pipe to render
|
// Create envelope and pipe to render
|
||||||
|
|||||||
@@ -577,10 +577,7 @@ describe("Phase 2: Schema Validation", () => {
|
|||||||
const { openStore: openFsStore } = await import("@ocas/fs");
|
const { openStore: openFsStore } = await import("@ocas/fs");
|
||||||
const { putSchema } = await import("@ocas/core");
|
const { putSchema } = await import("@ocas/core");
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
typeHash = await putSchema(
|
typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8")));
|
||||||
store,
|
|
||||||
JSON.parse(readFileSync(schemaFile, "utf-8")),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeFile = join(tmpStore, "test-node.json");
|
const nodeFile = join(tmpStore, "test-node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|||||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { Hash, OcasStore } from "@ocas/core";
|
import type { Hash, Store } from "@ocas/core";
|
||||||
import { bootstrap } from "@ocas/core";
|
import { bootstrap } from "@ocas/core";
|
||||||
import { openStore as openFsStore } from "@ocas/fs";
|
import { openStore as openFsStore } from "@ocas/fs";
|
||||||
|
|
||||||
@@ -67,8 +67,8 @@ async function runCli(...args: string[]): Promise<{
|
|||||||
/**
|
/**
|
||||||
* Get bootstrap @ocas/string type hash
|
* Get bootstrap @ocas/string type hash
|
||||||
*/
|
*/
|
||||||
async function getStringHash(store: OcasStore): Promise<Hash> {
|
async function getStringHash(store: Store): Promise<Hash> {
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
return builtinSchemas["@ocas/string"] ?? "";
|
return builtinSchemas["@ocas/string"] ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|||||||
import { mkdirSync, rmSync } from "node:fs";
|
import { mkdirSync, rmSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { Hash, OcasStore } from "@ocas/core";
|
import type { Hash, Store } from "@ocas/core";
|
||||||
import { bootstrap } from "@ocas/core";
|
import { bootstrap } from "@ocas/core";
|
||||||
import { openStore as openFsStore } from "@ocas/fs";
|
import { openStore as openFsStore } from "@ocas/fs";
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ async function setupSchemaAndValues(): Promise<{
|
|||||||
schema: Hash;
|
schema: Hash;
|
||||||
values: Hash[];
|
values: Hash[];
|
||||||
}> {
|
}> {
|
||||||
const store: OcasStore = await openFsStore(storePath);
|
const store: Store = await openFsStore(storePath);
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const numberHash = aliases["@ocas/number"] as Hash;
|
const numberHash = aliases["@ocas/number"] as Hash;
|
||||||
const values: Hash[] = [];
|
const values: Hash[] = [];
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|||||||
import { mkdirSync, rmSync } from "node:fs";
|
import { mkdirSync, rmSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { Hash, OcasStore } from "@ocas/core";
|
import type { Hash, Store } from "@ocas/core";
|
||||||
import { bootstrap, putSchema } from "@ocas/core";
|
import { bootstrap, putSchema } from "@ocas/core";
|
||||||
import { openStore as openFsStore } from "@ocas/fs";
|
import { openStore as openFsStore } from "@ocas/fs";
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ async function runCli(...args: string[]): Promise<{
|
|||||||
* Create a test CAS node and return its hash
|
* Create a test CAS node and return its hash
|
||||||
*/
|
*/
|
||||||
async function createTestNode(
|
async function createTestNode(
|
||||||
store: OcasStore,
|
store: Store,
|
||||||
typeHash: Hash,
|
typeHash: Hash,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
): Promise<Hash> {
|
): Promise<Hash> {
|
||||||
@@ -78,8 +78,8 @@ async function createTestNode(
|
|||||||
/**
|
/**
|
||||||
* Get bootstrap type hash
|
* Get bootstrap type hash
|
||||||
*/
|
*/
|
||||||
async function getBootstrapHash(store: OcasStore): Promise<Hash> {
|
async function getBootstrapHash(store: Store): Promise<Hash> {
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
return builtinSchemas["@ocas/schema"] ?? "";
|
return builtinSchemas["@ocas/schema"] ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ describe("var set", () => {
|
|||||||
test("create variant with different schema", async () => {
|
test("create variant with different schema", async () => {
|
||||||
const store = await openFsStore(storePath);
|
const store = await openFsStore(storePath);
|
||||||
const typeHash1 = await getBootstrapHash(store);
|
const typeHash1 = await getBootstrapHash(store);
|
||||||
const typeHash2 = await putSchema(store, { title: "Test", type: "object" });
|
const typeHash2 = putSchema(store, { title: "Test", type: "object" });
|
||||||
const hash1 = await createTestNode(store, typeHash1, { test: "data1" });
|
const hash1 = await createTestNode(store, typeHash1, { test: "data1" });
|
||||||
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
|
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ describe("var get", () => {
|
|||||||
test("distinguish variants by schema", async () => {
|
test("distinguish variants by schema", async () => {
|
||||||
const store = await openFsStore(storePath);
|
const store = await openFsStore(storePath);
|
||||||
const typeHash1 = await getBootstrapHash(store);
|
const typeHash1 = await getBootstrapHash(store);
|
||||||
const typeHash2 = await putSchema(store, { title: "Test", type: "object" });
|
const typeHash2 = putSchema(store, { title: "Test", type: "object" });
|
||||||
const hash1 = await createTestNode(store, typeHash1, { test: "data1" });
|
const hash1 = await createTestNode(store, typeHash1, { test: "data1" });
|
||||||
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
|
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@ describe("var delete", () => {
|
|||||||
test("remove all schema variants", async () => {
|
test("remove all schema variants", async () => {
|
||||||
const store = await openFsStore(storePath);
|
const store = await openFsStore(storePath);
|
||||||
const typeHash1 = await getBootstrapHash(store);
|
const typeHash1 = await getBootstrapHash(store);
|
||||||
const typeHash2 = await putSchema(store, { title: "Test", type: "object" });
|
const typeHash2 = putSchema(store, { title: "Test", type: "object" });
|
||||||
const hash1 = await createTestNode(store, typeHash1, { test: "data1" });
|
const hash1 = await createTestNode(store, typeHash1, { test: "data1" });
|
||||||
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
|
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
|
||||||
|
|
||||||
@@ -432,7 +432,7 @@ describe("var delete", () => {
|
|||||||
test("remove specific variant by schema", async () => {
|
test("remove specific variant by schema", async () => {
|
||||||
const store = await openFsStore(storePath);
|
const store = await openFsStore(storePath);
|
||||||
const typeHash1 = await getBootstrapHash(store);
|
const typeHash1 = await getBootstrapHash(store);
|
||||||
const typeHash2 = await putSchema(store, { title: "Test", type: "object" });
|
const typeHash2 = putSchema(store, { title: "Test", type: "object" });
|
||||||
const hash1 = await createTestNode(store, typeHash1, { test: "data1" });
|
const hash1 = await createTestNode(store, typeHash1, { test: "data1" });
|
||||||
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
|
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
|
||||||
|
|
||||||
@@ -579,11 +579,11 @@ describe("var list", () => {
|
|||||||
test("filter by schema", async () => {
|
test("filter by schema", async () => {
|
||||||
const store = await openFsStore(storePath);
|
const store = await openFsStore(storePath);
|
||||||
const bootstrapHash = await getBootstrapHash(store);
|
const bootstrapHash = await getBootstrapHash(store);
|
||||||
const typeHash1 = await putSchema(store, {
|
const typeHash1 = putSchema(store, {
|
||||||
title: "TypeA",
|
title: "TypeA",
|
||||||
type: "object",
|
type: "object",
|
||||||
});
|
});
|
||||||
const typeHash2 = await putSchema(store, {
|
const typeHash2 = putSchema(store, {
|
||||||
title: "TypeB",
|
title: "TypeB",
|
||||||
type: "object",
|
type: "object",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ beforeAll(async () => {
|
|||||||
const { openStore: openFsStore } = await import("@ocas/fs");
|
const { openStore: openFsStore } = await import("@ocas/fs");
|
||||||
const { putSchema } = await import("@ocas/core");
|
const { putSchema } = await import("@ocas/core");
|
||||||
const store = await openFsStore(tmpStore);
|
const store = await openFsStore(tmpStore);
|
||||||
typeHash = await putSchema(
|
typeHash = putSchema(store, JSON.parse(readFileSync(schemaFile, "utf-8")));
|
||||||
store,
|
|
||||||
JSON.parse(readFileSync(schemaFile, "utf-8")),
|
|
||||||
);
|
|
||||||
|
|
||||||
const nodeFile = join(tmpStore, "test-node.json");
|
const nodeFile = join(tmpStore, "test-node.json");
|
||||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
writeFileSync(nodeFile, JSON.stringify({ name: "Alice", age: 30 }));
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { Hash, Store } from "./types.js";
|
import type { CasStore, Hash } from "./types.js";
|
||||||
|
|
||||||
/** @internal Store implementations attach this for bootstrap() only. */
|
/** @internal Store implementations attach this for bootstrap() only. */
|
||||||
export const BOOTSTRAP_STORE = Symbol.for("@ocas/core/bootstrap-store");
|
export const BOOTSTRAP_STORE = Symbol.for("@ocas/core/bootstrap-store");
|
||||||
|
|
||||||
export type BootstrapCapableStore = Store & {
|
export type BootstrapCapableStore = CasStore & {
|
||||||
[BOOTSTRAP_STORE](payload: unknown): Hash | Promise<Hash>;
|
[BOOTSTRAP_STORE](payload: unknown): Hash;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isBootstrapCapableStore(
|
export function isBootstrapCapableStore(
|
||||||
store: Store,
|
store: CasStore,
|
||||||
): store is BootstrapCapableStore {
|
): store is BootstrapCapableStore {
|
||||||
return (
|
return (
|
||||||
typeof (store as BootstrapCapableStore)[BOOTSTRAP_STORE] === "function"
|
typeof (store as BootstrapCapableStore)[BOOTSTRAP_STORE] === "function"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const OUTPUT_ALIASES = [
|
|||||||
describe("bootstrap - Built-in Schemas", () => {
|
describe("bootstrap - Built-in Schemas", () => {
|
||||||
test("should return map of 30 built-in schema aliases to hashes", async () => {
|
test("should return map of 30 built-in schema aliases to hashes", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
|
|
||||||
// Should return object with 9 primitive + 21 output aliases = 30
|
// Should return object with 9 primitive + 21 output aliases = 30
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/schema");
|
expect(builtinSchemas).toHaveProperty("@ocas/schema");
|
||||||
@@ -63,7 +63,7 @@ describe("bootstrap - Built-in Schemas", () => {
|
|||||||
|
|
||||||
test("should register @ocas/schema as meta-schema alias", async () => {
|
test("should register @ocas/schema as meta-schema alias", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
|
|
||||||
const metaHash = builtinSchemas["@ocas/schema"];
|
const metaHash = builtinSchemas["@ocas/schema"];
|
||||||
if (!metaHash) throw new Error("@ocas/schema not found");
|
if (!metaHash) throw new Error("@ocas/schema not found");
|
||||||
@@ -76,7 +76,7 @@ describe("bootstrap - Built-in Schemas", () => {
|
|||||||
|
|
||||||
test("should register @ocas/string schema correctly", async () => {
|
test("should register @ocas/string schema correctly", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
|
|
||||||
const stringHash = builtinSchemas["@ocas/string"];
|
const stringHash = builtinSchemas["@ocas/string"];
|
||||||
if (!stringHash) throw new Error("@ocas/string not found");
|
if (!stringHash) throw new Error("@ocas/string not found");
|
||||||
@@ -87,7 +87,7 @@ describe("bootstrap - Built-in Schemas", () => {
|
|||||||
|
|
||||||
test("should register @ocas/number schema correctly", async () => {
|
test("should register @ocas/number schema correctly", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
|
|
||||||
const numberHash = builtinSchemas["@ocas/number"];
|
const numberHash = builtinSchemas["@ocas/number"];
|
||||||
if (!numberHash) throw new Error("@ocas/number not found");
|
if (!numberHash) throw new Error("@ocas/number not found");
|
||||||
@@ -98,7 +98,7 @@ describe("bootstrap - Built-in Schemas", () => {
|
|||||||
|
|
||||||
test("should register @ocas/object schema correctly", async () => {
|
test("should register @ocas/object schema correctly", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
|
|
||||||
const objectHash = builtinSchemas["@ocas/object"];
|
const objectHash = builtinSchemas["@ocas/object"];
|
||||||
if (!objectHash) throw new Error("@ocas/object not found");
|
if (!objectHash) throw new Error("@ocas/object not found");
|
||||||
@@ -109,7 +109,7 @@ describe("bootstrap - Built-in Schemas", () => {
|
|||||||
|
|
||||||
test("should register @ocas/array schema correctly", async () => {
|
test("should register @ocas/array schema correctly", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
|
|
||||||
const arrayHash = builtinSchemas["@ocas/array"];
|
const arrayHash = builtinSchemas["@ocas/array"];
|
||||||
if (!arrayHash) throw new Error("@ocas/array not found");
|
if (!arrayHash) throw new Error("@ocas/array not found");
|
||||||
@@ -120,7 +120,7 @@ describe("bootstrap - Built-in Schemas", () => {
|
|||||||
|
|
||||||
test("should register @ocas/bool schema correctly", async () => {
|
test("should register @ocas/bool schema correctly", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
|
|
||||||
const boolHash = builtinSchemas["@ocas/bool"];
|
const boolHash = builtinSchemas["@ocas/bool"];
|
||||||
if (!boolHash) throw new Error("@ocas/bool not found");
|
if (!boolHash) throw new Error("@ocas/bool not found");
|
||||||
@@ -131,8 +131,8 @@ describe("bootstrap - Built-in Schemas", () => {
|
|||||||
|
|
||||||
test("should return same hashes on repeated bootstrap calls", async () => {
|
test("should return same hashes on repeated bootstrap calls", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const first = await bootstrap(store);
|
const first = bootstrap(store);
|
||||||
const second = await bootstrap(store);
|
const second = bootstrap(store);
|
||||||
|
|
||||||
expect(first).toEqual(second);
|
expect(first).toEqual(second);
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ describe("bootstrap - Built-in Schemas", () => {
|
|||||||
|
|
||||||
test("all built-in schemas should be typed by meta-schema", async () => {
|
test("all built-in schemas should be typed by meta-schema", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
|
|
||||||
const metaHash = builtinSchemas["@ocas/schema"];
|
const metaHash = builtinSchemas["@ocas/schema"];
|
||||||
if (!metaHash) throw new Error("@ocas/schema not found");
|
if (!metaHash) throw new Error("@ocas/schema not found");
|
||||||
@@ -169,7 +169,7 @@ describe("bootstrap - Built-in Schemas", () => {
|
|||||||
describe("bootstrap - @ocas/output/* Schemas", () => {
|
describe("bootstrap - @ocas/output/* Schemas", () => {
|
||||||
test("each @ocas/output/* schema has a title", async () => {
|
test("each @ocas/output/* schema has a title", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
|
|
||||||
for (const alias of OUTPUT_ALIASES) {
|
for (const alias of OUTPUT_ALIASES) {
|
||||||
const hash = aliases[alias];
|
const hash = aliases[alias];
|
||||||
@@ -184,7 +184,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("@ocas/output/put schema describes a ocas_ref string", async () => {
|
test("@ocas/output/put schema describes a ocas_ref string", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const hash = aliases["@ocas/output/put"];
|
const hash = aliases["@ocas/output/put"];
|
||||||
if (!hash) throw new Error("@ocas/output/put not found");
|
if (!hash) throw new Error("@ocas/output/put not found");
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("@ocas/output/get schema describes object with type, payload, timestamp", async () => {
|
test("@ocas/output/get schema describes object with type, payload, timestamp", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const hash = aliases["@ocas/output/get"];
|
const hash = aliases["@ocas/output/get"];
|
||||||
if (!hash) throw new Error("@ocas/output/get not found");
|
if (!hash) throw new Error("@ocas/output/get not found");
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("@ocas/output/has schema describes a boolean", async () => {
|
test("@ocas/output/has schema describes a boolean", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const hash = aliases["@ocas/output/has"];
|
const hash = aliases["@ocas/output/has"];
|
||||||
if (!hash) throw new Error("@ocas/output/has not found");
|
if (!hash) throw new Error("@ocas/output/has not found");
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("@ocas/output/verify schema describes enum of ok|corrupted|invalid", async () => {
|
test("@ocas/output/verify schema describes enum of ok|corrupted|invalid", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const hash = aliases["@ocas/output/verify"];
|
const hash = aliases["@ocas/output/verify"];
|
||||||
if (!hash) throw new Error("@ocas/output/verify not found");
|
if (!hash) throw new Error("@ocas/output/verify not found");
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("@ocas/output/refs schema describes array of ocas_ref strings", async () => {
|
test("@ocas/output/refs schema describes array of ocas_ref strings", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const hash = aliases["@ocas/output/refs"];
|
const hash = aliases["@ocas/output/refs"];
|
||||||
if (!hash) throw new Error("@ocas/output/refs not found");
|
if (!hash) throw new Error("@ocas/output/refs not found");
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("@ocas/output/gc schema describes object with gc stats fields", async () => {
|
test("@ocas/output/gc schema describes object with gc stats fields", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const hash = aliases["@ocas/output/gc"];
|
const hash = aliases["@ocas/output/gc"];
|
||||||
if (!hash) throw new Error("@ocas/output/gc not found");
|
if (!hash) throw new Error("@ocas/output/gc not found");
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("@ocas/output/var-set schema describes a Variable object", async () => {
|
test("@ocas/output/var-set schema describes a Variable object", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const hash = aliases["@ocas/output/var-set"];
|
const hash = aliases["@ocas/output/var-set"];
|
||||||
if (!hash) throw new Error("@ocas/output/var-set not found");
|
if (!hash) throw new Error("@ocas/output/var-set not found");
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("@ocas/output/var-list schema describes array of Variable objects", async () => {
|
test("@ocas/output/var-list schema describes array of Variable objects", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const hash = aliases["@ocas/output/var-list"];
|
const hash = aliases["@ocas/output/var-list"];
|
||||||
if (!hash) throw new Error("@ocas/output/var-list not found");
|
if (!hash) throw new Error("@ocas/output/var-list not found");
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("@ocas/output/template-delete schema describes object with deleted boolean", async () => {
|
test("@ocas/output/template-delete schema describes object with deleted boolean", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const hash = aliases["@ocas/output/template-delete"];
|
const hash = aliases["@ocas/output/template-delete"];
|
||||||
if (!hash) throw new Error("@ocas/output/template-delete not found");
|
if (!hash) throw new Error("@ocas/output/template-delete not found");
|
||||||
|
|
||||||
@@ -315,7 +315,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
|
|
||||||
test("all @ocas/output/* schemas are distinct hashes", async () => {
|
test("all @ocas/output/* schemas are distinct hashes", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
|
|
||||||
const outputHashes = OUTPUT_ALIASES.map((alias) => aliases[alias]);
|
const outputHashes = OUTPUT_ALIASES.map((alias) => aliases[alias]);
|
||||||
const uniqueHashes = new Set(outputHashes);
|
const uniqueHashes = new Set(outputHashes);
|
||||||
@@ -326,7 +326,7 @@ describe("bootstrap - @ocas/output/* Schemas", () => {
|
|||||||
describe("bootstrap - meta and schemas indexes (D1)", () => {
|
describe("bootstrap - meta and schemas indexes (D1)", () => {
|
||||||
test("listMeta contains the bootstrap meta-schema hash", async () => {
|
test("listMeta contains the bootstrap meta-schema hash", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const metaHash = aliases["@ocas/schema"];
|
const metaHash = aliases["@ocas/schema"];
|
||||||
expect(store.cas.listMeta().map((e) => e.hash)).toContain(
|
expect(store.cas.listMeta().map((e) => e.hash)).toContain(
|
||||||
metaHash as string,
|
metaHash as string,
|
||||||
@@ -335,7 +335,7 @@ describe("bootstrap - meta and schemas indexes (D1)", () => {
|
|||||||
|
|
||||||
test("listSchemas contains meta-schema and all built-in schemas", async () => {
|
test("listSchemas contains meta-schema and all built-in schemas", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const schemas = store.cas.listSchemas().map((e) => e.hash);
|
const schemas = store.cas.listSchemas().map((e) => e.hash);
|
||||||
|
|
||||||
for (const [, hash] of Object.entries(aliases)) {
|
for (const [, hash] of Object.entries(aliases)) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
BOOTSTRAP_STORE,
|
BOOTSTRAP_STORE,
|
||||||
isBootstrapCapableStore,
|
isBootstrapCapableStore,
|
||||||
} from "./bootstrap-capable.js";
|
} from "./bootstrap-capable.js";
|
||||||
import type { Hash, OcasStore } from "./types.js";
|
import type { Hash, Store } from "./types.js";
|
||||||
|
|
||||||
const JSON_SCHEMA_TYPES = [
|
const JSON_SCHEMA_TYPES = [
|
||||||
"string",
|
"string",
|
||||||
@@ -328,25 +328,23 @@ const OUTPUT_SCHEMAS: ReadonlyArray<
|
|||||||
* All aliases are written to `store.var` via `var.set(name, hash)`, bypassing
|
* All aliases are written to `store.var` via `var.set(name, hash)`, bypassing
|
||||||
* @ocas/ namespace protection (protection is enforced only at the CLI layer).
|
* @ocas/ namespace protection (protection is enforced only at the CLI layer).
|
||||||
*/
|
*/
|
||||||
export async function bootstrap(
|
export function bootstrap(store: Store): Record<string, Hash> {
|
||||||
store: OcasStore,
|
|
||||||
): Promise<Record<string, Hash>> {
|
|
||||||
const cas = store.cas;
|
const cas = store.cas;
|
||||||
if (!isBootstrapCapableStore(cas)) {
|
if (!isBootstrapCapableStore(cas)) {
|
||||||
throw new Error("Store does not support bootstrap");
|
throw new Error("Store does not support bootstrap");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Bootstrap the meta-schema (self-referential)
|
// 1. Bootstrap the meta-schema (self-referential)
|
||||||
const metaHash = await cas[BOOTSTRAP_STORE](BOOTSTRAP_PAYLOAD);
|
const metaHash = cas[BOOTSTRAP_STORE](BOOTSTRAP_PAYLOAD);
|
||||||
|
|
||||||
// 2. Register built-in primitive schemas directly (without putSchema to avoid recursion)
|
// 2. Register built-in primitive schemas directly (without putSchema to avoid recursion)
|
||||||
const stringHash = await cas.put(metaHash, { type: "string" });
|
const stringHash = cas.put(metaHash, { type: "string" });
|
||||||
const numberHash = await cas.put(metaHash, { type: "number" });
|
const numberHash = cas.put(metaHash, { type: "number" });
|
||||||
const integerHash = await cas.put(metaHash, { type: "integer" });
|
const integerHash = cas.put(metaHash, { type: "integer" });
|
||||||
const boolHash = await cas.put(metaHash, { type: "boolean" });
|
const boolHash = cas.put(metaHash, { type: "boolean" });
|
||||||
const objectHash = await cas.put(metaHash, { type: "object" });
|
const objectHash = cas.put(metaHash, { type: "object" });
|
||||||
const arrayHash = await cas.put(metaHash, { type: "array" });
|
const arrayHash = cas.put(metaHash, { type: "array" });
|
||||||
const nullHash = await cas.put(metaHash, { type: "null" });
|
const nullHash = cas.put(metaHash, { type: "null" });
|
||||||
|
|
||||||
// 3. Register @ocas/output/* schemas
|
// 3. Register @ocas/output/* schemas
|
||||||
const aliases: Record<string, Hash> = {
|
const aliases: Record<string, Hash> = {
|
||||||
@@ -362,7 +360,7 @@ export async function bootstrap(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const [alias, schema] of OUTPUT_SCHEMAS) {
|
for (const [alias, schema] of OUTPUT_SCHEMAS) {
|
||||||
aliases[alias] = await cas.put(metaHash, schema);
|
aliases[alias] = cas.put(metaHash, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Write all aliases to the var store. Idempotent: VarStore.set is an
|
// 4. Write all aliases to the var store. Idempotent: VarStore.set is an
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { createMemoryStore } from "./store.js";
|
|||||||
describe("GC - Variable Model Refactoring", () => {
|
describe("GC - Variable Model Refactoring", () => {
|
||||||
test("GC preserves variable-referenced nodes", async () => {
|
test("GC preserves variable-referenced nodes", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const schema = { type: "object", properties: { name: { type: "string" } } };
|
const schema = { type: "object", properties: { name: { type: "string" } } };
|
||||||
const schemaHash = await putSchema(store, schema);
|
const schemaHash = putSchema(store, schema);
|
||||||
|
|
||||||
const hashRef = store.cas.put(schemaHash, { name: "referenced" });
|
const hashRef = store.cas.put(schemaHash, { name: "referenced" });
|
||||||
const hashOrphan = store.cas.put(schemaHash, { name: "orphan" });
|
const hashOrphan = store.cas.put(schemaHash, { name: "orphan" });
|
||||||
@@ -26,11 +26,11 @@ describe("GC - Variable Model Refactoring", () => {
|
|||||||
|
|
||||||
test("GC preserves nodes from variables with same name, different schemas", async () => {
|
test("GC preserves nodes from variables with same name, different schemas", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const schemaA = { type: "object", properties: { x: { type: "number" } } };
|
const schemaA = { type: "object", properties: { x: { type: "number" } } };
|
||||||
const schemaB = { type: "object", properties: { y: { type: "string" } } };
|
const schemaB = { type: "object", properties: { y: { type: "string" } } };
|
||||||
const schemaAHash = await putSchema(store, schemaA);
|
const schemaAHash = putSchema(store, schemaA);
|
||||||
const schemaBHash = await putSchema(store, schemaB);
|
const schemaBHash = putSchema(store, schemaB);
|
||||||
|
|
||||||
const hashA = store.cas.put(schemaAHash, { x: 42 });
|
const hashA = store.cas.put(schemaAHash, { x: 42 });
|
||||||
const hashB = store.cas.put(schemaBHash, { y: "hello" });
|
const hashB = store.cas.put(schemaBHash, { y: "hello" });
|
||||||
@@ -48,9 +48,9 @@ describe("GC - Variable Model Refactoring", () => {
|
|||||||
|
|
||||||
test("GC removes nodes after variable deletion", async () => {
|
test("GC removes nodes after variable deletion", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const schema = { type: "object", properties: { name: { type: "string" } } };
|
const schema = { type: "object", properties: { name: { type: "string" } } };
|
||||||
const schemaHash = await putSchema(store, schema);
|
const schemaHash = putSchema(store, schema);
|
||||||
|
|
||||||
const hashRef = store.cas.put(schemaHash, { name: "referenced" });
|
const hashRef = store.cas.put(schemaHash, { name: "referenced" });
|
||||||
|
|
||||||
@@ -64,11 +64,11 @@ describe("GC - Variable Model Refactoring", () => {
|
|||||||
|
|
||||||
test("GC is global across all variables", async () => {
|
test("GC is global across all variables", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const schemaA = { type: "object", properties: { x: { type: "number" } } };
|
const schemaA = { type: "object", properties: { x: { type: "number" } } };
|
||||||
const schemaB = { type: "object", properties: { y: { type: "string" } } };
|
const schemaB = { type: "object", properties: { y: { type: "string" } } };
|
||||||
const schemaAHash = await putSchema(store, schemaA);
|
const schemaAHash = putSchema(store, schemaA);
|
||||||
const schemaBHash = await putSchema(store, schemaB);
|
const schemaBHash = putSchema(store, schemaB);
|
||||||
|
|
||||||
const hash1 = store.cas.put(schemaAHash, { x: 1 });
|
const hash1 = store.cas.put(schemaAHash, { x: 1 });
|
||||||
const hash2 = store.cas.put(schemaAHash, { x: 2 });
|
const hash2 = store.cas.put(schemaAHash, { x: 2 });
|
||||||
@@ -89,12 +89,12 @@ describe("GC - Variable Model Refactoring", () => {
|
|||||||
|
|
||||||
test("GC integration with refactored variable store", async () => {
|
test("GC integration with refactored variable store", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const schemaA = { type: "object", properties: { x: { type: "number" } } };
|
const schemaA = { type: "object", properties: { x: { type: "number" } } };
|
||||||
const schemaB = { type: "object", properties: { y: { type: "string" } } };
|
const schemaB = { type: "object", properties: { y: { type: "string" } } };
|
||||||
const schemaAHash = await putSchema(store, schemaA);
|
const schemaAHash = putSchema(store, schemaA);
|
||||||
const schemaBHash = await putSchema(store, schemaB);
|
const schemaBHash = putSchema(store, schemaB);
|
||||||
|
|
||||||
const hashA1 = store.cas.put(schemaAHash, { x: 1 });
|
const hashA1 = store.cas.put(schemaAHash, { x: 1 });
|
||||||
const hashA2 = store.cas.put(schemaAHash, { x: 2 });
|
const hashA2 = store.cas.put(schemaAHash, { x: 2 });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { walk } from "./schema.js";
|
import { walk } from "./schema.js";
|
||||||
import type { Hash, OcasStore } from "./types.js";
|
import type { Hash, Store } from "./types.js";
|
||||||
|
|
||||||
export interface GcStats {
|
export interface GcStats {
|
||||||
total: number; // Total CAS nodes before GC
|
total: number; // Total CAS nodes before GC
|
||||||
@@ -15,7 +15,7 @@ export interface GcStats {
|
|||||||
* - Sweep: delete unmarked nodes
|
* - Sweep: delete unmarked nodes
|
||||||
* - Schema preservation: schemas of reachable nodes are also marked
|
* - Schema preservation: schemas of reachable nodes are also marked
|
||||||
*/
|
*/
|
||||||
export function gc(store: OcasStore): GcStats {
|
export function gc(store: Store): GcStats {
|
||||||
// Get all variables (no filters → global). Omit `limit` so the full
|
// Get all variables (no filters → global). Omit `limit` so the full
|
||||||
// variable set is returned for use as gc roots.
|
// variable set is returned for use as gc roots.
|
||||||
const variables = store.var.list();
|
const variables = store.var.list();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { bootstrap } from "./bootstrap.js";
|
|||||||
import { cborEncode } from "./cbor.js";
|
import { cborEncode } from "./cbor.js";
|
||||||
import { computeHash, computeSelfHash } from "./hash.js";
|
import { computeHash, computeSelfHash } from "./hash.js";
|
||||||
import { createMemoryStore } from "./store.js";
|
import { createMemoryStore } from "./store.js";
|
||||||
import type { CasNode, Store } from "./types.js";
|
import type { CasNode, CasStore } from "./types.js";
|
||||||
import { verify } from "./verify.js";
|
import { verify } from "./verify.js";
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -197,7 +197,7 @@ describe("createMemoryStore – listByType", () => {
|
|||||||
|
|
||||||
test("bootstrap node is listed under its self type", async () => {
|
test("bootstrap node is listed under its self type", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
|
|
||||||
// All built-in schemas should be typed by the meta-schema
|
// All built-in schemas should be typed by the meta-schema
|
||||||
@@ -253,25 +253,25 @@ describe("verify", () => {
|
|||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
describe("bootstrap", () => {
|
describe("bootstrap", () => {
|
||||||
test("throws when store lacks internal bootstrap path", async () => {
|
test("throws when store lacks internal bootstrap path", async () => {
|
||||||
const cas: Store = {
|
const cas = {
|
||||||
put: async () => "0000000000000",
|
put: () => "0000000000000",
|
||||||
get: () => null,
|
get: () => null,
|
||||||
has: () => false,
|
has: () => false,
|
||||||
listByType: () => [],
|
listByType: () => [],
|
||||||
};
|
} as unknown as CasStore;
|
||||||
const fakeStore = {
|
const fakeStore = {
|
||||||
cas,
|
cas,
|
||||||
var: { set: () => null } as never,
|
var: { set: () => null } as never,
|
||||||
tag: {} as never,
|
tag: {} as never,
|
||||||
} as never;
|
} as never;
|
||||||
await expect(bootstrap(fakeStore)).rejects.toThrow(
|
expect(() => bootstrap(fakeStore)).toThrow(
|
||||||
"Store does not support bootstrap",
|
"Store does not support bootstrap",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns a map with 30 built-in schema aliases", async () => {
|
test("returns a map with 30 built-in schema aliases", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
|
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/schema");
|
expect(builtinSchemas).toHaveProperty("@ocas/schema");
|
||||||
expect(builtinSchemas).toHaveProperty("@ocas/string");
|
expect(builtinSchemas).toHaveProperty("@ocas/string");
|
||||||
@@ -294,7 +294,7 @@ describe("bootstrap", () => {
|
|||||||
|
|
||||||
test("meta-schema node is stored and retrievable", async () => {
|
test("meta-schema node is stored and retrievable", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
|
|
||||||
expect(store.cas.has(metaHash)).toBe(true);
|
expect(store.cas.has(metaHash)).toBe(true);
|
||||||
@@ -304,7 +304,7 @@ describe("bootstrap", () => {
|
|||||||
|
|
||||||
test("meta-schema node is self-referencing: type === hash", async () => {
|
test("meta-schema node is self-referencing: type === hash", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const node = store.cas.get(metaHash) as CasNode;
|
const node = store.cas.get(metaHash) as CasNode;
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ describe("bootstrap", () => {
|
|||||||
|
|
||||||
test("bootstrap node passes verify()", async () => {
|
test("bootstrap node passes verify()", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const node = store.cas.get(metaHash) as CasNode;
|
const node = store.cas.get(metaHash) as CasNode;
|
||||||
|
|
||||||
@@ -322,8 +322,8 @@ describe("bootstrap", () => {
|
|||||||
|
|
||||||
test("bootstrap is idempotent: same hashes on repeated calls", async () => {
|
test("bootstrap is idempotent: same hashes on repeated calls", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const h1 = await bootstrap(store);
|
const h1 = bootstrap(store);
|
||||||
const h2 = await bootstrap(store);
|
const h2 = bootstrap(store);
|
||||||
|
|
||||||
expect(h1).toEqual(h2);
|
expect(h1).toEqual(h2);
|
||||||
// All built-in schemas typed by the meta-schema (1 self + 7 unique primitives + 21 outputs)
|
// All built-in schemas typed by the meta-schema (1 self + 7 unique primitives + 21 outputs)
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export type {
|
|||||||
ListEntry,
|
ListEntry,
|
||||||
ListOptions,
|
ListOptions,
|
||||||
ListSort,
|
ListSort,
|
||||||
OcasStore,
|
|
||||||
Store,
|
Store,
|
||||||
Tag,
|
Tag,
|
||||||
TagOp,
|
TagOp,
|
||||||
@@ -60,6 +59,16 @@ export type {
|
|||||||
VarStore,
|
VarStore,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
export { validateName } from "./validation.js";
|
export { validateName } from "./validation.js";
|
||||||
|
export {
|
||||||
|
addNameIndex,
|
||||||
|
checkTagLabelConflict,
|
||||||
|
cloneVarRecord,
|
||||||
|
extractSchema,
|
||||||
|
pushHistory,
|
||||||
|
removeNameIndex,
|
||||||
|
type VarRecord,
|
||||||
|
varKey,
|
||||||
|
} from "./var-store-helpers.js";
|
||||||
export type { Variable } from "./variable.js";
|
export type { Variable } from "./variable.js";
|
||||||
export { verify } from "./verify.js";
|
export { verify } from "./verify.js";
|
||||||
export { wrapEnvelope } from "./wrap-envelope.js";
|
export { wrapEnvelope } from "./wrap-envelope.js";
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { putSchema } from "./schema.js";
|
|||||||
import { createMemoryStore } from "./store.js";
|
import { createMemoryStore } from "./store.js";
|
||||||
import type { Hash } from "./types.js";
|
import type { Hash } from "./types.js";
|
||||||
|
|
||||||
// Helper to create an in-memory OcasStore with bootstrap
|
// Helper to create an in-memory Store with bootstrap
|
||||||
async function createTempVarStore() {
|
async function createTempVarStore() {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
return {
|
return {
|
||||||
store,
|
store,
|
||||||
cleanup: async () => {},
|
cleanup: async () => {},
|
||||||
@@ -55,7 +55,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -65,7 +65,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
value: "child content",
|
value: "child content",
|
||||||
});
|
});
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -78,7 +78,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template for parent
|
// Register template for parent
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Parent: {{ payload.name }}\n{% render payload.child %}",
|
"Parent: {{ payload.name }}\n{% render payload.child %}",
|
||||||
@@ -109,7 +109,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
level: { type: "number" },
|
level: { type: "number" },
|
||||||
@@ -130,7 +130,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
child: level1Hash,
|
child: level1Hash,
|
||||||
});
|
});
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
|
|
||||||
// Template that shows the level and renders child with explicit decay
|
// Template that shows the level and renders child with explicit decay
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
@@ -158,7 +158,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -167,7 +167,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
const leftHash = store.cas.put(childSchema, { value: "left" });
|
const leftHash = store.cas.put(childSchema, { value: "left" });
|
||||||
const rightHash = store.cas.put(childSchema, { value: "right" });
|
const rightHash = store.cas.put(childSchema, { value: "right" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
left: { type: "string", format: "ocas_ref" },
|
left: { type: "string", format: "ocas_ref" },
|
||||||
@@ -179,7 +179,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
right: rightHash,
|
right: rightHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplate = store.cas.put(
|
const parentTemplate = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Left:\n{% render payload.left %}\nRight:\n{% render payload.right %}",
|
"Left:\n{% render payload.left %}\nRight:\n{% render payload.right %}",
|
||||||
@@ -211,7 +211,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -225,7 +225,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
child: null,
|
child: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Before\n{% render payload.child %}\nAfter",
|
"Before\n{% render payload.child %}\nAfter",
|
||||||
@@ -251,7 +251,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const fakeHash = "ZZZZZZZZZZZZZ" as Hash;
|
const fakeHash = "ZZZZZZZZZZZZZ" as Hash;
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -263,7 +263,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
child: fakeHash,
|
child: fakeHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render payload.child %}",
|
"{% render payload.child %}",
|
||||||
@@ -286,7 +286,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -294,7 +294,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -302,7 +302,7 @@ describe("Suite 2: Custom {% render %} Tag Implementation", () => {
|
|||||||
});
|
});
|
||||||
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplate = store.cas.put(
|
const parentTemplate = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render payload.child %}",
|
"{% render payload.child %}",
|
||||||
@@ -328,7 +328,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -336,7 +336,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Resolution: {{ resolution }}",
|
"Resolution: {{ resolution }}",
|
||||||
@@ -359,7 +359,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -367,7 +367,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(templateSchema, "Epsilon: {{ epsilon }}");
|
const template = store.cas.put(templateSchema, "Epsilon: {{ epsilon }}");
|
||||||
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
||||||
|
|
||||||
@@ -387,7 +387,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -395,7 +395,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(templateSchema, "Hash: {{ hash }}");
|
const template = store.cas.put(templateSchema, "Hash: {{ hash }}");
|
||||||
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
||||||
|
|
||||||
@@ -415,7 +415,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -424,7 +424,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test", count: 42 });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test", count: 42 });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Name: {{ payload.name }}, Count: {{ payload.count }}",
|
"Name: {{ payload.name }}, Count: {{ payload.count }}",
|
||||||
@@ -447,7 +447,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -455,7 +455,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(templateSchema, "Type: {{ type }}");
|
const template = store.cas.put(templateSchema, "Type: {{ type }}");
|
||||||
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
||||||
|
|
||||||
@@ -475,7 +475,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -483,7 +483,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Timestamp: {{ timestamp }}",
|
"Timestamp: {{ timestamp }}",
|
||||||
@@ -506,7 +506,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -514,7 +514,7 @@ describe("Suite 3: Template Context Variables", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
`Hash: {{ hash }}
|
`Hash: {{ hash }}
|
||||||
@@ -549,7 +549,7 @@ describe("Suite 4: Render Flow Integration", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -557,7 +557,7 @@ describe("Suite 4: Render Flow Integration", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Custom template: {{ payload.name }}",
|
"Custom template: {{ payload.name }}",
|
||||||
@@ -580,7 +580,7 @@ describe("Suite 4: Render Flow Integration", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -588,7 +588,7 @@ describe("Suite 4: Render Flow Integration", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(templateSchema, "");
|
const template = store.cas.put(templateSchema, "");
|
||||||
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
||||||
|
|
||||||
@@ -608,7 +608,7 @@ describe("Suite 4: Render Flow Integration", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -616,7 +616,7 @@ describe("Suite 4: Render Flow Integration", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render %}", // Invalid: no variable
|
"{% render %}", // Invalid: no variable
|
||||||
@@ -641,7 +641,7 @@ describe("Suite 5: Decay Priority Chain", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -649,7 +649,7 @@ describe("Suite 5: Decay Priority Chain", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -657,7 +657,7 @@ describe("Suite 5: Decay Priority Chain", () => {
|
|||||||
});
|
});
|
||||||
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplate = store.cas.put(
|
const parentTemplate = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render payload.child, decay: 0.7 %}",
|
"{% render payload.child, decay: 0.7 %}",
|
||||||
@@ -687,7 +687,7 @@ describe("Suite 5: Decay Priority Chain", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -695,7 +695,7 @@ describe("Suite 5: Decay Priority Chain", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -703,7 +703,7 @@ describe("Suite 5: Decay Priority Chain", () => {
|
|||||||
});
|
});
|
||||||
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplate = store.cas.put(
|
const parentTemplate = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render payload.child %}", // No explicit decay
|
"{% render payload.child %}", // No explicit decay
|
||||||
@@ -733,7 +733,7 @@ describe("Suite 5: Decay Priority Chain", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -741,7 +741,7 @@ describe("Suite 5: Decay Priority Chain", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -749,7 +749,7 @@ describe("Suite 5: Decay Priority Chain", () => {
|
|||||||
});
|
});
|
||||||
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplate = store.cas.put(
|
const parentTemplate = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render payload.child %}",
|
"{% render payload.child %}",
|
||||||
@@ -781,7 +781,7 @@ describe("Suite 6: Recursive Rendering Edge Cases", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
level: { type: "number" },
|
level: { type: "number" },
|
||||||
@@ -800,7 +800,7 @@ describe("Suite 6: Recursive Rendering Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Level {{ payload.level }}\n{% render payload.next %}",
|
"Level {{ payload.level }}\n{% render payload.next %}",
|
||||||
@@ -826,7 +826,7 @@ describe("Suite 6: Recursive Rendering Edge Cases", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -839,7 +839,7 @@ describe("Suite 6: Recursive Rendering Edge Cases", () => {
|
|||||||
// Create simple node first
|
// Create simple node first
|
||||||
const nodeAHash = store.cas.put(nodeSchema, { name: "A", ref: null });
|
const nodeAHash = store.cas.put(nodeSchema, { name: "A", ref: null });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Node {{ payload.name }}\n{% render payload.ref %}",
|
"Node {{ payload.name }}\n{% render payload.ref %}",
|
||||||
@@ -862,7 +862,7 @@ describe("Suite 6: Recursive Rendering Edge Cases", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const itemSchema = await putSchema(store, {
|
const itemSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -872,7 +872,7 @@ describe("Suite 6: Recursive Rendering Edge Cases", () => {
|
|||||||
const item2 = store.cas.put(itemSchema, { name: "item2" });
|
const item2 = store.cas.put(itemSchema, { name: "item2" });
|
||||||
const item3 = store.cas.put(itemSchema, { name: "item3" });
|
const item3 = store.cas.put(itemSchema, { name: "item3" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
items: {
|
items: {
|
||||||
@@ -885,7 +885,7 @@ describe("Suite 6: Recursive Rendering Edge Cases", () => {
|
|||||||
items: [item1, item2, item3],
|
items: [item1, item2, item3],
|
||||||
});
|
});
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplate = store.cas.put(
|
const parentTemplate = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% for item in payload.items %}{% render item %}\n{% endfor %}",
|
"{% for item in payload.items %}{% render item %}\n{% endfor %}",
|
||||||
@@ -918,7 +918,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -926,7 +926,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "test" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(templateSchema, "{% render missingVar %}");
|
const template = store.cas.put(templateSchema, "{% render missingVar %}");
|
||||||
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
store.var.set(`@ocas/template/text/${nodeSchema}`, template);
|
||||||
|
|
||||||
@@ -947,7 +947,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -955,7 +955,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -963,7 +963,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render payload.child, decay: 1.5 %}",
|
"{% render payload.child, decay: 1.5 %}",
|
||||||
@@ -986,7 +986,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -994,7 +994,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -1002,7 +1002,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render payload.child, decay: -0.5 %}",
|
"{% render payload.child, decay: -0.5 %}",
|
||||||
@@ -1025,7 +1025,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -1033,7 +1033,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -1041,7 +1041,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render payload.child, decay: 0 %}",
|
"{% render payload.child, decay: 0 %}",
|
||||||
@@ -1064,7 +1064,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -1072,7 +1072,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -1080,7 +1080,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
const parentHash = store.cas.put(parentSchema, { child: childHash });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplate = store.cas.put(
|
const parentTemplate = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% render payload.child, decay: 1 %}",
|
"{% render payload.child, decay: 1 %}",
|
||||||
@@ -1110,7 +1110,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1118,7 +1118,7 @@ describe("Suite 7: Error Handling & Edge Cases", () => {
|
|||||||
});
|
});
|
||||||
const nodeHash = store.cas.put(nodeSchema, { name: "世界" });
|
const nodeHash = store.cas.put(nodeSchema, { name: "世界" });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const template = store.cas.put(
|
const template = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"你好: {{ payload.name }} 🌍",
|
"你好: {{ payload.name }} 🌍",
|
||||||
@@ -1143,7 +1143,7 @@ describe("Suite 8: Performance & Scalability", () => {
|
|||||||
const { store, cleanup } = await createTempVarStore();
|
const { store, cleanup } = await createTempVarStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const itemSchema = await putSchema(store, {
|
const itemSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "number" },
|
value: { type: "number" },
|
||||||
@@ -1156,7 +1156,7 @@ describe("Suite 8: Performance & Scalability", () => {
|
|||||||
children.push(hash);
|
children.push(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
items: {
|
items: {
|
||||||
@@ -1167,7 +1167,7 @@ describe("Suite 8: Performance & Scalability", () => {
|
|||||||
});
|
});
|
||||||
const parentHash = store.cas.put(parentSchema, { items: children });
|
const parentHash = store.cas.put(parentSchema, { items: children });
|
||||||
|
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplate = store.cas.put(
|
const parentTemplate = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"{% for child in payload.items %}{% render child %}{% endfor %}",
|
"{% for child in payload.items %}{% render child %}{% endfor %}",
|
||||||
@@ -1199,7 +1199,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema for person object
|
// Create schema for person object
|
||||||
const personSchema = await putSchema(store, {
|
const personSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1214,7 +1214,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template using direct property access (incorrect syntax)
|
// Register template using direct property access (incorrect syntax)
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Name: {{ name }}, Age: {{ age }}",
|
"Name: {{ name }}, Age: {{ age }}",
|
||||||
@@ -1239,7 +1239,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema for person object
|
// Create schema for person object
|
||||||
const personSchema = await putSchema(store, {
|
const personSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1254,7 +1254,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template using correct payload. prefix
|
// Register template using correct payload. prefix
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Name: {{ payload.name }}, Age: {{ payload.age }}",
|
"Name: {{ payload.name }}, Age: {{ payload.age }}",
|
||||||
@@ -1279,7 +1279,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema and node
|
// Create schema and node
|
||||||
const personSchema = await putSchema(store, {
|
const personSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1293,7 +1293,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"User: {{ payload.name }}, Age: {{ payload.age }}",
|
"User: {{ payload.name }}, Age: {{ payload.age }}",
|
||||||
@@ -1319,13 +1319,13 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema for simple string
|
// Create schema for simple string
|
||||||
const stringSchema = await putSchema(store, { type: "string" });
|
const stringSchema = putSchema(store, { type: "string" });
|
||||||
|
|
||||||
// Create node with string payload
|
// Create node with string payload
|
||||||
const stringHash = store.cas.put(stringSchema, "Hello World");
|
const stringHash = store.cas.put(stringSchema, "Hello World");
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Value is: {{ payload }}",
|
"Value is: {{ payload }}",
|
||||||
@@ -1350,13 +1350,13 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema for number
|
// Create schema for number
|
||||||
const numberSchema = await putSchema(store, { type: "number" });
|
const numberSchema = putSchema(store, { type: "number" });
|
||||||
|
|
||||||
// Create node with number payload
|
// Create node with number payload
|
||||||
const numberHash = store.cas.put(numberSchema, 42);
|
const numberHash = store.cas.put(numberSchema, 42);
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"The answer is {{ payload }}",
|
"The answer is {{ payload }}",
|
||||||
@@ -1381,7 +1381,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema for nested object
|
// Create schema for nested object
|
||||||
const userSchema = await putSchema(store, {
|
const userSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
user: {
|
user: {
|
||||||
@@ -1410,7 +1410,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template with deep property access
|
// Register template with deep property access
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"User {{ payload.user.name }} lives in {{ payload.user.address.city }}",
|
"User {{ payload.user.name }} lives in {{ payload.user.address.city }}",
|
||||||
@@ -1435,7 +1435,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema with array
|
// Create schema with array
|
||||||
const tagsSchema = await putSchema(store, {
|
const tagsSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
tags: {
|
tags: {
|
||||||
@@ -1451,7 +1451,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template with array iteration
|
// Register template with array iteration
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Tags: {% for tag in payload.tags %}{{ tag }}{% unless forloop.last %}, {% endunless %}{% endfor %}",
|
"Tags: {% for tag in payload.tags %}{{ tag }}{% unless forloop.last %}, {% endunless %}{% endfor %}",
|
||||||
@@ -1476,7 +1476,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema
|
// Create schema
|
||||||
const personSchema = await putSchema(store, {
|
const personSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1489,7 +1489,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template that references missing property
|
// Register template that references missing property
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Name: {{ payload.name }}, Age: {{ payload.age }}",
|
"Name: {{ payload.name }}, Age: {{ payload.age }}",
|
||||||
@@ -1514,7 +1514,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema allowing null
|
// Create schema allowing null
|
||||||
const personSchema = await putSchema(store, {
|
const personSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1529,7 +1529,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Name: {{ payload.name }}, Email: {{ payload.email }}",
|
"Name: {{ payload.name }}, Email: {{ payload.email }}",
|
||||||
@@ -1554,7 +1554,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema with boolean
|
// Create schema with boolean
|
||||||
const userSchema = await putSchema(store, {
|
const userSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1569,7 +1569,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template with conditional
|
// Register template with conditional
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"User {{ payload.name }} is {% if payload.active %}active{% else %}inactive{% endif %}",
|
"User {{ payload.name }} is {% if payload.active %}active{% else %}inactive{% endif %}",
|
||||||
@@ -1594,7 +1594,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema
|
// Create schema
|
||||||
const dataSchema = await putSchema(store, {
|
const dataSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1609,7 +1609,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Name: '{{ payload.name }}', Count: {{ payload.count }}",
|
"Name: '{{ payload.name }}', Count: {{ payload.count }}",
|
||||||
@@ -1634,7 +1634,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create schema
|
// Create schema
|
||||||
const textSchema = await putSchema(store, {
|
const textSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
text: { type: "string" },
|
text: { type: "string" },
|
||||||
@@ -1647,7 +1647,7 @@ describe("Suite 9: E2E Template Variable Rendering (Issue #52)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const templateHash = store.cas.put(
|
const templateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Text: {{ payload.text }}",
|
"Text: {{ payload.text }}",
|
||||||
@@ -1674,7 +1674,7 @@ describe("Suite 10: Context Variable Completeness", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create child schema and node
|
// Create child schema and node
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1683,7 +1683,7 @@ describe("Suite 10: Context Variable Completeness", () => {
|
|||||||
const childHash = store.cas.put(childSchema, { name: "child" });
|
const childHash = store.cas.put(childSchema, { name: "child" });
|
||||||
|
|
||||||
// Create parent schema and node
|
// Create parent schema and node
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -1696,7 +1696,7 @@ describe("Suite 10: Context Variable Completeness", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register parent template
|
// Register parent template
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplateHash = store.cas.put(
|
const parentTemplateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Parent: {{ payload.name }}\n{% render payload.child %}",
|
"Parent: {{ payload.name }}\n{% render payload.child %}",
|
||||||
@@ -1731,7 +1731,7 @@ describe("Suite 10: Context Variable Completeness", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create child schema and node
|
// Create child schema and node
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
custom: { type: "string" },
|
custom: { type: "string" },
|
||||||
@@ -1742,7 +1742,7 @@ describe("Suite 10: Context Variable Completeness", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create parent schema and node
|
// Create parent schema and node
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
custom: { type: "string" },
|
custom: { type: "string" },
|
||||||
@@ -1755,7 +1755,7 @@ describe("Suite 10: Context Variable Completeness", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register parent template
|
// Register parent template
|
||||||
const templateSchema = await putSchema(store, { type: "string" });
|
const templateSchema = putSchema(store, { type: "string" });
|
||||||
const parentTemplateHash = store.cas.put(
|
const parentTemplateHash = store.cas.put(
|
||||||
templateSchema,
|
templateSchema,
|
||||||
"Parent custom: {{ payload.custom }}\n{% render payload.child %}",
|
"Parent custom: {{ payload.custom }}\n{% render payload.child %}",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type Context, Liquid, type TagToken } from "liquidjs";
|
import { type Context, Liquid, type TagToken } from "liquidjs";
|
||||||
import type { RenderOptions } from "./render.js";
|
import type { RenderOptions } from "./render.js";
|
||||||
import { putSchema } from "./schema.js";
|
import { putSchema } from "./schema.js";
|
||||||
import type { Hash, OcasStore } from "./types.js";
|
import type { Hash, Store } from "./types.js";
|
||||||
|
|
||||||
const DEFAULT_RESOLUTION = 1.0;
|
const DEFAULT_RESOLUTION = 1.0;
|
||||||
const DEFAULT_DECAY = 0.5;
|
const DEFAULT_DECAY = 0.5;
|
||||||
@@ -13,7 +13,7 @@ const FLOAT_TOLERANCE = 1e-10;
|
|||||||
* Templates are discovered via variables: @ocas/template/text/<type-hash>
|
* Templates are discovered via variables: @ocas/template/text/<type-hash>
|
||||||
*/
|
*/
|
||||||
export async function renderWithTemplate(
|
export async function renderWithTemplate(
|
||||||
store: OcasStore,
|
store: Store,
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
options?: RenderOptions,
|
options?: RenderOptions,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
@@ -43,7 +43,7 @@ export async function renderWithTemplate(
|
|||||||
/**
|
/**
|
||||||
* Create a Liquid engine instance with custom render tag
|
* Create a Liquid engine instance with custom render tag
|
||||||
*/
|
*/
|
||||||
function createLiquidEngine(store: OcasStore, globalDecay: number): Liquid {
|
function createLiquidEngine(store: Store, globalDecay: number): Liquid {
|
||||||
const engine = new Liquid({
|
const engine = new Liquid({
|
||||||
strictFilters: false,
|
strictFilters: false,
|
||||||
strictVariables: false,
|
strictVariables: false,
|
||||||
@@ -141,7 +141,7 @@ function createLiquidEngine(store: OcasStore, globalDecay: number): Liquid {
|
|||||||
*/
|
*/
|
||||||
async function renderNode(
|
async function renderNode(
|
||||||
engine: Liquid,
|
engine: Liquid,
|
||||||
store: OcasStore,
|
store: Store,
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
currentResolution: number,
|
currentResolution: number,
|
||||||
epsilon: number,
|
epsilon: number,
|
||||||
@@ -200,14 +200,14 @@ async function renderNode(
|
|||||||
* Find a template for a given type hash
|
* Find a template for a given type hash
|
||||||
*/
|
*/
|
||||||
async function findTemplate(
|
async function findTemplate(
|
||||||
store: OcasStore,
|
store: Store,
|
||||||
typeHash: Hash,
|
typeHash: Hash,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const varName = `@ocas/template/text/${typeHash}`;
|
const varName = `@ocas/template/text/${typeHash}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find the string schema hash (we need this to query variables)
|
// Find the string schema hash (we need this to query variables)
|
||||||
const stringSchema = await putSchema(store, { type: "string" });
|
const stringSchema = putSchema(store, { type: "string" });
|
||||||
|
|
||||||
const variable = store.var.get(varName, stringSchema);
|
const variable = store.var.get(varName, stringSchema);
|
||||||
if (variable === null) {
|
if (variable === null) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { createMemoryStore } from "./store.js";
|
import { createMemoryStore } from "./store.js";
|
||||||
import type { CasStore, OcasStore, TagStore, VarStore } from "./types.js";
|
import type { CasStore, Store, TagStore, VarStore } from "./types.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In-memory `OcasStore` used by schema validation tests. It exposes the
|
* In-memory `Store` used by schema validation tests. It exposes the
|
||||||
* `cas`, `var`, and `tag` sub-stores of an `OcasStore` plus a few legacy
|
* `cas`, `var`, and `tag` sub-stores of an `Store` plus a few legacy
|
||||||
* pass-through helpers (`get`, `put`, `has`, …) that some older tests still
|
* pass-through helpers (`get`, `put`, `has`, …) that some older tests still
|
||||||
* use directly.
|
* use directly.
|
||||||
*/
|
*/
|
||||||
export class MemStore implements OcasStore {
|
export class MemStore implements Store {
|
||||||
readonly cas: CasStore;
|
readonly cas: CasStore;
|
||||||
readonly var: VarStore;
|
readonly var: VarStore;
|
||||||
readonly tag: TagStore;
|
readonly tag: TagStore;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const OUTPUT_ALIASES = [
|
|||||||
describe("registerOutputTemplates", () => {
|
describe("registerOutputTemplates", () => {
|
||||||
test("registers a template for every @ocas/output/* schema", async () => {
|
test("registers a template for every @ocas/output/* schema", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const registered = await registerOutputTemplates(store);
|
const registered = await registerOutputTemplates(store);
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ describe("registerOutputTemplates", () => {
|
|||||||
|
|
||||||
test("each template is retrievable via @ocas/template/text/<hash>", async () => {
|
test("each template is retrievable via @ocas/template/text/<hash>", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
|
|
||||||
await registerOutputTemplates(store);
|
await registerOutputTemplates(store);
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ describe("registerOutputTemplates", () => {
|
|||||||
|
|
||||||
test("is idempotent — safe to call multiple times", async () => {
|
test("is idempotent — safe to call multiple times", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const first = await registerOutputTemplates(store);
|
const first = await registerOutputTemplates(store);
|
||||||
const second = await registerOutputTemplates(store);
|
const second = await registerOutputTemplates(store);
|
||||||
@@ -75,7 +75,7 @@ describe("registerOutputTemplates", () => {
|
|||||||
|
|
||||||
test("@ocas/output/put template contains payload reference", async () => {
|
test("@ocas/output/put template contains payload reference", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
|
|
||||||
await registerOutputTemplates(store);
|
await registerOutputTemplates(store);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { bootstrap } from "./bootstrap.js";
|
import { bootstrap } from "./bootstrap.js";
|
||||||
import type { Hash, OcasStore } from "./types.js";
|
import type { Hash, Store } from "./types.js";
|
||||||
|
|
||||||
const DEFAULT_TEMPLATES: ReadonlyArray<
|
const DEFAULT_TEMPLATES: ReadonlyArray<
|
||||||
readonly [alias: string, template: string]
|
readonly [alias: string, template: string]
|
||||||
@@ -63,9 +63,9 @@ const DEFAULT_TEMPLATES: ReadonlyArray<
|
|||||||
* Idempotent: safe to call multiple times.
|
* Idempotent: safe to call multiple times.
|
||||||
*/
|
*/
|
||||||
export async function registerOutputTemplates(
|
export async function registerOutputTemplates(
|
||||||
store: OcasStore,
|
store: Store,
|
||||||
): Promise<Record<string, Hash>> {
|
): Promise<Record<string, Hash>> {
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const stringHash = aliases["@ocas/string"];
|
const stringHash = aliases["@ocas/string"];
|
||||||
if (stringHash === undefined) {
|
if (stringHash === undefined) {
|
||||||
throw new Error("@ocas/string schema not found in bootstrap result");
|
throw new Error("@ocas/string schema not found in bootstrap result");
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import type { Hash } from "./types.js";
|
|||||||
describe("Suite 1: Basic Rendering (No Nesting)", () => {
|
describe("Suite 1: Basic Rendering (No Nesting)", () => {
|
||||||
test("1.1 Render Simple Primitives", async () => {
|
test("1.1 Render Simple Primitives", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const textSchema = await putSchema(store, { type: "string" });
|
const textSchema = putSchema(store, { type: "string" });
|
||||||
const hash = store.cas.put(textSchema, "hello");
|
const hash = store.cas.put(textSchema, "hello");
|
||||||
|
|
||||||
const output = render(store, hash, { resolution: 1.0 });
|
const output = render(store, hash, { resolution: 1.0 });
|
||||||
@@ -21,8 +21,8 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => {
|
|||||||
|
|
||||||
test("1.2 Render Object Node (Flat)", async () => {
|
test("1.2 Render Object Node (Flat)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const objSchema = await putSchema(store, {
|
const objSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -41,8 +41,8 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => {
|
|||||||
|
|
||||||
test("1.3 Render Array Node (Flat)", async () => {
|
test("1.3 Render Array Node (Flat)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const arraySchema = await putSchema(store, {
|
const arraySchema = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: { type: "number" },
|
items: { type: "number" },
|
||||||
});
|
});
|
||||||
@@ -57,8 +57,8 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => {
|
|||||||
|
|
||||||
test("1.4 Render with resolution=0 (Force Reference)", async () => {
|
test("1.4 Render with resolution=0 (Force Reference)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const textSchema = await putSchema(store, { type: "string" });
|
const textSchema = putSchema(store, { type: "string" });
|
||||||
const hash = store.cas.put(textSchema, "hello");
|
const hash = store.cas.put(textSchema, "hello");
|
||||||
|
|
||||||
const output = render(store, hash, { resolution: 0 });
|
const output = render(store, hash, { resolution: 0 });
|
||||||
@@ -80,9 +80,9 @@ describe("Suite 1: Basic Rendering (No Nesting)", () => {
|
|||||||
describe("Suite 2: Resolution Decay Model", () => {
|
describe("Suite 2: Resolution Decay Model", () => {
|
||||||
test("2.1 Single-level Nesting with Default Decay", async () => {
|
test("2.1 Single-level Nesting with Default Decay", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
content: { type: "string" },
|
content: { type: "string" },
|
||||||
@@ -90,7 +90,7 @@ describe("Suite 2: Resolution Decay Model", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { content: "leaf" });
|
const childHash = store.cas.put(childSchema, { content: "leaf" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
title: { type: "string" },
|
title: { type: "string" },
|
||||||
@@ -116,9 +116,9 @@ describe("Suite 2: Resolution Decay Model", () => {
|
|||||||
|
|
||||||
test("2.2 Multi-level Nesting Reaches Epsilon", async () => {
|
test("2.2 Multi-level Nesting Reaches Epsilon", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const leafSchema = await putSchema(store, {
|
const leafSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "number" },
|
value: { type: "number" },
|
||||||
@@ -152,9 +152,9 @@ describe("Suite 2: Resolution Decay Model", () => {
|
|||||||
|
|
||||||
test("2.3 High Decay (Quick Cutoff)", async () => {
|
test("2.3 High Decay (Quick Cutoff)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
level: { type: "number" },
|
level: { type: "number" },
|
||||||
@@ -190,9 +190,9 @@ describe("Suite 2: Resolution Decay Model", () => {
|
|||||||
|
|
||||||
test("2.4 Low Decay (Deep Expansion)", async () => {
|
test("2.4 Low Decay (Deep Expansion)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
level: { type: "number" },
|
level: { type: "number" },
|
||||||
@@ -225,9 +225,9 @@ describe("Suite 2: Resolution Decay Model", () => {
|
|||||||
|
|
||||||
test("2.5 Starting Resolution Below 1.0", async () => {
|
test("2.5 Starting Resolution Below 1.0", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
level: { type: "number" },
|
level: { type: "number" },
|
||||||
@@ -263,9 +263,9 @@ describe("Suite 2: Resolution Decay Model", () => {
|
|||||||
describe("Suite 3: Complex Graph Structures", () => {
|
describe("Suite 3: Complex Graph Structures", () => {
|
||||||
test("3.1 Multiple Child References", async () => {
|
test("3.1 Multiple Child References", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const itemSchema = await putSchema(store, {
|
const itemSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -276,7 +276,7 @@ describe("Suite 3: Complex Graph Structures", () => {
|
|||||||
const item2 = store.cas.put(itemSchema, { name: "item2" });
|
const item2 = store.cas.put(itemSchema, { name: "item2" });
|
||||||
const item3 = store.cas.put(itemSchema, { name: "item3" });
|
const item3 = store.cas.put(itemSchema, { name: "item3" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
items: {
|
items: {
|
||||||
@@ -302,9 +302,9 @@ describe("Suite 3: Complex Graph Structures", () => {
|
|||||||
|
|
||||||
test("3.2 Object with Multiple ocas_ref Fields", async () => {
|
test("3.2 Object with Multiple ocas_ref Fields", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -314,7 +314,7 @@ describe("Suite 3: Complex Graph Structures", () => {
|
|||||||
const leftHash = store.cas.put(childSchema, { value: "left" });
|
const leftHash = store.cas.put(childSchema, { value: "left" });
|
||||||
const rightHash = store.cas.put(childSchema, { value: "right" });
|
const rightHash = store.cas.put(childSchema, { value: "right" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
left: { type: "string", format: "ocas_ref" },
|
left: { type: "string", format: "ocas_ref" },
|
||||||
@@ -341,9 +341,9 @@ describe("Suite 3: Complex Graph Structures", () => {
|
|||||||
|
|
||||||
test("3.3 Cycle Detection", async () => {
|
test("3.3 Cycle Detection", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -373,9 +373,9 @@ describe("Suite 3: Complex Graph Structures", () => {
|
|||||||
|
|
||||||
test("3.4 DAG (Shared Descendant)", async () => {
|
test("3.4 DAG (Shared Descendant)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const leafSchema = await putSchema(store, {
|
const leafSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -383,7 +383,7 @@ describe("Suite 3: Complex Graph Structures", () => {
|
|||||||
});
|
});
|
||||||
const sharedLeaf = store.cas.put(leafSchema, { value: "shared" });
|
const sharedLeaf = store.cas.put(leafSchema, { value: "shared" });
|
||||||
|
|
||||||
const branchSchema = await putSchema(store, {
|
const branchSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -399,7 +399,7 @@ describe("Suite 3: Complex Graph Structures", () => {
|
|||||||
child: sharedLeaf,
|
child: sharedLeaf,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootSchema = await putSchema(store, {
|
const rootSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
left: { type: "string", format: "ocas_ref" },
|
left: { type: "string", format: "ocas_ref" },
|
||||||
@@ -424,9 +424,9 @@ describe("Suite 3: Complex Graph Structures", () => {
|
|||||||
|
|
||||||
test("3.5 Deep Tree", async () => {
|
test("3.5 Deep Tree", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "number" },
|
value: { type: "number" },
|
||||||
@@ -465,8 +465,8 @@ describe("Suite 3: Complex Graph Structures", () => {
|
|||||||
describe("Suite 4: Epsilon Boundary Cases", () => {
|
describe("Suite 4: Epsilon Boundary Cases", () => {
|
||||||
test("4.1 Resolution Exactly at Epsilon", async () => {
|
test("4.1 Resolution Exactly at Epsilon", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const textSchema = await putSchema(store, { type: "string" });
|
const textSchema = putSchema(store, { type: "string" });
|
||||||
const hash = store.cas.put(textSchema, "test");
|
const hash = store.cas.put(textSchema, "test");
|
||||||
|
|
||||||
const output = render(store, hash, {
|
const output = render(store, hash, {
|
||||||
@@ -480,8 +480,8 @@ describe("Suite 4: Epsilon Boundary Cases", () => {
|
|||||||
|
|
||||||
test("4.2 Resolution Just Above Epsilon", async () => {
|
test("4.2 Resolution Just Above Epsilon", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const textSchema = await putSchema(store, { type: "string" });
|
const textSchema = putSchema(store, { type: "string" });
|
||||||
const hash = store.cas.put(textSchema, "test");
|
const hash = store.cas.put(textSchema, "test");
|
||||||
|
|
||||||
const output = render(store, hash, {
|
const output = render(store, hash, {
|
||||||
@@ -495,9 +495,9 @@ describe("Suite 4: Epsilon Boundary Cases", () => {
|
|||||||
|
|
||||||
test("4.3 Very Small Epsilon (Deep Expansion)", async () => {
|
test("4.3 Very Small Epsilon (Deep Expansion)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
level: { type: "number" },
|
level: { type: "number" },
|
||||||
@@ -530,9 +530,9 @@ describe("Suite 4: Epsilon Boundary Cases", () => {
|
|||||||
|
|
||||||
test("4.4 Zero Epsilon (Never Prune)", async () => {
|
test("4.4 Zero Epsilon (Never Prune)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
level: { type: "number" },
|
level: { type: "number" },
|
||||||
@@ -567,8 +567,8 @@ describe("Suite 4: Epsilon Boundary Cases", () => {
|
|||||||
describe("Suite 5: YAML Output Format", () => {
|
describe("Suite 5: YAML Output Format", () => {
|
||||||
test("5.1 Valid YAML Syntax", async () => {
|
test("5.1 Valid YAML Syntax", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const objSchema = await putSchema(store, {
|
const objSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -585,8 +585,8 @@ describe("Suite 5: YAML Output Format", () => {
|
|||||||
|
|
||||||
test("5.2 Nested Object Indentation", async () => {
|
test("5.2 Nested Object Indentation", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const nestedSchema = await putSchema(store, {
|
const nestedSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
outer: {
|
outer: {
|
||||||
@@ -611,8 +611,8 @@ describe("Suite 5: YAML Output Format", () => {
|
|||||||
|
|
||||||
test("5.3 Array Rendering", async () => {
|
test("5.3 Array Rendering", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const arraySchema = await putSchema(store, {
|
const arraySchema = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: { type: "number" },
|
items: { type: "number" },
|
||||||
});
|
});
|
||||||
@@ -626,9 +626,9 @@ describe("Suite 5: YAML Output Format", () => {
|
|||||||
|
|
||||||
test("5.4 CAS Reference in YAML", async () => {
|
test("5.4 CAS Reference in YAML", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -636,7 +636,7 @@ describe("Suite 5: YAML Output Format", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -656,8 +656,8 @@ describe("Suite 5: YAML Output Format", () => {
|
|||||||
|
|
||||||
test("5.5 Special Characters Escaping", async () => {
|
test("5.5 Special Characters Escaping", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const textSchema = await putSchema(store, { type: "string" });
|
const textSchema = putSchema(store, { type: "string" });
|
||||||
const hash = store.cas.put(textSchema, "line1\nline2: value");
|
const hash = store.cas.put(textSchema, "line1\nline2: value");
|
||||||
|
|
||||||
const output = render(store, hash);
|
const output = render(store, hash);
|
||||||
@@ -668,8 +668,8 @@ describe("Suite 5: YAML Output Format", () => {
|
|||||||
|
|
||||||
test("5.6 Null Handling", async () => {
|
test("5.6 Null Handling", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const nullableSchema = await putSchema(store, {
|
const nullableSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
ref: {
|
ref: {
|
||||||
@@ -688,9 +688,9 @@ describe("Suite 5: YAML Output Format", () => {
|
|||||||
describe("Suite 6: Schema Integration", () => {
|
describe("Suite 6: Schema Integration", () => {
|
||||||
test("6.1 Detect ocas_ref Fields via Schema", async () => {
|
test("6.1 Detect ocas_ref Fields via Schema", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -698,7 +698,7 @@ describe("Suite 6: Schema Integration", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
link: { type: "string", format: "ocas_ref" },
|
link: { type: "string", format: "ocas_ref" },
|
||||||
@@ -717,8 +717,8 @@ describe("Suite 6: Schema Integration", () => {
|
|||||||
|
|
||||||
test("6.2 Non-ocas_ref String Not Expanded", async () => {
|
test("6.2 Non-ocas_ref String Not Expanded", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const objSchema = await putSchema(store, {
|
const objSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -735,9 +735,9 @@ describe("Suite 6: Schema Integration", () => {
|
|||||||
|
|
||||||
test("6.3 Array of ocas_ref", async () => {
|
test("6.3 Array of ocas_ref", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const itemSchema = await putSchema(store, {
|
const itemSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -746,7 +746,7 @@ describe("Suite 6: Schema Integration", () => {
|
|||||||
const item1 = store.cas.put(itemSchema, { name: "item1" });
|
const item1 = store.cas.put(itemSchema, { name: "item1" });
|
||||||
const item2 = store.cas.put(itemSchema, { name: "item2" });
|
const item2 = store.cas.put(itemSchema, { name: "item2" });
|
||||||
|
|
||||||
const arraySchema = await putSchema(store, {
|
const arraySchema = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: { type: "string", format: "ocas_ref" },
|
items: { type: "string", format: "ocas_ref" },
|
||||||
});
|
});
|
||||||
@@ -764,9 +764,9 @@ describe("Suite 6: Schema Integration", () => {
|
|||||||
|
|
||||||
test("6.4 anyOf with ocas_ref (Nullable Reference)", async () => {
|
test("6.4 anyOf with ocas_ref (Nullable Reference)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "string" },
|
value: { type: "string" },
|
||||||
@@ -774,7 +774,7 @@ describe("Suite 6: Schema Integration", () => {
|
|||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { value: "child" });
|
const childHash = store.cas.put(childSchema, { value: "child" });
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
ref: {
|
ref: {
|
||||||
@@ -795,7 +795,7 @@ describe("Suite 6: Schema Integration", () => {
|
|||||||
|
|
||||||
test("6.5 Schema-less Node (Bootstrap Node)", async () => {
|
test("6.5 Schema-less Node (Bootstrap Node)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const types = await bootstrap(store);
|
const types = bootstrap(store);
|
||||||
const schemaHash = types["@ocas/schema"];
|
const schemaHash = types["@ocas/schema"];
|
||||||
|
|
||||||
const output = render(store, schemaHash);
|
const output = render(store, schemaHash);
|
||||||
@@ -808,9 +808,9 @@ describe("Suite 6: Schema Integration", () => {
|
|||||||
describe("Suite 7: Error Handling", () => {
|
describe("Suite 7: Error Handling", () => {
|
||||||
test("7.1 Missing Referenced Node", async () => {
|
test("7.1 Missing Referenced Node", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -850,8 +850,8 @@ describe("Suite 7: Error Handling", () => {
|
|||||||
describe("Suite 8: Performance & Edge Cases", () => {
|
describe("Suite 8: Performance & Edge Cases", () => {
|
||||||
test("8.1 Large Payload", async () => {
|
test("8.1 Large Payload", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const arraySchema = await putSchema(store, {
|
const arraySchema = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: {
|
items: {
|
||||||
type: "object",
|
type: "object",
|
||||||
@@ -878,9 +878,9 @@ describe("Suite 8: Performance & Edge Cases", () => {
|
|||||||
|
|
||||||
test("8.2 Wide Fan-out", async () => {
|
test("8.2 Wide Fan-out", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const itemSchema = await putSchema(store, {
|
const itemSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
value: { type: "number" },
|
value: { type: "number" },
|
||||||
@@ -893,7 +893,7 @@ describe("Suite 8: Performance & Edge Cases", () => {
|
|||||||
children.push(hash);
|
children.push(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: { type: "string", format: "ocas_ref" },
|
items: { type: "string", format: "ocas_ref" },
|
||||||
});
|
});
|
||||||
@@ -910,8 +910,8 @@ describe("Suite 8: Performance & Edge Cases", () => {
|
|||||||
|
|
||||||
test("8.3 Empty Payload", async () => {
|
test("8.3 Empty Payload", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const emptySchema = await putSchema(store, { type: "object" });
|
const emptySchema = putSchema(store, { type: "object" });
|
||||||
const hash = store.cas.put(emptySchema, {});
|
const hash = store.cas.put(emptySchema, {});
|
||||||
|
|
||||||
const output = render(store, hash);
|
const output = render(store, hash);
|
||||||
@@ -921,8 +921,8 @@ describe("Suite 8: Performance & Edge Cases", () => {
|
|||||||
|
|
||||||
test("8.4 Unicode in Payload", async () => {
|
test("8.4 Unicode in Payload", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const textSchema = await putSchema(store, {
|
const textSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
text: { type: "string" },
|
text: { type: "string" },
|
||||||
@@ -986,17 +986,17 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => {
|
|||||||
|
|
||||||
test("9.5 Render with store expands ocas_ref fields", async () => {
|
test("9.5 Render with store expands ocas_ref fields", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
// Create a child node
|
// Create a child node
|
||||||
const childSchema = await putSchema(store, {
|
const childSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { msg: { type: "string" } },
|
properties: { msg: { type: "string" } },
|
||||||
});
|
});
|
||||||
const childHash = store.cas.put(childSchema, { msg: "inner" });
|
const childHash = store.cas.put(childSchema, { msg: "inner" });
|
||||||
|
|
||||||
// Parent schema with ocas_ref
|
// Parent schema with ocas_ref
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
child: { type: "string", format: "ocas_ref" },
|
child: { type: "string", format: "ocas_ref" },
|
||||||
@@ -1052,7 +1052,7 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => {
|
|||||||
|
|
||||||
test("9.10 store present but schema missing — renders without ref expansion", async () => {
|
test("9.10 store present but schema missing — renders without ref expansion", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const unknownType = "ZZZZZZZZZZZZ0" as Hash;
|
const unknownType = "ZZZZZZZZZZZZ0" as Hash;
|
||||||
const output = renderDirect(unknownType, { key: "val" }, store, null);
|
const output = renderDirect(unknownType, { key: "val" }, store, null);
|
||||||
expect(output).toContain("key: val");
|
expect(output).toContain("key: val");
|
||||||
@@ -1062,7 +1062,7 @@ describe("Suite 9: renderDirect (in-memory rendering)", () => {
|
|||||||
describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => {
|
describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => {
|
||||||
test("10.1 renderAsync() throws CasNodeNotFoundError for missing root hash", async () => {
|
test("10.1 renderAsync() throws CasNodeNotFoundError for missing root hash", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const fakeHash = "AAAAAAAAAAAAA" as Hash;
|
const fakeHash = "AAAAAAAAAAAAA" as Hash;
|
||||||
|
|
||||||
await expect(renderAsync(store, fakeHash)).rejects.toThrow(
|
await expect(renderAsync(store, fakeHash)).rejects.toThrow(
|
||||||
@@ -1093,9 +1093,9 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => {
|
|||||||
|
|
||||||
test("10.4 Missing nested node renders as cas: reference (no error)", async () => {
|
test("10.4 Missing nested node renders as cas: reference (no error)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const parentSchema = await putSchema(store, {
|
const parentSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
title: { type: "string" },
|
title: { type: "string" },
|
||||||
@@ -1117,9 +1117,9 @@ describe("Suite 10: Missing Root Hash Error Handling (Issue #53)", () => {
|
|||||||
|
|
||||||
test("10.5 Resolution below epsilon renders as cas: reference (no error)", async () => {
|
test("10.5 Resolution below epsilon renders as cas: reference (no error)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const nodeSchema = await putSchema(store, {
|
const nodeSchema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
level: { type: "number" },
|
level: { type: "number" },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CasNodeNotFoundError } from "./errors.js";
|
import { CasNodeNotFoundError } from "./errors.js";
|
||||||
import { renderWithTemplate } from "./liquid-render.js";
|
import { renderWithTemplate } from "./liquid-render.js";
|
||||||
import { collectRefs, getSchema, putSchema, refs } from "./schema.js";
|
import { collectRefs, getSchema, putSchema, refs } from "./schema.js";
|
||||||
import type { Hash, OcasStore } from "./types.js";
|
import type { Hash, Store } from "./types.js";
|
||||||
|
|
||||||
export type RenderOptions = {
|
export type RenderOptions = {
|
||||||
resolution?: number; // (0, 1], default 1.0
|
resolution?: number; // (0, 1], default 1.0
|
||||||
@@ -47,7 +47,7 @@ function validateAndExtractOptions(options: RenderOptions | null | undefined): {
|
|||||||
* For template support, use renderAsync().
|
* For template support, use renderAsync().
|
||||||
*/
|
*/
|
||||||
export function render(
|
export function render(
|
||||||
store: OcasStore,
|
store: Store,
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
options?: RenderOptions,
|
options?: RenderOptions,
|
||||||
): string {
|
): string {
|
||||||
@@ -68,7 +68,7 @@ export function render(
|
|||||||
* Attempts to use LiquidJS templates first, falling back to YAML.
|
* Attempts to use LiquidJS templates first, falling back to YAML.
|
||||||
*/
|
*/
|
||||||
export async function renderAsync(
|
export async function renderAsync(
|
||||||
store: OcasStore,
|
store: Store,
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
options?: RenderOptions,
|
options?: RenderOptions,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
@@ -111,7 +111,7 @@ export async function renderAsync(
|
|||||||
export function renderDirect(
|
export function renderDirect(
|
||||||
typeHash: Hash,
|
typeHash: Hash,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
store: OcasStore | null,
|
store: Store | null,
|
||||||
options: RenderOptions | null,
|
options: RenderOptions | null,
|
||||||
): string {
|
): string {
|
||||||
const { resolution, decay, epsilon } = validateAndExtractOptions(options);
|
const { resolution, decay, epsilon } = validateAndExtractOptions(options);
|
||||||
@@ -142,10 +142,10 @@ export function renderDirect(
|
|||||||
/**
|
/**
|
||||||
* Check if a template exists for a given type
|
* Check if a template exists for a given type
|
||||||
*/
|
*/
|
||||||
async function hasTemplate(store: OcasStore, typeHash: Hash): Promise<boolean> {
|
async function hasTemplate(store: Store, typeHash: Hash): Promise<boolean> {
|
||||||
const varName = `@ocas/template/text/${typeHash}`;
|
const varName = `@ocas/template/text/${typeHash}`;
|
||||||
try {
|
try {
|
||||||
const stringSchema = await putSchema(store, { type: "string" });
|
const stringSchema = putSchema(store, { type: "string" });
|
||||||
const variable = store.var.get(varName, stringSchema);
|
const variable = store.var.get(varName, stringSchema);
|
||||||
return variable !== null;
|
return variable !== null;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -154,7 +154,7 @@ async function hasTemplate(store: OcasStore, typeHash: Hash): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderNode(
|
function renderNode(
|
||||||
store: OcasStore | null,
|
store: Store | null,
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
currentResolution: number,
|
currentResolution: number,
|
||||||
decay: number,
|
decay: number,
|
||||||
@@ -203,7 +203,7 @@ function renderNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderValue(
|
function renderValue(
|
||||||
store: OcasStore | null,
|
store: Store | null,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
refHashes: Set<Hash>,
|
refHashes: Set<Hash>,
|
||||||
childResolution: number,
|
childResolution: number,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { CasNode } from "./types.js";
|
|||||||
describe("putSchema", () => {
|
describe("putSchema", () => {
|
||||||
test("returns a valid 13-char hash", async () => {
|
test("returns a valid 13-char hash", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, { type: "object", properties: {} });
|
const hash = putSchema(store, { type: "object", properties: {} });
|
||||||
expect(hash).toHaveLength(13);
|
expect(hash).toHaveLength(13);
|
||||||
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||||
});
|
});
|
||||||
@@ -19,7 +19,7 @@ describe("putSchema", () => {
|
|||||||
test("schema node is stored in the store", async () => {
|
test("schema node is stored in the store", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schema = { type: "object", properties: { name: { type: "string" } } };
|
const schema = { type: "object", properties: { name: { type: "string" } } };
|
||||||
const hash = await putSchema(store, schema);
|
const hash = putSchema(store, schema);
|
||||||
|
|
||||||
expect(store.cas.has(hash)).toBe(true);
|
expect(store.cas.has(hash)).toBe(true);
|
||||||
const node = store.cas.get(hash);
|
const node = store.cas.get(hash);
|
||||||
@@ -29,9 +29,9 @@ describe("putSchema", () => {
|
|||||||
|
|
||||||
test("schema node type equals the meta-schema hash", async () => {
|
test("schema node type equals the meta-schema hash", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const schemaHash = await putSchema(store, { type: "string" });
|
const schemaHash = putSchema(store, { type: "string" });
|
||||||
const node = store.cas.get(schemaHash) as CasNode;
|
const node = store.cas.get(schemaHash) as CasNode;
|
||||||
|
|
||||||
expect(node.type).toBe(metaHash);
|
expect(node.type).toBe(metaHash);
|
||||||
@@ -40,16 +40,16 @@ describe("putSchema", () => {
|
|||||||
test("putSchema is idempotent: same schema → same hash", async () => {
|
test("putSchema is idempotent: same schema → same hash", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schema = { type: "number" };
|
const schema = { type: "number" };
|
||||||
const h1 = await putSchema(store, schema);
|
const h1 = putSchema(store, schema);
|
||||||
const h2 = await putSchema(store, schema);
|
const h2 = putSchema(store, schema);
|
||||||
|
|
||||||
expect(h1).toBe(h2);
|
expect(h1).toBe(h2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("different schemas produce different hashes", async () => {
|
test("different schemas produce different hashes", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const h1 = await putSchema(store, { type: "string" });
|
const h1 = putSchema(store, { type: "string" });
|
||||||
const h2 = await putSchema(store, { type: "number" });
|
const h2 = putSchema(store, { type: "number" });
|
||||||
|
|
||||||
expect(h1).not.toBe(h2);
|
expect(h1).not.toBe(h2);
|
||||||
});
|
});
|
||||||
@@ -62,7 +62,7 @@ describe("getSchema", () => {
|
|||||||
test("returns the original schema object", async () => {
|
test("returns the original schema object", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schema = { type: "object", properties: { age: { type: "number" } } };
|
const schema = { type: "object", properties: { age: { type: "number" } } };
|
||||||
const hash = await putSchema(store, schema);
|
const hash = putSchema(store, schema);
|
||||||
|
|
||||||
expect(getSchema(store, hash)).toEqual(schema);
|
expect(getSchema(store, hash)).toEqual(schema);
|
||||||
});
|
});
|
||||||
@@ -82,7 +82,7 @@ describe("getSchema", () => {
|
|||||||
label: { type: "string" },
|
label: { type: "string" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const hash = await putSchema(store, schema);
|
const hash = putSchema(store, schema);
|
||||||
expect(getSchema(store, hash)).toEqual(schema);
|
expect(getSchema(store, hash)).toEqual(schema);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -93,7 +93,7 @@ describe("getSchema", () => {
|
|||||||
describe("validate", () => {
|
describe("validate", () => {
|
||||||
test("returns true when payload matches the schema", async () => {
|
test("returns true when payload matches the schema", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { name: { type: "string" }, age: { type: "number" } },
|
properties: { name: { type: "string" }, age: { type: "number" } },
|
||||||
required: ["name"],
|
required: ["name"],
|
||||||
@@ -106,7 +106,7 @@ describe("validate", () => {
|
|||||||
|
|
||||||
test("returns false when payload violates the schema", async () => {
|
test("returns false when payload violates the schema", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { count: { type: "number" } },
|
properties: { count: { type: "number" } },
|
||||||
required: ["count"],
|
required: ["count"],
|
||||||
@@ -119,7 +119,7 @@ describe("validate", () => {
|
|||||||
|
|
||||||
test("returns false when required field is missing", async () => {
|
test("returns false when required field is missing", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["title"],
|
required: ["title"],
|
||||||
properties: { title: { type: "string" } },
|
properties: { title: { type: "string" } },
|
||||||
@@ -148,7 +148,7 @@ describe("validate", () => {
|
|||||||
describe("refs", () => {
|
describe("refs", () => {
|
||||||
test("returns empty array when schema has no ocas_ref fields", async () => {
|
test("returns empty array when schema has no ocas_ref fields", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { title: { type: "string" } },
|
properties: { title: { type: "string" } },
|
||||||
});
|
});
|
||||||
@@ -160,7 +160,7 @@ describe("refs", () => {
|
|||||||
|
|
||||||
test("returns the ocas_ref hash values from payload", async () => {
|
test("returns the ocas_ref hash values from payload", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
parentHash: { type: "string", format: "ocas_ref" },
|
parentHash: { type: "string", format: "ocas_ref" },
|
||||||
@@ -180,7 +180,7 @@ describe("refs", () => {
|
|||||||
|
|
||||||
test("collects multiple ocas_ref fields", async () => {
|
test("collects multiple ocas_ref fields", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
leftHash: { type: "string", format: "ocas_ref" },
|
leftHash: { type: "string", format: "ocas_ref" },
|
||||||
@@ -204,7 +204,7 @@ describe("refs", () => {
|
|||||||
|
|
||||||
test("skips null/undefined ocas_ref values", async () => {
|
test("skips null/undefined ocas_ref values", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
optionalRef: { type: "string", format: "ocas_ref" },
|
optionalRef: { type: "string", format: "ocas_ref" },
|
||||||
@@ -236,7 +236,7 @@ describe("refs", () => {
|
|||||||
describe("walk", () => {
|
describe("walk", () => {
|
||||||
test("visits a single node with no refs", async () => {
|
test("visits a single node with no refs", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { val: { type: "number" } },
|
properties: { val: { type: "number" } },
|
||||||
});
|
});
|
||||||
@@ -250,7 +250,7 @@ describe("walk", () => {
|
|||||||
|
|
||||||
test("visits all reachable nodes in a chain A → B → C", async () => {
|
test("visits all reachable nodes in a chain A → B → C", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
nextHash: { type: "string", format: "ocas_ref" },
|
nextHash: { type: "string", format: "ocas_ref" },
|
||||||
@@ -274,7 +274,7 @@ describe("walk", () => {
|
|||||||
|
|
||||||
test("handles cycles without infinite loop", async () => {
|
test("handles cycles without infinite loop", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
peerHash: { type: "string", format: "ocas_ref" },
|
peerHash: { type: "string", format: "ocas_ref" },
|
||||||
@@ -292,7 +292,7 @@ describe("walk", () => {
|
|||||||
const hashD = store.cas.put(schemaHash, { peerHash: hashC, val: 4 });
|
const hashD = store.cas.put(schemaHash, { peerHash: hashC, val: 4 });
|
||||||
const hashE = store.cas.put(schemaHash, { peerHash: hashC, val: 5 });
|
const hashE = store.cas.put(schemaHash, { peerHash: hashC, val: 5 });
|
||||||
|
|
||||||
const schemaHash2 = await putSchema(store, {
|
const schemaHash2 = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
leftHash: { type: "string", format: "ocas_ref" },
|
leftHash: { type: "string", format: "ocas_ref" },
|
||||||
@@ -317,7 +317,7 @@ describe("walk", () => {
|
|||||||
|
|
||||||
test("skips missing hashes gracefully", async () => {
|
test("skips missing hashes gracefully", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { ref: { type: "string", format: "ocas_ref" } },
|
properties: { ref: { type: "string", format: "ocas_ref" } },
|
||||||
});
|
});
|
||||||
@@ -332,7 +332,7 @@ describe("walk", () => {
|
|||||||
|
|
||||||
test("visitor receives both hash and node", async () => {
|
test("visitor receives both hash and node", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { x: { type: "number" } },
|
properties: { x: { type: "number" } },
|
||||||
});
|
});
|
||||||
@@ -356,7 +356,7 @@ describe("walk", () => {
|
|||||||
describe("bootstrap meta-schema self-reference", () => {
|
describe("bootstrap meta-schema self-reference", () => {
|
||||||
test("metaNode.type === metaHash (self-referencing)", async () => {
|
test("metaNode.type === metaHash (self-referencing)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const metaNode = store.cas.get(metaHash) as CasNode;
|
const metaNode = store.cas.get(metaHash) as CasNode;
|
||||||
|
|
||||||
@@ -365,9 +365,9 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("schema nodes have type === metaHash", async () => {
|
test("schema nodes have type === metaHash", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const schemaHash = await putSchema(store, { type: "string" });
|
const schemaHash = putSchema(store, { type: "string" });
|
||||||
const schemaNode = store.cas.get(schemaHash) as CasNode;
|
const schemaNode = store.cas.get(schemaHash) as CasNode;
|
||||||
|
|
||||||
expect(schemaNode.type).toBe(metaHash);
|
expect(schemaNode.type).toBe(metaHash);
|
||||||
@@ -375,9 +375,9 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("data nodes have type === schemaHash (not metaHash)", async () => {
|
test("data nodes have type === schemaHash (not metaHash)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { val: { type: "number" } },
|
properties: { val: { type: "number" } },
|
||||||
});
|
});
|
||||||
@@ -392,7 +392,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with numeric constraints (minimum/maximum)", async () => {
|
test("accepts schema with numeric constraints (minimum/maximum)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "number",
|
type: "number",
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
maximum: 100,
|
maximum: 100,
|
||||||
@@ -414,7 +414,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with string constraints (minLength/maxLength/pattern)", async () => {
|
test("accepts schema with string constraints (minLength/maxLength/pattern)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "string",
|
type: "string",
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
maxLength: 10,
|
maxLength: 10,
|
||||||
@@ -431,7 +431,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with array constraints (minItems/maxItems/uniqueItems)", async () => {
|
test("accepts schema with array constraints (minItems/maxItems/uniqueItems)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: { type: "number" },
|
items: { type: "number" },
|
||||||
minItems: 1,
|
minItems: 1,
|
||||||
@@ -452,20 +452,20 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("rejects schema with wrong constraint types", async () => {
|
test("rejects schema with wrong constraint types", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await expect(
|
expect(() =>
|
||||||
putSchema(store, { type: "number", minimum: "zero" } as never),
|
putSchema(store, { type: "number", minimum: "zero" } as never),
|
||||||
).rejects.toThrow();
|
).toThrow();
|
||||||
await expect(
|
expect(() =>
|
||||||
putSchema(store, { type: "string", maxLength: true } as never),
|
putSchema(store, { type: "string", maxLength: true } as never),
|
||||||
).rejects.toThrow();
|
).toThrow();
|
||||||
await expect(
|
expect(() =>
|
||||||
putSchema(store, { type: "array", uniqueItems: 1 } as never),
|
putSchema(store, { type: "array", uniqueItems: 1 } as never),
|
||||||
).rejects.toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("accepts schema with nested property constraints", async () => {
|
test("accepts schema with nested property constraints", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string", minLength: 1, maxLength: 50 },
|
name: { type: "string", minLength: 1, maxLength: 50 },
|
||||||
@@ -491,11 +491,11 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("bootstrap is idempotent across putSchema calls", async () => {
|
test("bootstrap is idempotent across putSchema calls", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
|
|
||||||
await putSchema(store, { type: "string" });
|
putSchema(store, { type: "string" });
|
||||||
await putSchema(store, { type: "number" });
|
putSchema(store, { type: "number" });
|
||||||
|
|
||||||
// bootstrap node should still be there and unchanged
|
// bootstrap node should still be there and unchanged
|
||||||
const metaNode = store.cas.get(metaHash) as CasNode;
|
const metaNode = store.cas.get(metaHash) as CasNode;
|
||||||
@@ -506,7 +506,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with allOf", async () => {
|
test("accepts schema with allOf", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
allOf: [
|
allOf: [
|
||||||
{ type: "object", properties: { name: { type: "string" } } },
|
{ type: "object", properties: { name: { type: "string" } } },
|
||||||
{ required: ["name"] },
|
{ required: ["name"] },
|
||||||
@@ -523,7 +523,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with if/then/else", async () => {
|
test("accepts schema with if/then/else", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
kind: { type: "string" },
|
kind: { type: "string" },
|
||||||
@@ -539,7 +539,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with patternProperties", async () => {
|
test("accepts schema with patternProperties", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
patternProperties: {
|
patternProperties: {
|
||||||
"^x-": { type: "string" },
|
"^x-": { type: "string" },
|
||||||
@@ -553,7 +553,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with prefixItems (tuple)", async () => {
|
test("accepts schema with prefixItems (tuple)", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
prefixItems: [{ type: "string" }, { type: "number" }],
|
prefixItems: [{ type: "string" }, { type: "number" }],
|
||||||
});
|
});
|
||||||
@@ -562,7 +562,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with multipleOf", async () => {
|
test("accepts schema with multipleOf", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "number",
|
type: "number",
|
||||||
multipleOf: 5,
|
multipleOf: 5,
|
||||||
});
|
});
|
||||||
@@ -577,7 +577,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with minProperties/maxProperties", async () => {
|
test("accepts schema with minProperties/maxProperties", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
minProperties: 1,
|
minProperties: 1,
|
||||||
maxProperties: 3,
|
maxProperties: 3,
|
||||||
@@ -593,7 +593,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with default value", async () => {
|
test("accepts schema with default value", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "string",
|
type: "string",
|
||||||
default: "hello",
|
default: "hello",
|
||||||
});
|
});
|
||||||
@@ -602,21 +602,17 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("rejects invalid P2 keyword types", async () => {
|
test("rejects invalid P2 keyword types", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await expect(
|
expect(() => putSchema(store, { allOf: "not-array" } as never)).toThrow();
|
||||||
putSchema(store, { allOf: "not-array" } as never),
|
expect(() => putSchema(store, { multipleOf: "five" } as never)).toThrow();
|
||||||
).rejects.toThrow();
|
expect(() =>
|
||||||
await expect(
|
|
||||||
putSchema(store, { multipleOf: "five" } as never),
|
|
||||||
).rejects.toThrow();
|
|
||||||
await expect(
|
|
||||||
putSchema(store, { patternProperties: [1, 2] } as never),
|
putSchema(store, { patternProperties: [1, 2] } as never),
|
||||||
).rejects.toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("collectRefs traverses allOf sub-schemas", async () => {
|
test("collectRefs traverses allOf sub-schemas", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const innerSchema = await putSchema(store, { type: "string" });
|
const innerSchema = putSchema(store, { type: "string" });
|
||||||
const schema = await putSchema(store, {
|
const schema = putSchema(store, {
|
||||||
allOf: [
|
allOf: [
|
||||||
{
|
{
|
||||||
type: "object",
|
type: "object",
|
||||||
@@ -634,8 +630,8 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("collectRefs traverses patternProperties", async () => {
|
test("collectRefs traverses patternProperties", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const innerSchema = await putSchema(store, { type: "string" });
|
const innerSchema = putSchema(store, { type: "string" });
|
||||||
const schema = await putSchema(store, {
|
const schema = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
patternProperties: {
|
patternProperties: {
|
||||||
"^ref_": { type: "string", format: "ocas_ref" },
|
"^ref_": { type: "string", format: "ocas_ref" },
|
||||||
@@ -651,8 +647,8 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("collectRefs traverses prefixItems", async () => {
|
test("collectRefs traverses prefixItems", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const innerSchema = await putSchema(store, { type: "string" });
|
const innerSchema = putSchema(store, { type: "string" });
|
||||||
const schema = await putSchema(store, {
|
const schema = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
prefixItems: [{ type: "string", format: "ocas_ref" }, { type: "number" }],
|
prefixItems: [{ type: "string", format: "ocas_ref" }, { type: "number" }],
|
||||||
});
|
});
|
||||||
@@ -668,7 +664,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with not", async () => {
|
test("accepts schema with not", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
not: { type: "string" },
|
not: { type: "string" },
|
||||||
});
|
});
|
||||||
expect(hash).toHaveLength(13);
|
expect(hash).toHaveLength(13);
|
||||||
@@ -682,7 +678,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with contains", async () => {
|
test("accepts schema with contains", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
contains: { type: "number", minimum: 10 },
|
contains: { type: "number", minimum: 10 },
|
||||||
});
|
});
|
||||||
@@ -697,7 +693,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with propertyNames", async () => {
|
test("accepts schema with propertyNames", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
propertyNames: { pattern: "^[a-z]+$" },
|
propertyNames: { pattern: "^[a-z]+$" },
|
||||||
});
|
});
|
||||||
@@ -712,7 +708,7 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("accepts schema with metadata keywords", async () => {
|
test("accepts schema with metadata keywords", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "string",
|
type: "string",
|
||||||
examples: ["hello", "world"],
|
examples: ["hello", "world"],
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
@@ -724,22 +720,18 @@ describe("bootstrap meta-schema self-reference", () => {
|
|||||||
|
|
||||||
test("rejects invalid P3 keyword types", async () => {
|
test("rejects invalid P3 keyword types", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await expect(
|
expect(() => putSchema(store, { not: "not-object" } as never)).toThrow();
|
||||||
putSchema(store, { not: "not-object" } as never),
|
expect(() =>
|
||||||
).rejects.toThrow();
|
|
||||||
await expect(
|
|
||||||
putSchema(store, { examples: "not-array" } as never),
|
putSchema(store, { examples: "not-array" } as never),
|
||||||
).rejects.toThrow();
|
).toThrow();
|
||||||
await expect(
|
expect(() => putSchema(store, { readOnly: "yes" } as never)).toThrow();
|
||||||
putSchema(store, { readOnly: "yes" } as never),
|
expect(() => putSchema(store, { $comment: 42 } as never)).toThrow();
|
||||||
).rejects.toThrow();
|
|
||||||
await expect(putSchema(store, { $comment: 42 } as never)).rejects.toThrow();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("collectRefs traverses contains", async () => {
|
test("collectRefs traverses contains", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const innerSchema = await putSchema(store, { type: "string" });
|
const innerSchema = putSchema(store, { type: "string" });
|
||||||
const schema = await putSchema(store, {
|
const schema = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
contains: { type: "string", format: "ocas_ref" },
|
contains: { type: "string", format: "ocas_ref" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const Ajv = ((AjvModule as any).default ?? AjvModule) as {
|
|||||||
};
|
};
|
||||||
|
|
||||||
import { bootstrap } from "./bootstrap.js";
|
import { bootstrap } from "./bootstrap.js";
|
||||||
import type { CasNode, Hash, OcasStore } from "./types.js";
|
import type { CasNode, Hash, Store } from "./types.js";
|
||||||
|
|
||||||
export type JSONSchema = Record<string, unknown>;
|
export type JSONSchema = Record<string, unknown>;
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ function isValidSchema(value: unknown): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMetaSchemaNode(store: OcasStore, node: CasNode): boolean {
|
function isMetaSchemaNode(store: Store, node: CasNode): boolean {
|
||||||
const schema = getSchema(store, node.type);
|
const schema = getSchema(store, node.type);
|
||||||
return schema !== null && schema === node.payload;
|
return schema !== null && schema === node.payload;
|
||||||
}
|
}
|
||||||
@@ -248,11 +248,8 @@ function isMetaSchemaNode(store: OcasStore, node: CasNode): boolean {
|
|||||||
* Store a JSON Schema as a CAS node typed by the meta-schema hash.
|
* Store a JSON Schema as a CAS node typed by the meta-schema hash.
|
||||||
* The returned hash becomes the typeHash for nodes that conform to this schema.
|
* The returned hash becomes the typeHash for nodes that conform to this schema.
|
||||||
*/
|
*/
|
||||||
export async function putSchema(
|
export function putSchema(store: Store, jsonSchema: JSONSchema): Hash {
|
||||||
store: OcasStore,
|
const builtinSchemas = bootstrap(store);
|
||||||
jsonSchema: JSONSchema,
|
|
||||||
): Promise<Hash> {
|
|
||||||
const builtinSchemas = await bootstrap(store);
|
|
||||||
const metaHash = builtinSchemas["@ocas/schema"];
|
const metaHash = builtinSchemas["@ocas/schema"];
|
||||||
if (!metaHash) {
|
if (!metaHash) {
|
||||||
throw new Error("Meta-schema not found in bootstrap result");
|
throw new Error("Meta-schema not found in bootstrap result");
|
||||||
@@ -262,14 +259,14 @@ export async function putSchema(
|
|||||||
"Invalid schema: input does not conform to the ocas JSON Schema meta-schema",
|
"Invalid schema: input does not conform to the ocas JSON Schema meta-schema",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Promise.resolve(store.cas.put(metaHash, jsonSchema));
|
return store.cas.put(metaHash, jsonSchema);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the JSON Schema payload for a given type hash.
|
* Retrieve the JSON Schema payload for a given type hash.
|
||||||
* Returns null if no node exists at that hash.
|
* Returns null if no node exists at that hash.
|
||||||
*/
|
*/
|
||||||
export function getSchema(store: OcasStore, typeHash: Hash): JSONSchema | null {
|
export function getSchema(store: Store, typeHash: Hash): JSONSchema | null {
|
||||||
const node = store.cas.get(typeHash);
|
const node = store.cas.get(typeHash);
|
||||||
if (node === null) return null;
|
if (node === null) return null;
|
||||||
return node.payload as JSONSchema;
|
return node.payload as JSONSchema;
|
||||||
@@ -279,7 +276,7 @@ export function getSchema(store: OcasStore, typeHash: Hash): JSONSchema | null {
|
|||||||
* Validate a node's payload against the schema identified by node.type.
|
* Validate a node's payload against the schema identified by node.type.
|
||||||
* Returns false if the schema cannot be found or validation fails.
|
* Returns false if the schema cannot be found or validation fails.
|
||||||
*/
|
*/
|
||||||
export function validate(store: OcasStore, node: CasNode): boolean {
|
export function validate(store: Store, node: CasNode): boolean {
|
||||||
const schema = getSchema(store, node.type);
|
const schema = getSchema(store, node.type);
|
||||||
if (schema === null) return false;
|
if (schema === null) return false;
|
||||||
if (isMetaSchemaNode(store, node)) {
|
if (isMetaSchemaNode(store, node)) {
|
||||||
@@ -416,7 +413,7 @@ export function collectRefs(schema: JSONSchema, value: unknown): Hash[] {
|
|||||||
* Return all hashes referenced by this node via ocas_ref fields in its schema.
|
* Return all hashes referenced by this node via ocas_ref fields in its schema.
|
||||||
* Null/undefined values are skipped.
|
* Null/undefined values are skipped.
|
||||||
*/
|
*/
|
||||||
export function refs(store: OcasStore, node: CasNode): Hash[] {
|
export function refs(store: Store, node: CasNode): Hash[] {
|
||||||
const schema = getSchema(store, node.type);
|
const schema = getSchema(store, node.type);
|
||||||
if (schema === null) return [];
|
if (schema === null) return [];
|
||||||
return collectRefs(schema, node.payload);
|
return collectRefs(schema, node.payload);
|
||||||
@@ -428,7 +425,7 @@ export function refs(store: OcasStore, node: CasNode): Hash[] {
|
|||||||
* Handles cycles via a visited set.
|
* Handles cycles via a visited set.
|
||||||
*/
|
*/
|
||||||
export function walk(
|
export function walk(
|
||||||
store: OcasStore,
|
store: Store,
|
||||||
rootHash: Hash,
|
rootHash: Hash,
|
||||||
visitor: (hash: Hash, node: CasNode) => void,
|
visitor: (hash: Hash, node: CasNode) => void,
|
||||||
): void {
|
): void {
|
||||||
|
|||||||
+36
-110
@@ -2,13 +2,7 @@ import {
|
|||||||
BOOTSTRAP_STORE,
|
BOOTSTRAP_STORE,
|
||||||
type BootstrapCapableStore,
|
type BootstrapCapableStore,
|
||||||
} from "./bootstrap-capable.js";
|
} from "./bootstrap-capable.js";
|
||||||
import {
|
import { SchemaMismatchError, VariableNotFoundError } from "./errors.js";
|
||||||
CasNodeNotFoundError,
|
|
||||||
MAX_HISTORY,
|
|
||||||
SchemaMismatchError,
|
|
||||||
TagLabelConflictError,
|
|
||||||
VariableNotFoundError,
|
|
||||||
} from "./errors.js";
|
|
||||||
import { computeHashSync, computeSelfHashSync, initHasher } from "./hash.js";
|
import { computeHashSync, computeSelfHashSync, initHasher } from "./hash.js";
|
||||||
import { applyListOptions, casListEntry } from "./list-utils.js";
|
import { applyListOptions, casListEntry } from "./list-utils.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -18,7 +12,7 @@ import type {
|
|||||||
HistoryEntry,
|
HistoryEntry,
|
||||||
ListEntry,
|
ListEntry,
|
||||||
ListOptions,
|
ListOptions,
|
||||||
OcasStore,
|
Store,
|
||||||
Tag,
|
Tag,
|
||||||
TagOp,
|
TagOp,
|
||||||
TagStore,
|
TagStore,
|
||||||
@@ -27,6 +21,16 @@ import type {
|
|||||||
VarStore,
|
VarStore,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import { validateName } from "./validation.js";
|
import { validateName } from "./validation.js";
|
||||||
|
import {
|
||||||
|
addNameIndex,
|
||||||
|
checkTagLabelConflict,
|
||||||
|
cloneVarRecord,
|
||||||
|
extractSchema,
|
||||||
|
pushHistory,
|
||||||
|
removeNameIndex,
|
||||||
|
type VarRecord,
|
||||||
|
varKey,
|
||||||
|
} from "./var-store-helpers.js";
|
||||||
import type { Variable } from "./variable.js";
|
import type { Variable } from "./variable.js";
|
||||||
|
|
||||||
// Initialise the xxhash WASM instance once at module load. This allows the
|
// Initialise the xxhash WASM instance once at module load. This allows the
|
||||||
@@ -34,7 +38,7 @@ import type { Variable } from "./variable.js";
|
|||||||
await initHasher();
|
await initHasher();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The cas sub-store of an in-memory `OcasStore` — also satisfies the legacy
|
* The cas sub-store of an in-memory `Store` — also satisfies the legacy
|
||||||
* `BootstrapCapableStore` interface so that helpers that have not yet been
|
* `BootstrapCapableStore` interface so that helpers that have not yet been
|
||||||
* refactored (e.g. bootstrap, gc, render) continue to work against
|
* refactored (e.g. bootstrap, gc, render) continue to work against
|
||||||
* `store.cas`.
|
* `store.cas`.
|
||||||
@@ -142,32 +146,9 @@ function createCasStore(): MemoryCasStore {
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
type VarRecord = {
|
|
||||||
name: string;
|
|
||||||
schema: Hash;
|
|
||||||
value: Hash;
|
|
||||||
created: number;
|
|
||||||
updated: number;
|
|
||||||
tags: Record<string, string>;
|
|
||||||
labels: string[];
|
|
||||||
history: HistoryEntry[];
|
|
||||||
};
|
|
||||||
|
|
||||||
function cloneVar(rec: VarRecord): Variable {
|
|
||||||
return {
|
|
||||||
name: rec.name,
|
|
||||||
schema: rec.schema,
|
|
||||||
value: rec.value,
|
|
||||||
created: rec.created,
|
|
||||||
updated: rec.updated,
|
|
||||||
tags: { ...rec.tags },
|
|
||||||
labels: [...rec.labels],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an in-memory `VarStore` backed by the supplied CAS store. Exposed so
|
* Build an in-memory `VarStore` backed by the supplied CAS store. Exposed so
|
||||||
* non-Memory CAS stores (e.g. the FS store) can compose a full `OcasStore`
|
* non-Memory CAS stores (e.g. the FS store) can compose a full `Store`
|
||||||
* without re-implementing variable storage.
|
* without re-implementing variable storage.
|
||||||
*/
|
*/
|
||||||
export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
||||||
@@ -175,73 +156,18 @@ export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
|||||||
const records = new Map<string, VarRecord>();
|
const records = new Map<string, VarRecord>();
|
||||||
const byName = new Map<string, Set<string>>(); // name -> set of composite keys
|
const byName = new Map<string, Set<string>>(); // name -> set of composite keys
|
||||||
|
|
||||||
function key(name: string, schema: Hash): string {
|
|
||||||
return `${name}\u0000${schema}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addIndex(name: string, k: string): void {
|
|
||||||
let set = byName.get(name);
|
|
||||||
if (!set) {
|
|
||||||
set = new Set();
|
|
||||||
byName.set(name, set);
|
|
||||||
}
|
|
||||||
set.add(k);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeIndex(name: string, k: string): void {
|
|
||||||
const set = byName.get(name);
|
|
||||||
if (!set) return;
|
|
||||||
set.delete(k);
|
|
||||||
if (set.size === 0) byName.delete(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractSchema(hash: Hash): Hash {
|
|
||||||
const node = cas.get(hash);
|
|
||||||
if (node === null) {
|
|
||||||
throw new CasNodeNotFoundError(hash);
|
|
||||||
}
|
|
||||||
return node.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkConflict(tags: Record<string, string>, labels: string[]): void {
|
|
||||||
for (const tk of Object.keys(tags)) {
|
|
||||||
if (labels.includes(tk)) {
|
|
||||||
throw new TagLabelConflictError(tk, "label", "tag");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushHistory(rec: VarRecord, value: Hash, now: number): boolean {
|
|
||||||
if (rec.history.length > 0 && rec.history[0]?.value === value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const existingIdx = rec.history.findIndex((e) => e.value === value);
|
|
||||||
if (existingIdx > 0) {
|
|
||||||
rec.history.splice(existingIdx, 1);
|
|
||||||
}
|
|
||||||
rec.history.unshift({ value, position: 0, setAt: now });
|
|
||||||
if (rec.history.length > MAX_HISTORY) {
|
|
||||||
rec.history.length = MAX_HISTORY;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < rec.history.length; i++) {
|
|
||||||
const entry = rec.history[i];
|
|
||||||
if (entry !== undefined) entry.position = i;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const varStore: VarStore = {
|
const varStore: VarStore = {
|
||||||
set(name: string, hash: Hash, options?: VarSetOptions): Variable {
|
set(name: string, hash: Hash, options?: VarSetOptions): Variable {
|
||||||
validateName(name);
|
validateName(name);
|
||||||
const schema = extractSchema(hash);
|
const schema = extractSchema(cas, hash);
|
||||||
const k = key(name, schema);
|
const k = varKey(name, schema);
|
||||||
const existing = records.get(k);
|
const existing = records.get(k);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const tags = options?.tags ?? existing.tags;
|
const tags = options?.tags ?? existing.tags;
|
||||||
const labels = options?.labels ?? existing.labels;
|
const labels = options?.labels ?? existing.labels;
|
||||||
if (options !== undefined) checkConflict(tags, labels);
|
if (options !== undefined) checkTagLabelConflict(tags, labels);
|
||||||
const changed = pushHistory(existing, hash, now);
|
const changed = pushHistory(existing, hash, now);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
existing.value = hash;
|
existing.value = hash;
|
||||||
@@ -251,12 +177,12 @@ export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
|||||||
existing.tags = { ...tags };
|
existing.tags = { ...tags };
|
||||||
existing.labels = [...labels];
|
existing.labels = [...labels];
|
||||||
}
|
}
|
||||||
return cloneVar(existing);
|
return cloneVarRecord(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = options?.tags ?? {};
|
const tags = options?.tags ?? {};
|
||||||
const labels = options?.labels ?? [];
|
const labels = options?.labels ?? [];
|
||||||
checkConflict(tags, labels);
|
checkTagLabelConflict(tags, labels);
|
||||||
const rec: VarRecord = {
|
const rec: VarRecord = {
|
||||||
name,
|
name,
|
||||||
schema,
|
schema,
|
||||||
@@ -268,14 +194,14 @@ export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
|||||||
history: [{ value: hash, position: 0, setAt: now }],
|
history: [{ value: hash, position: 0, setAt: now }],
|
||||||
};
|
};
|
||||||
records.set(k, rec);
|
records.set(k, rec);
|
||||||
addIndex(name, k);
|
addNameIndex(byName, name, k);
|
||||||
return cloneVar(rec);
|
return cloneVarRecord(rec);
|
||||||
},
|
},
|
||||||
|
|
||||||
get(name: string, schema?: Hash): Variable | null {
|
get(name: string, schema?: Hash): Variable | null {
|
||||||
if (schema !== undefined) {
|
if (schema !== undefined) {
|
||||||
const rec = records.get(key(name, schema));
|
const rec = records.get(varKey(name, schema));
|
||||||
return rec ? cloneVar(rec) : null;
|
return rec ? cloneVarRecord(rec) : null;
|
||||||
}
|
}
|
||||||
// No schema: if exactly one variant, return it; otherwise null
|
// No schema: if exactly one variant, return it; otherwise null
|
||||||
const set = byName.get(name);
|
const set = byName.get(name);
|
||||||
@@ -283,17 +209,17 @@ export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
|||||||
const onlyKey = set.values().next().value;
|
const onlyKey = set.values().next().value;
|
||||||
if (onlyKey === undefined) return null;
|
if (onlyKey === undefined) return null;
|
||||||
const rec = records.get(onlyKey);
|
const rec = records.get(onlyKey);
|
||||||
return rec ? cloneVar(rec) : null;
|
return rec ? cloneVarRecord(rec) : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(name: string, schema?: Hash): Variable[] {
|
remove(name: string, schema?: Hash): Variable[] {
|
||||||
if (schema !== undefined) {
|
if (schema !== undefined) {
|
||||||
const k = key(name, schema);
|
const k = varKey(name, schema);
|
||||||
const rec = records.get(k);
|
const rec = records.get(k);
|
||||||
if (!rec) return [];
|
if (!rec) return [];
|
||||||
records.delete(k);
|
records.delete(k);
|
||||||
removeIndex(name, k);
|
removeNameIndex(byName, name, k);
|
||||||
return [cloneVar(rec)];
|
return [cloneVarRecord(rec)];
|
||||||
}
|
}
|
||||||
const set = byName.get(name);
|
const set = byName.get(name);
|
||||||
if (!set) return [];
|
if (!set) return [];
|
||||||
@@ -301,7 +227,7 @@ export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
|||||||
for (const k of [...set]) {
|
for (const k of [...set]) {
|
||||||
const rec = records.get(k);
|
const rec = records.get(k);
|
||||||
if (rec) {
|
if (rec) {
|
||||||
removed.push(cloneVar(rec));
|
removed.push(cloneVarRecord(rec));
|
||||||
records.delete(k);
|
records.delete(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,14 +237,14 @@ export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
|||||||
|
|
||||||
update(name: string, hash: Hash, options?: VarSetOptions): Variable {
|
update(name: string, hash: Hash, options?: VarSetOptions): Variable {
|
||||||
validateName(name);
|
validateName(name);
|
||||||
const newSchema = extractSchema(hash);
|
const newSchema = extractSchema(cas, hash);
|
||||||
// Find existing record by name; require existing schema match new schema
|
// Find existing record by name; require existing schema match new schema
|
||||||
const set = byName.get(name);
|
const set = byName.get(name);
|
||||||
if (!set || set.size === 0) {
|
if (!set || set.size === 0) {
|
||||||
throw new VariableNotFoundError(name, newSchema);
|
throw new VariableNotFoundError(name, newSchema);
|
||||||
}
|
}
|
||||||
// find a record matching newSchema
|
// find a record matching newSchema
|
||||||
const k = key(name, newSchema);
|
const k = varKey(name, newSchema);
|
||||||
const existing = records.get(k);
|
const existing = records.get(k);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
// Find any existing — schema mismatch
|
// Find any existing — schema mismatch
|
||||||
@@ -333,7 +259,7 @@ export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const tags = options?.tags ?? existing.tags;
|
const tags = options?.tags ?? existing.tags;
|
||||||
const labels = options?.labels ?? existing.labels;
|
const labels = options?.labels ?? existing.labels;
|
||||||
if (options !== undefined) checkConflict(tags, labels);
|
if (options !== undefined) checkTagLabelConflict(tags, labels);
|
||||||
const changed = pushHistory(existing, hash, now);
|
const changed = pushHistory(existing, hash, now);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
existing.value = hash;
|
existing.value = hash;
|
||||||
@@ -343,7 +269,7 @@ export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
|||||||
existing.tags = { ...tags };
|
existing.tags = { ...tags };
|
||||||
existing.labels = [...labels];
|
existing.labels = [...labels];
|
||||||
}
|
}
|
||||||
return cloneVar(existing);
|
return cloneVarRecord(existing);
|
||||||
},
|
},
|
||||||
|
|
||||||
list(options?: VarListOptions): Variable[] {
|
list(options?: VarListOptions): Variable[] {
|
||||||
@@ -400,12 +326,12 @@ export function createMemoryVarStoreFor(cas: CasStore): VarStore {
|
|||||||
|
|
||||||
if (offset > 0) results = results.slice(offset);
|
if (offset > 0) results = results.slice(offset);
|
||||||
if (limit !== undefined) results = results.slice(0, limit);
|
if (limit !== undefined) results = results.slice(0, limit);
|
||||||
return results.map(cloneVar);
|
return results.map(cloneVarRecord);
|
||||||
},
|
},
|
||||||
|
|
||||||
history(name: string, schema?: Hash): HistoryEntry[] {
|
history(name: string, schema?: Hash): HistoryEntry[] {
|
||||||
if (schema !== undefined) {
|
if (schema !== undefined) {
|
||||||
const rec = records.get(key(name, schema));
|
const rec = records.get(varKey(name, schema));
|
||||||
return rec ? rec.history.map((e) => ({ ...e })) : [];
|
return rec ? rec.history.map((e) => ({ ...e })) : [];
|
||||||
}
|
}
|
||||||
const set = byName.get(name);
|
const set = byName.get(name);
|
||||||
@@ -535,14 +461,14 @@ export function createMemoryTagStoreImpl(): TagStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an in-memory `OcasStore` with three sub-stores: `cas`, `var`, `tag`.
|
* Create an in-memory `Store` with three sub-stores: `cas`, `var`, `tag`.
|
||||||
*
|
*
|
||||||
* The `cas` sub-store also satisfies the legacy `BootstrapCapableStore`
|
* The `cas` sub-store also satisfies the legacy `BootstrapCapableStore`
|
||||||
* contract — it carries a `[BOOTSTRAP_STORE]` callable and a `listAll()`
|
* contract — it carries a `[BOOTSTRAP_STORE]` callable and a `listAll()`
|
||||||
* helper — so existing helpers (`bootstrap`, `gc`, `render`, …) can be
|
* helper — so existing helpers (`bootstrap`, `gc`, `render`, …) can be
|
||||||
* called with `store.cas` until they are migrated to the unified surface.
|
* called with `store.cas` until they are migrated to the unified surface.
|
||||||
*/
|
*/
|
||||||
export function createMemoryStore(): OcasStore & {
|
export function createMemoryStore(): Store & {
|
||||||
cas: MemoryCasStore;
|
cas: MemoryCasStore;
|
||||||
} {
|
} {
|
||||||
const cas = createCasStore();
|
const cas = createCasStore();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
HistoryEntry,
|
HistoryEntry,
|
||||||
ListEntry,
|
ListEntry,
|
||||||
ListOptions,
|
ListOptions,
|
||||||
OcasStore,
|
Store,
|
||||||
Tag,
|
Tag,
|
||||||
TagOp,
|
TagOp,
|
||||||
TagStore,
|
TagStore,
|
||||||
@@ -120,11 +120,11 @@ describe("TagStore type", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Aggregate OcasStore type", () => {
|
describe("Aggregate Store type", () => {
|
||||||
test("has cas, var, tag fields", () => {
|
test("has cas, var, tag fields", () => {
|
||||||
type _AssertCas = OcasStore["cas"] extends CasStore ? true : false;
|
type _AssertCas = Store["cas"] extends CasStore ? true : false;
|
||||||
type _AssertVar = OcasStore["var"] extends VarStore ? true : false;
|
type _AssertVar = Store["var"] extends VarStore ? true : false;
|
||||||
type _AssertTag = OcasStore["tag"] extends TagStore ? true : false;
|
type _AssertTag = Store["tag"] extends TagStore ? true : false;
|
||||||
const a: _AssertCas = true;
|
const a: _AssertCas = true;
|
||||||
const b: _AssertVar = true;
|
const b: _AssertVar = true;
|
||||||
const c: _AssertTag = true;
|
const c: _AssertTag = true;
|
||||||
@@ -147,7 +147,7 @@ describe("Exports surface", () => {
|
|||||||
type _C = CasStore extends object ? true : never;
|
type _C = CasStore extends object ? true : never;
|
||||||
type _V = VarStore extends object ? true : never;
|
type _V = VarStore extends object ? true : never;
|
||||||
type _T = TagStore extends object ? true : never;
|
type _T = TagStore extends object ? true : never;
|
||||||
type _O = OcasStore extends object ? true : never;
|
type _O = Store extends object ? true : never;
|
||||||
const _checks: [_C, _V, _T, _O] = [true, true, true, true];
|
const _checks: [_C, _V, _T, _O] = [true, true, true, true];
|
||||||
expect(_checks.length).toBe(4);
|
expect(_checks.length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,23 +47,7 @@ export type ListEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content-addressable store interface.
|
* Synchronous content-addressable store interface.
|
||||||
* Self-referencing nodes are created only via bootstrap().
|
|
||||||
*/
|
|
||||||
export type Store = {
|
|
||||||
put(typeHash: Hash, payload: unknown): Hash | Promise<Hash>;
|
|
||||||
get(hash: Hash): CasNode | null;
|
|
||||||
has(hash: Hash): boolean;
|
|
||||||
listByType(typeHash: Hash, options?: ListOptions): ListEntry[];
|
|
||||||
listAll(): Hash[];
|
|
||||||
listMeta(options?: ListOptions): ListEntry[];
|
|
||||||
listSchemas(options?: ListOptions): ListEntry[];
|
|
||||||
delete(hash: Hash): void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronous content-addressable store interface (new unified design).
|
|
||||||
* Unlike legacy `Store`, `put` returns the hash synchronously.
|
|
||||||
*/
|
*/
|
||||||
export type CasStore = {
|
export type CasStore = {
|
||||||
get(hash: Hash): CasNode | null;
|
get(hash: Hash): CasNode | null;
|
||||||
@@ -149,9 +133,8 @@ export type TagStore = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregate OCAS store: bundles CAS, variable, and tag stores.
|
* Aggregate OCAS store: bundles CAS, variable, and tag stores.
|
||||||
* Named `OcasStore` to avoid colliding with the legacy `Store` export.
|
|
||||||
*/
|
*/
|
||||||
export type OcasStore = {
|
export type Store = {
|
||||||
cas: CasStore;
|
cas: CasStore;
|
||||||
var: VarStore;
|
var: VarStore;
|
||||||
tag: TagStore;
|
tag: TagStore;
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
CasNodeNotFoundError,
|
||||||
|
MAX_HISTORY,
|
||||||
|
TagLabelConflictError,
|
||||||
|
} from "./errors.js";
|
||||||
|
import type { CasStore, Hash, HistoryEntry } from "./types.js";
|
||||||
|
import type { Variable } from "./variable.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal record shape used by both Memory and Fs VarStore implementations.
|
||||||
|
* Persistence-specific code lives in each factory; pure operations on this
|
||||||
|
* shape live here so they aren't duplicated.
|
||||||
|
*/
|
||||||
|
export type VarRecord = {
|
||||||
|
name: string;
|
||||||
|
schema: Hash;
|
||||||
|
value: Hash;
|
||||||
|
created: number;
|
||||||
|
updated: number;
|
||||||
|
tags: Record<string, string>;
|
||||||
|
labels: string[];
|
||||||
|
history: HistoryEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Build the composite map key from `(name, schema)`. */
|
||||||
|
export function varKey(name: string, schema: Hash): string {
|
||||||
|
return `${name}\u0000${schema}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add `k` to the `byName[name]` index, creating the set if needed. */
|
||||||
|
export function addNameIndex(
|
||||||
|
byName: Map<string, Set<string>>,
|
||||||
|
name: string,
|
||||||
|
k: string,
|
||||||
|
): void {
|
||||||
|
let set = byName.get(name);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
byName.set(name, set);
|
||||||
|
}
|
||||||
|
set.add(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove `k` from the `byName[name]` index, dropping empty sets. */
|
||||||
|
export function removeNameIndex(
|
||||||
|
byName: Map<string, Set<string>>,
|
||||||
|
name: string,
|
||||||
|
k: string,
|
||||||
|
): void {
|
||||||
|
const set = byName.get(name);
|
||||||
|
if (!set) return;
|
||||||
|
set.delete(k);
|
||||||
|
if (set.size === 0) byName.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve `hash → schema` (the type field of the stored CAS node). */
|
||||||
|
export function extractSchema(cas: CasStore, hash: Hash): Hash {
|
||||||
|
const node = cas.get(hash);
|
||||||
|
if (node === null) throw new CasNodeNotFoundError(hash);
|
||||||
|
return node.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reject `(tags, labels)` combinations where the same key appears in both. */
|
||||||
|
export function checkTagLabelConflict(
|
||||||
|
tags: Record<string, string>,
|
||||||
|
labels: string[],
|
||||||
|
): void {
|
||||||
|
for (const tk of Object.keys(tags)) {
|
||||||
|
if (labels.includes(tk)) {
|
||||||
|
throw new TagLabelConflictError(tk, "label", "tag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a new value onto the variable's history. Returns true if the head
|
||||||
|
* changed (i.e. a new value was set), false when the head was already `value`.
|
||||||
|
*/
|
||||||
|
export function pushHistory(rec: VarRecord, value: Hash, now: number): boolean {
|
||||||
|
if (rec.history.length > 0 && rec.history[0]?.value === value) return false;
|
||||||
|
const existingIdx = rec.history.findIndex((e) => e.value === value);
|
||||||
|
if (existingIdx > 0) rec.history.splice(existingIdx, 1);
|
||||||
|
rec.history.unshift({ value, position: 0, setAt: now });
|
||||||
|
if (rec.history.length > MAX_HISTORY) rec.history.length = MAX_HISTORY;
|
||||||
|
for (let i = 0; i < rec.history.length; i++) {
|
||||||
|
const entry = rec.history[i];
|
||||||
|
if (entry !== undefined) entry.position = i;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert a stored `VarRecord` to the public `Variable` shape (deep copy). */
|
||||||
|
export function cloneVarRecord(rec: VarRecord): Variable {
|
||||||
|
return {
|
||||||
|
name: rec.name,
|
||||||
|
schema: rec.schema,
|
||||||
|
value: rec.value,
|
||||||
|
created: rec.created,
|
||||||
|
updated: rec.updated,
|
||||||
|
tags: { ...rec.tags },
|
||||||
|
labels: [...rec.labels],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { wrapEnvelope } from "./wrap-envelope.js";
|
|||||||
describe("wrapEnvelope", () => {
|
describe("wrapEnvelope", () => {
|
||||||
test("resolves @ocas/output/put alias and returns envelope", async () => {
|
test("resolves @ocas/output/put alias and returns envelope", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
|
|
||||||
const envelope = await wrapEnvelope(
|
const envelope = await wrapEnvelope(
|
||||||
store,
|
store,
|
||||||
@@ -20,7 +20,7 @@ describe("wrapEnvelope", () => {
|
|||||||
|
|
||||||
test("resolves @ocas/output/has alias with boolean value", async () => {
|
test("resolves @ocas/output/has alias with boolean value", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
|
|
||||||
const envelope = await wrapEnvelope(store, "@ocas/output/has", true);
|
const envelope = await wrapEnvelope(store, "@ocas/output/has", true);
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ describe("wrapEnvelope", () => {
|
|||||||
|
|
||||||
test("resolves @ocas/output/gc alias with object value", async () => {
|
test("resolves @ocas/output/gc alias with object value", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
|
|
||||||
const gcStats = { total: 100, reachable: 80, collected: 20, scanned: 5 };
|
const gcStats = { total: 100, reachable: 80, collected: 20, scanned: 5 };
|
||||||
const envelope = await wrapEnvelope(store, "@ocas/output/gc", gcStats);
|
const envelope = await wrapEnvelope(store, "@ocas/output/gc", gcStats);
|
||||||
@@ -41,7 +41,7 @@ describe("wrapEnvelope", () => {
|
|||||||
|
|
||||||
test("resolves primitive alias @ocas/string", async () => {
|
test("resolves primitive alias @ocas/string", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
|
|
||||||
const envelope = await wrapEnvelope(store, "@ocas/string", "hello");
|
const envelope = await wrapEnvelope(store, "@ocas/string", "hello");
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ describe("wrapEnvelope", () => {
|
|||||||
|
|
||||||
test("throws for unknown alias", async () => {
|
test("throws for unknown alias", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
wrapEnvelope(store, "@ocas/output/nonexistent", "value"),
|
wrapEnvelope(store, "@ocas/output/nonexistent", "value"),
|
||||||
@@ -75,7 +75,7 @@ describe("wrapEnvelope", () => {
|
|||||||
|
|
||||||
test("preserves complex object values without mutation", async () => {
|
test("preserves complex object values without mutation", async () => {
|
||||||
const store = createMemoryStore();
|
const store = createMemoryStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const original = {
|
const original = {
|
||||||
name: "test",
|
name: "test",
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { bootstrap } from "./bootstrap.js";
|
import { bootstrap } from "./bootstrap.js";
|
||||||
import type { Hash, OcasStore } from "./types.js";
|
import type { Hash, Store } from "./types.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a schema alias (e.g. "@ocas/output/put") to its hash via bootstrap,
|
* Resolve a schema alias (e.g. "@ocas/output/put") to its hash via bootstrap,
|
||||||
* then return a typed envelope ready for store.cas.put() or direct rendering.
|
* then return a typed envelope ready for store.cas.put() or direct rendering.
|
||||||
*/
|
*/
|
||||||
export async function wrapEnvelope(
|
export async function wrapEnvelope(
|
||||||
store: OcasStore,
|
store: Store,
|
||||||
schemaAlias: string,
|
schemaAlias: string,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): Promise<{ type: Hash; value: unknown }> {
|
): Promise<{ type: Hash; value: unknown }> {
|
||||||
const aliases = await bootstrap(store);
|
const aliases = bootstrap(store);
|
||||||
const typeHash = aliases[schemaAlias];
|
const typeHash = aliases[schemaAlias];
|
||||||
if (typeHash === undefined) {
|
if (typeHash === undefined) {
|
||||||
throw new Error(`Unknown schema alias: ${schemaAlias}`);
|
throw new Error(`Unknown schema alias: ${schemaAlias}`);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type { CasNode } from "../src/types.js";
|
|||||||
describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
|
describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
|
||||||
test("1.1: Meta-schema is a valid JSON Schema", async () => {
|
test("1.1: Meta-schema is a valid JSON Schema", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const metaNode = store.get(metaHash);
|
const metaNode = store.get(metaHash);
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
|
|||||||
|
|
||||||
test("1.2: Meta-schema self-validates", async () => {
|
test("1.2: Meta-schema self-validates", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const metaNode = store.get(metaHash);
|
const metaNode = store.get(metaHash);
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
|
|||||||
|
|
||||||
test("1.3: Meta-schema defines all supported keywords", async () => {
|
test("1.3: Meta-schema defines all supported keywords", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const metaSchema = getSchema(store, metaHash);
|
const metaSchema = getSchema(store, metaHash);
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
|
|||||||
|
|
||||||
test("1.4: Meta-schema does not include unsupported keywords", async () => {
|
test("1.4: Meta-schema does not include unsupported keywords", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const metaSchema = getSchema(store, metaHash);
|
const metaSchema = getSchema(store, metaHash);
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
|
|||||||
|
|
||||||
test("1.5: Meta-schema node type equals its own hash", async () => {
|
test("1.5: Meta-schema node type equals its own hash", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const metaNode = store.get(metaHash);
|
const metaNode = store.get(metaHash);
|
||||||
|
|
||||||
@@ -110,22 +110,22 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
|
|||||||
describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
||||||
test("2.1: Accept minimal valid schema (empty object)", async () => {
|
test("2.1: Accept minimal valid schema (empty object)", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {});
|
const hash = putSchema(store, {});
|
||||||
expect(hash).toBeTruthy();
|
expect(hash).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("2.2: Accept schema with type constraint", async () => {
|
test("2.2: Accept schema with type constraint", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, { type: "string" });
|
const hash = putSchema(store, { type: "string" });
|
||||||
expect(hash).toBeTruthy();
|
expect(hash).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("2.3: Accept schema with properties", async () => {
|
test("2.3: Accept schema with properties", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { name: { type: "string" } },
|
properties: { name: { type: "string" } },
|
||||||
});
|
});
|
||||||
@@ -134,8 +134,8 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.4: Accept schema with required fields", async () => {
|
test("2.4: Accept schema with required fields", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["id"],
|
required: ["id"],
|
||||||
properties: { id: { type: "string" } },
|
properties: { id: { type: "string" } },
|
||||||
@@ -145,8 +145,8 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.5: Accept schema with additionalProperties = false", async () => {
|
test("2.5: Accept schema with additionalProperties = false", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
});
|
});
|
||||||
@@ -155,8 +155,8 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.6: Accept schema with additionalProperties = schema", async () => {
|
test("2.6: Accept schema with additionalProperties = schema", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
additionalProperties: { type: "string" },
|
additionalProperties: { type: "string" },
|
||||||
});
|
});
|
||||||
@@ -165,8 +165,8 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.7: Accept schema with anyOf", async () => {
|
test("2.7: Accept schema with anyOf", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
anyOf: [{ type: "string" }, { type: "null" }],
|
anyOf: [{ type: "string" }, { type: "null" }],
|
||||||
});
|
});
|
||||||
expect(hash).toBeTruthy();
|
expect(hash).toBeTruthy();
|
||||||
@@ -174,8 +174,8 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.7b: Accept schema with oneOf", async () => {
|
test("2.7b: Accept schema with oneOf", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
oneOf: [
|
oneOf: [
|
||||||
{ properties: { status: { const: "ready" } }, required: ["status"] },
|
{ properties: { status: { const: "ready" } }, required: ["status"] },
|
||||||
{ properties: { status: { const: "failed" } }, required: ["status"] },
|
{ properties: { status: { const: "failed" } }, required: ["status"] },
|
||||||
@@ -186,8 +186,8 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.8: Accept schema with array items", async () => {
|
test("2.8: Accept schema with array items", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: { type: "number" },
|
items: { type: "number" },
|
||||||
});
|
});
|
||||||
@@ -196,8 +196,8 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.9: Accept schema with format constraint", async () => {
|
test("2.9: Accept schema with format constraint", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "string",
|
type: "string",
|
||||||
format: "ocas_ref",
|
format: "ocas_ref",
|
||||||
});
|
});
|
||||||
@@ -206,8 +206,8 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.10: Accept schema with enum", async () => {
|
test("2.10: Accept schema with enum", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["red", "green", "blue"],
|
enum: ["red", "green", "blue"],
|
||||||
});
|
});
|
||||||
@@ -216,15 +216,15 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.11: Accept schema with const", async () => {
|
test("2.11: Accept schema with const", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, { const: "FIXED_VALUE" });
|
const hash = putSchema(store, { const: "FIXED_VALUE" });
|
||||||
expect(hash).toBeTruthy();
|
expect(hash).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("2.12: Accept schema with title and description", async () => {
|
test("2.12: Accept schema with title and description", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "string",
|
type: "string",
|
||||||
title: "User Name",
|
title: "User Name",
|
||||||
description: "The user's full name",
|
description: "The user's full name",
|
||||||
@@ -234,8 +234,8 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
|
|
||||||
test("2.13: Accept complex nested schema", async () => {
|
test("2.13: Accept complex nested schema", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
const hash = await putSchema(store, {
|
const hash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["type", "payload"],
|
required: ["type", "payload"],
|
||||||
properties: {
|
properties: {
|
||||||
@@ -257,183 +257,169 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
describe("Test Suite 3: putSchema Validation - Invalid Schemas", () => {
|
describe("Test Suite 3: putSchema Validation - Invalid Schemas", () => {
|
||||||
test("3.1: Reject schema with invalid type value", async () => {
|
test("3.1: Reject schema with invalid type value", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(async () => await putSchema(store, { type: "garbage" })).toThrow();
|
expect(async () => putSchema(store, { type: "garbage" })).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.2: Reject schema with type as number", async () => {
|
test("3.2: Reject schema with type as number", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, { type: 123 } as unknown as JSONSchema),
|
||||||
await putSchema(store, { type: 123 } as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.3: Reject schema with properties not an object", async () => {
|
test("3.3: Reject schema with properties not an object", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "object",
|
||||||
type: "object",
|
properties: "not-an-object",
|
||||||
properties: "not-an-object",
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.4: Reject schema with required not an array", async () => {
|
test("3.4: Reject schema with required not an array", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "object",
|
||||||
type: "object",
|
required: "name",
|
||||||
required: "name",
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.5: Reject schema with required containing non-strings", async () => {
|
test("3.5: Reject schema with required containing non-strings", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "object",
|
||||||
type: "object",
|
required: ["name", 123, true],
|
||||||
required: ["name", 123, true],
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.6: Reject schema with additionalProperties as string", async () => {
|
test("3.6: Reject schema with additionalProperties as string", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "object",
|
||||||
type: "object",
|
additionalProperties: "yes",
|
||||||
additionalProperties: "yes",
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.7: Reject schema with anyOf not an array", async () => {
|
test("3.7: Reject schema with anyOf not an array", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
anyOf: { type: "string" },
|
||||||
anyOf: { type: "string" },
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.8: Reject schema with empty anyOf array", async () => {
|
test("3.8: Reject schema with empty anyOf array", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(async () => await putSchema(store, { anyOf: [] })).toThrow();
|
expect(async () => putSchema(store, { anyOf: [] })).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.9: Reject schema with items not an object", async () => {
|
test("3.9: Reject schema with items not an object", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "array",
|
||||||
type: "array",
|
items: "string",
|
||||||
items: "string",
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.10: Reject schema with format not a string", async () => {
|
test("3.10: Reject schema with format not a string", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "string",
|
||||||
type: "string",
|
format: 123,
|
||||||
format: 123,
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.11: Reject schema with enum not an array", async () => {
|
test("3.11: Reject schema with enum not an array", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "string",
|
||||||
type: "string",
|
enum: "red",
|
||||||
enum: "red",
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.12: Reject schema with empty enum array", async () => {
|
test("3.12: Reject schema with empty enum array", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () => await putSchema(store, { type: "string", enum: [] }),
|
putSchema(store, { type: "string", enum: [] }),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.13: Reject schema with title not a string", async () => {
|
test("3.13: Reject schema with title not a string", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "string",
|
||||||
type: "string",
|
title: 123,
|
||||||
title: 123,
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.14: Reject schema with description not a string", async () => {
|
test("3.14: Reject schema with description not a string", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "string",
|
||||||
type: "string",
|
description: ["not a string"],
|
||||||
description: ["not a string"],
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.15: Reject schema with unsupported $ref keyword", async () => {
|
test("3.15: Reject schema with unsupported $ref keyword", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
$ref: "#/definitions/user",
|
||||||
$ref: "#/definitions/user",
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.16: Reject completely invalid data (non-object)", async () => {
|
test("3.16: Reject completely invalid data (non-object)", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, "not-a-schema" as unknown as JSONSchema),
|
||||||
await putSchema(store, "not-a-schema" as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("3.17: Reject nested invalid schema in properties", async () => {
|
test("3.17: Reject nested invalid schema in properties", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, {
|
||||||
await putSchema(store, {
|
type: "object",
|
||||||
type: "object",
|
properties: {
|
||||||
properties: {
|
name: { type: "invalid-type" },
|
||||||
name: { type: "invalid-type" },
|
},
|
||||||
},
|
} as unknown as JSONSchema),
|
||||||
} as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -441,9 +427,9 @@ describe("Test Suite 3: putSchema Validation - Invalid Schemas", () => {
|
|||||||
describe("Test Suite 4: Error Messages and Debugging", () => {
|
describe("Test Suite 4: Error Messages and Debugging", () => {
|
||||||
test("4.1: Error includes schema validation details", async () => {
|
test("4.1: Error includes schema validation details", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
try {
|
try {
|
||||||
await putSchema(store, { type: 123 } as unknown as JSONSchema);
|
putSchema(store, { type: 123 } as unknown as JSONSchema);
|
||||||
expect(true).toBe(false); // Should not reach here
|
expect(true).toBe(false); // Should not reach here
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(SchemaValidationError);
|
expect(error).toBeInstanceOf(SchemaValidationError);
|
||||||
@@ -453,9 +439,9 @@ describe("Test Suite 4: Error Messages and Debugging", () => {
|
|||||||
|
|
||||||
test("4.2: Error distinguishes schema validation from data validation", async () => {
|
test("4.2: Error distinguishes schema validation from data validation", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
try {
|
try {
|
||||||
await putSchema(store, { type: "invalid-type" } as unknown as JSONSchema);
|
putSchema(store, { type: "invalid-type" } as unknown as JSONSchema);
|
||||||
expect(true).toBe(false); // Should not reach here
|
expect(true).toBe(false); // Should not reach here
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(SchemaValidationError);
|
expect(error).toBeInstanceOf(SchemaValidationError);
|
||||||
@@ -468,7 +454,7 @@ describe("Test Suite 5: Backward Compatibility and Migration", () => {
|
|||||||
test("5.1: Bootstrap hash changes (breaking change)", async () => {
|
test("5.1: Bootstrap hash changes (breaking change)", async () => {
|
||||||
// This is a documentation test - the old hash was different
|
// This is a documentation test - the old hash was different
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const newMetaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const newMetaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
|
|
||||||
// The new hash should be different from the old system metadata hash
|
// The new hash should be different from the old system metadata hash
|
||||||
@@ -479,10 +465,10 @@ describe("Test Suite 5: Backward Compatibility and Migration", () => {
|
|||||||
test("5.2: Existing tests compatibility", async () => {
|
test("5.2: Existing tests compatibility", async () => {
|
||||||
// This test ensures our changes don't break existing valid schema usage
|
// This test ensures our changes don't break existing valid schema usage
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
// This is the kind of schema that existed before
|
// This is the kind of schema that existed before
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
@@ -494,9 +480,9 @@ describe("Test Suite 5: Backward Compatibility and Migration", () => {
|
|||||||
|
|
||||||
test("5.3: Data nodes with valid schemas still validate", async () => {
|
test("5.3: Data nodes with valid schemas still validate", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["name"],
|
required: ["name"],
|
||||||
properties: {
|
properties: {
|
||||||
@@ -512,9 +498,9 @@ describe("Test Suite 5: Backward Compatibility and Migration", () => {
|
|||||||
|
|
||||||
test("5.4: Invalid data still fails validation", async () => {
|
test("5.4: Invalid data still fails validation", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["name"],
|
required: ["name"],
|
||||||
properties: {
|
properties: {
|
||||||
@@ -534,10 +520,10 @@ describe("Test Suite 5: Backward Compatibility and Migration", () => {
|
|||||||
describe("Test Suite 6: Integration with Existing Functionality", () => {
|
describe("Test Suite 6: Integration with Existing Functionality", () => {
|
||||||
test("6.1: getSchema works with validated schemas", async () => {
|
test("6.1: getSchema works with validated schemas", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const originalSchema = { type: "string", title: "Test" };
|
const originalSchema = { type: "string", title: "Test" };
|
||||||
const schemaHash = await putSchema(store, originalSchema);
|
const schemaHash = putSchema(store, originalSchema);
|
||||||
const retrieved = getSchema(store, schemaHash);
|
const retrieved = getSchema(store, schemaHash);
|
||||||
|
|
||||||
expect(retrieved).toEqual(originalSchema);
|
expect(retrieved).toEqual(originalSchema);
|
||||||
@@ -545,9 +531,9 @@ describe("Test Suite 6: Integration with Existing Functionality", () => {
|
|||||||
|
|
||||||
test("6.2: validate() works with schemas validated by meta-schema", async () => {
|
test("6.2: validate() works with schemas validated by meta-schema", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const schemaHash = await putSchema(store, { type: "number" });
|
const schemaHash = putSchema(store, { type: "number" });
|
||||||
const validNode = store.get(await store.put(schemaHash, 42));
|
const validNode = store.get(await store.put(schemaHash, 42));
|
||||||
const invalidNode = store.get(await store.put(schemaHash, "not a number"));
|
const invalidNode = store.get(await store.put(schemaHash, "not a number"));
|
||||||
|
|
||||||
@@ -557,9 +543,9 @@ describe("Test Suite 6: Integration with Existing Functionality", () => {
|
|||||||
|
|
||||||
test("6.3: refs() works with validated schemas containing ocas_ref", async () => {
|
test("6.3: refs() works with validated schemas containing ocas_ref", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
ref: { type: "string", format: "ocas_ref" },
|
ref: { type: "string", format: "ocas_ref" },
|
||||||
@@ -575,9 +561,9 @@ describe("Test Suite 6: Integration with Existing Functionality", () => {
|
|||||||
|
|
||||||
test("6.4: walk() works with graphs using validated schemas", async () => {
|
test("6.4: walk() works with graphs using validated schemas", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const schemaHash = await putSchema(store, {
|
const schemaHash = putSchema(store, {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
next: {
|
next: {
|
||||||
@@ -598,11 +584,11 @@ describe("Test Suite 6: Integration with Existing Functionality", () => {
|
|||||||
|
|
||||||
test("6.5: Idempotency preserved for putSchema", async () => {
|
test("6.5: Idempotency preserved for putSchema", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const schema = { type: "string", title: "Test" };
|
const schema = { type: "string", title: "Test" };
|
||||||
const hash1 = await putSchema(store, schema);
|
const hash1 = putSchema(store, schema);
|
||||||
const hash2 = await putSchema(store, schema);
|
const hash2 = putSchema(store, schema);
|
||||||
|
|
||||||
expect(hash1).toBe(hash2);
|
expect(hash1).toBe(hash2);
|
||||||
});
|
});
|
||||||
@@ -611,7 +597,7 @@ describe("Test Suite 6: Integration with Existing Functionality", () => {
|
|||||||
describe("Test Suite 7: Meta-Schema Content Validation", () => {
|
describe("Test Suite 7: Meta-Schema Content Validation", () => {
|
||||||
test("7.1: Meta-schema allows recursive schema definitions", async () => {
|
test("7.1: Meta-schema allows recursive schema definitions", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
const metaHash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
const metaSchema = getSchema(store, metaHash);
|
const metaSchema = getSchema(store, metaHash);
|
||||||
|
|
||||||
@@ -624,11 +610,11 @@ describe("Test Suite 7: Meta-Schema Content Validation", () => {
|
|||||||
|
|
||||||
test("7.2: Meta-schema restricts additionalProperties", async () => {
|
test("7.2: Meta-schema restricts additionalProperties", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
// Schema with unknown keyword should be rejected if meta-schema is strict
|
// Schema with unknown keyword should be rejected if meta-schema is strict
|
||||||
try {
|
try {
|
||||||
await putSchema(store, {
|
putSchema(store, {
|
||||||
type: "string",
|
type: "string",
|
||||||
unknownKeyword: "value",
|
unknownKeyword: "value",
|
||||||
} as unknown as JSONSchema);
|
} as unknown as JSONSchema);
|
||||||
@@ -642,22 +628,21 @@ describe("Test Suite 7: Meta-Schema Content Validation", () => {
|
|||||||
|
|
||||||
test("7.3: Meta-schema validates type as string OR array", async () => {
|
test("7.3: Meta-schema validates type as string OR array", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
// Single string type
|
// Single string type
|
||||||
const hash1 = await putSchema(store, { type: "string" });
|
const hash1 = putSchema(store, { type: "string" });
|
||||||
expect(hash1).toBeTruthy();
|
expect(hash1).toBeTruthy();
|
||||||
|
|
||||||
// Array of types
|
// Array of types
|
||||||
const hash2 = await putSchema(store, {
|
const hash2 = putSchema(store, {
|
||||||
type: ["string", "null"],
|
type: ["string", "null"],
|
||||||
} as unknown as JSONSchema);
|
} as unknown as JSONSchema);
|
||||||
expect(hash2).toBeTruthy();
|
expect(hash2).toBeTruthy();
|
||||||
|
|
||||||
// Invalid type (number)
|
// Invalid type (number)
|
||||||
expect(
|
expect(async () =>
|
||||||
async () =>
|
putSchema(store, { type: 123 } as unknown as JSONSchema),
|
||||||
await putSchema(store, { type: 123 } as unknown as JSONSchema),
|
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -665,7 +650,7 @@ describe("Test Suite 7: Meta-Schema Content Validation", () => {
|
|||||||
describe("Test Suite 8: Performance and Edge Cases", () => {
|
describe("Test Suite 8: Performance and Edge Cases", () => {
|
||||||
test("8.1: Validation performance is acceptable", async () => {
|
test("8.1: Validation performance is acceptable", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const complexSchema = {
|
const complexSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
@@ -686,7 +671,7 @@ describe("Test Suite 8: Performance and Edge Cases", () => {
|
|||||||
|
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
await putSchema(store, complexSchema);
|
putSchema(store, complexSchema);
|
||||||
}
|
}
|
||||||
const duration = performance.now() - start;
|
const duration = performance.now() - start;
|
||||||
|
|
||||||
@@ -696,7 +681,7 @@ describe("Test Suite 8: Performance and Edge Cases", () => {
|
|||||||
|
|
||||||
test("8.2: Large schemas are handled correctly", async () => {
|
test("8.2: Large schemas are handled correctly", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
const largeSchema: Record<string, unknown> = {
|
const largeSchema: Record<string, unknown> = {
|
||||||
type: "object",
|
type: "object",
|
||||||
@@ -709,13 +694,13 @@ describe("Test Suite 8: Performance and Edge Cases", () => {
|
|||||||
props[`prop${i}`] = { type: "string" };
|
props[`prop${i}`] = { type: "string" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = await putSchema(store, largeSchema);
|
const hash = putSchema(store, largeSchema);
|
||||||
expect(hash).toBeTruthy();
|
expect(hash).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("8.3: Deeply nested schemas validate correctly", async () => {
|
test("8.3: Deeply nested schemas validate correctly", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
// Build a 5-level deep schema
|
// Build a 5-level deep schema
|
||||||
let schema: Record<string, unknown> = { type: "string" };
|
let schema: Record<string, unknown> = { type: "string" };
|
||||||
@@ -726,13 +711,13 @@ describe("Test Suite 8: Performance and Edge Cases", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = await putSchema(store, schema);
|
const hash = putSchema(store, schema);
|
||||||
expect(hash).toBeTruthy();
|
expect(hash).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("8.4: Circular-like schemas don't cause infinite loops", async () => {
|
test("8.4: Circular-like schemas don't cause infinite loops", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
bootstrap(store);
|
||||||
|
|
||||||
// Schema where additionalProperties has same structure as parent
|
// Schema where additionalProperties has same structure as parent
|
||||||
const schema = {
|
const schema = {
|
||||||
@@ -748,7 +733,7 @@ describe("Test Suite 8: Performance and Edge Cases", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const hash = await putSchema(store, schema);
|
const hash = putSchema(store, schema);
|
||||||
expect(hash).toBeTruthy();
|
expect(hash).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe("createFsStore – init and bootstrap", () => {
|
|||||||
|
|
||||||
test("bootstrap returns a valid 13-char self-referencing hash", async () => {
|
test("bootstrap returns a valid 13-char self-referencing hash", async () => {
|
||||||
const store = await openStore(dir);
|
const store = await openStore(dir);
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
|
|
||||||
expect(hash).toHaveLength(13);
|
expect(hash).toHaveLength(13);
|
||||||
@@ -63,8 +63,8 @@ describe("createFsStore – init and bootstrap", () => {
|
|||||||
|
|
||||||
test("bootstrap is idempotent across calls", async () => {
|
test("bootstrap is idempotent across calls", async () => {
|
||||||
const store = await openStore(dir);
|
const store = await openStore(dir);
|
||||||
const h1 = await bootstrap(store);
|
const h1 = bootstrap(store);
|
||||||
const h2 = await bootstrap(store);
|
const h2 = bootstrap(store);
|
||||||
|
|
||||||
expect(h1).toEqual(h2);
|
expect(h1).toEqual(h2);
|
||||||
expect(store.cas.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(29);
|
expect(store.cas.listByType(h1["@ocas/schema"] ?? "")).toHaveLength(29);
|
||||||
@@ -113,7 +113,7 @@ describe("createFsStore – persistence round-trip", () => {
|
|||||||
|
|
||||||
test("bootstrap survives round-trip: self-referencing node reloads correctly", async () => {
|
test("bootstrap survives round-trip: self-referencing node reloads correctly", async () => {
|
||||||
const store1 = await openStore(dir);
|
const store1 = await openStore(dir);
|
||||||
const builtinSchemas = await bootstrap(store1);
|
const builtinSchemas = bootstrap(store1);
|
||||||
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
|
|
||||||
const store2 = createFsStore(dir);
|
const store2 = createFsStore(dir);
|
||||||
@@ -261,7 +261,7 @@ describe("createFsStore – listByType", () => {
|
|||||||
|
|
||||||
test("bootstrap node is listed under its self type after reload", async () => {
|
test("bootstrap node is listed under its self type after reload", async () => {
|
||||||
const store1 = await openStore(dir);
|
const store1 = await openStore(dir);
|
||||||
const builtinSchemas = await bootstrap(store1);
|
const builtinSchemas = bootstrap(store1);
|
||||||
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
|
|
||||||
const store2 = createFsStore(dir);
|
const store2 = createFsStore(dir);
|
||||||
@@ -295,7 +295,7 @@ describe("createFsStore – verify on disk-loaded nodes", () => {
|
|||||||
|
|
||||||
test("verify passes on a disk-loaded bootstrap node", async () => {
|
test("verify passes on a disk-loaded bootstrap node", async () => {
|
||||||
const store1 = await openStore(dir);
|
const store1 = await openStore(dir);
|
||||||
const builtinSchemas = await bootstrap(store1);
|
const builtinSchemas = bootstrap(store1);
|
||||||
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
const hash = builtinSchemas["@ocas/schema"] ?? "";
|
||||||
|
|
||||||
const store2 = createFsStore(dir);
|
const store2 = createFsStore(dir);
|
||||||
@@ -375,7 +375,7 @@ describe("openStore – async with auto-bootstrap", () => {
|
|||||||
const store = await openStore(dir);
|
const store = await openStore(dir);
|
||||||
|
|
||||||
// Check that bootstrap schemas exist
|
// Check that bootstrap schemas exist
|
||||||
const builtinSchemas = await bootstrap(store);
|
const builtinSchemas = bootstrap(store);
|
||||||
const metaHash = builtinSchemas["@ocas/schema"];
|
const metaHash = builtinSchemas["@ocas/schema"];
|
||||||
|
|
||||||
expect(metaHash).toBeDefined();
|
expect(metaHash).toBeDefined();
|
||||||
@@ -392,11 +392,11 @@ describe("openStore – async with auto-bootstrap", () => {
|
|||||||
|
|
||||||
test("openStore bootstrap is idempotent on subsequent opens", async () => {
|
test("openStore bootstrap is idempotent on subsequent opens", async () => {
|
||||||
const store1 = await openStore(dir);
|
const store1 = await openStore(dir);
|
||||||
const schemas1 = await bootstrap(store1);
|
const schemas1 = bootstrap(store1);
|
||||||
const count1 = store1.cas.listAll().length;
|
const count1 = store1.cas.listAll().length;
|
||||||
|
|
||||||
const store2 = await openStore(dir);
|
const store2 = await openStore(dir);
|
||||||
const schemas2 = await bootstrap(store2);
|
const schemas2 = bootstrap(store2);
|
||||||
const count2 = store2.cas.listAll().length;
|
const count2 = store2.cas.listAll().length;
|
||||||
|
|
||||||
// Same schemas, same count
|
// Same schemas, same count
|
||||||
@@ -407,11 +407,11 @@ describe("openStore – async with auto-bootstrap", () => {
|
|||||||
test("openStore works on already-bootstrapped store", async () => {
|
test("openStore works on already-bootstrapped store", async () => {
|
||||||
// Open + bootstrap
|
// Open + bootstrap
|
||||||
const store1 = await openStore(dir);
|
const store1 = await openStore(dir);
|
||||||
const schemas1 = await bootstrap(store1);
|
const schemas1 = bootstrap(store1);
|
||||||
|
|
||||||
// Open again
|
// Open again
|
||||||
const store2 = await openStore(dir);
|
const store2 = await openStore(dir);
|
||||||
const schemas2 = await bootstrap(store2);
|
const schemas2 = bootstrap(store2);
|
||||||
|
|
||||||
expect(schemas1).toEqual(schemas2);
|
expect(schemas1).toEqual(schemas2);
|
||||||
});
|
});
|
||||||
@@ -424,7 +424,7 @@ describe("openStore – async with auto-bootstrap", () => {
|
|||||||
|
|
||||||
// Open with openStore - should auto-bootstrap
|
// Open with openStore - should auto-bootstrap
|
||||||
const store2 = await openStore(dir);
|
const store2 = await openStore(dir);
|
||||||
const schemas = await bootstrap(store2);
|
const schemas = bootstrap(store2);
|
||||||
|
|
||||||
expect(store2.cas.has(schemas["@ocas/schema"] as string)).toBe(true);
|
expect(store2.cas.has(schemas["@ocas/schema"] as string)).toBe(true);
|
||||||
// Old data still exists
|
// Old data still exists
|
||||||
@@ -560,9 +560,9 @@ describe("createFsStore – listMeta and listSchemas", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
// E2. OcasStore shape from openStore
|
// E2. Store shape from openStore
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
describe("openStore – OcasStore shape", () => {
|
describe("openStore – Store shape", () => {
|
||||||
let dir: string;
|
let dir: string;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dir = makeTmpDir();
|
dir = makeTmpDir();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
initHasher,
|
initHasher,
|
||||||
type ListEntry,
|
type ListEntry,
|
||||||
type ListOptions,
|
type ListOptions,
|
||||||
type OcasStore,
|
type Store,
|
||||||
} from "@ocas/core";
|
} from "@ocas/core";
|
||||||
import { decode } from "cborg";
|
import { decode } from "cborg";
|
||||||
import { createFsTagStore, createFsVarStoreFor } from "./var-store.js";
|
import { createFsTagStore, createFsVarStoreFor } from "./var-store.js";
|
||||||
@@ -194,7 +194,7 @@ function hashesToEntries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CAS sub-store of an FS-backed `OcasStore` — also satisfies the legacy
|
* The CAS sub-store of an FS-backed `Store` — also satisfies the legacy
|
||||||
* `BootstrapCapableStore` interface so `bootstrap()` can run against it.
|
* `BootstrapCapableStore` interface so `bootstrap()` can run against it.
|
||||||
*/
|
*/
|
||||||
export type FsCasStore = BootstrapCapableStore & {
|
export type FsCasStore = BootstrapCapableStore & {
|
||||||
@@ -383,21 +383,21 @@ export async function prepareStore(dir: string): Promise<FsCasStore> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a filesystem-backed `OcasStore` with automatic directory creation and
|
* Open a filesystem-backed `Store` with automatic directory creation and
|
||||||
* bootstrap. The CAS sub-store is FS-backed; the variable and tag sub-stores
|
* bootstrap. The CAS sub-store is FS-backed; the variable and tag sub-stores
|
||||||
* are in-memory (provided by `@ocas/core`).
|
* are in-memory (provided by `@ocas/core`).
|
||||||
*
|
*
|
||||||
* @param dir - The directory path for the CAS store
|
* @param dir - The directory path for the CAS store
|
||||||
* @returns A Promise resolving to the OcasStore
|
* @returns A Promise resolving to the Store
|
||||||
* @throws Error if the path exists but is not a directory
|
* @throws Error if the path exists but is not a directory
|
||||||
*/
|
*/
|
||||||
export async function openStore(dir: string): Promise<OcasStore> {
|
export async function openStore(dir: string): Promise<Store> {
|
||||||
const cas = await prepareStore(dir);
|
const cas = await prepareStore(dir);
|
||||||
const ocas: OcasStore = {
|
const ocas: Store = {
|
||||||
cas,
|
cas,
|
||||||
var: createFsVarStoreFor(dir, cas),
|
var: createFsVarStoreFor(dir, cas),
|
||||||
tag: createFsTagStore(dir),
|
tag: createFsTagStore(dir),
|
||||||
};
|
};
|
||||||
await bootstrap(ocas);
|
bootstrap(ocas);
|
||||||
return ocas;
|
return ocas;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|||||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { Hash, OcasStore } from "@ocas/core";
|
import type { Hash, Store } from "@ocas/core";
|
||||||
import {
|
import {
|
||||||
CasNodeNotFoundError,
|
CasNodeNotFoundError,
|
||||||
InvalidVariableNameError,
|
InvalidVariableNameError,
|
||||||
@@ -16,7 +16,7 @@ import { openStore } from "./store.js";
|
|||||||
const META_TYPE_KEY = Symbol.for("@ocas/core/bootstrap-store");
|
const META_TYPE_KEY = Symbol.for("@ocas/core/bootstrap-store");
|
||||||
|
|
||||||
async function setupStore(dir: string): Promise<{
|
async function setupStore(dir: string): Promise<{
|
||||||
store: OcasStore;
|
store: Store;
|
||||||
schema: Hash;
|
schema: Hash;
|
||||||
put: (payload: unknown) => Hash;
|
put: (payload: unknown) => Hash;
|
||||||
}> {
|
}> {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { join } from "node:path";
|
|||||||
import type {
|
import type {
|
||||||
CasStore,
|
CasStore,
|
||||||
Hash,
|
Hash,
|
||||||
HistoryEntry,
|
|
||||||
ListEntry,
|
ListEntry,
|
||||||
Tag,
|
Tag,
|
||||||
TagStore,
|
TagStore,
|
||||||
@@ -17,65 +16,29 @@ import type {
|
|||||||
VarStore,
|
VarStore,
|
||||||
} from "@ocas/core";
|
} from "@ocas/core";
|
||||||
import {
|
import {
|
||||||
|
addNameIndex,
|
||||||
applyListOptions,
|
applyListOptions,
|
||||||
CasNodeNotFoundError,
|
|
||||||
casListEntry,
|
casListEntry,
|
||||||
MAX_HISTORY,
|
checkTagLabelConflict,
|
||||||
|
cloneVarRecord,
|
||||||
|
extractSchema,
|
||||||
|
pushHistory,
|
||||||
|
removeNameIndex,
|
||||||
SchemaMismatchError,
|
SchemaMismatchError,
|
||||||
TagLabelConflictError,
|
|
||||||
VariableNotFoundError,
|
VariableNotFoundError,
|
||||||
|
type VarRecord,
|
||||||
validateName,
|
validateName,
|
||||||
|
varKey,
|
||||||
} from "@ocas/core";
|
} from "@ocas/core";
|
||||||
|
|
||||||
const VARS_FILE = "_vars.jsonl";
|
const VARS_FILE = "_vars.jsonl";
|
||||||
const TAGS_FILE = "_tags.jsonl";
|
const TAGS_FILE = "_tags.jsonl";
|
||||||
|
|
||||||
type VarRecord = {
|
|
||||||
name: string;
|
|
||||||
schema: Hash;
|
|
||||||
value: Hash;
|
|
||||||
created: number;
|
|
||||||
updated: number;
|
|
||||||
tags: Record<string, string>;
|
|
||||||
labels: string[];
|
|
||||||
history: HistoryEntry[];
|
|
||||||
};
|
|
||||||
|
|
||||||
function cloneVar(rec: VarRecord): Variable {
|
|
||||||
return {
|
|
||||||
name: rec.name,
|
|
||||||
schema: rec.schema,
|
|
||||||
value: rec.value,
|
|
||||||
created: rec.created,
|
|
||||||
updated: rec.updated,
|
|
||||||
tags: { ...rec.tags },
|
|
||||||
labels: [...rec.labels],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
||||||
const records = new Map<string, VarRecord>();
|
const records = new Map<string, VarRecord>();
|
||||||
const byName = new Map<string, Set<string>>();
|
const byName = new Map<string, Set<string>>();
|
||||||
const path = join(dir, VARS_FILE);
|
const path = join(dir, VARS_FILE);
|
||||||
|
|
||||||
function key(name: string, schema: Hash): string {
|
|
||||||
return `${name}\u0000${schema}`;
|
|
||||||
}
|
|
||||||
function addIndex(name: string, k: string): void {
|
|
||||||
let set = byName.get(name);
|
|
||||||
if (!set) {
|
|
||||||
set = new Set();
|
|
||||||
byName.set(name, set);
|
|
||||||
}
|
|
||||||
set.add(k);
|
|
||||||
}
|
|
||||||
function removeIndex(name: string, k: string): void {
|
|
||||||
const set = byName.get(name);
|
|
||||||
if (!set) return;
|
|
||||||
set.delete(k);
|
|
||||||
if (set.size === 0) byName.delete(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load existing records (last record per key wins)
|
// Load existing records (last record per key wins)
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(path, "utf8");
|
const content = readFileSync(path, "utf8");
|
||||||
@@ -84,13 +47,13 @@ export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
|||||||
try {
|
try {
|
||||||
const rec = JSON.parse(line) as VarRecord & { __op?: string };
|
const rec = JSON.parse(line) as VarRecord & { __op?: string };
|
||||||
if (rec.__op === "remove") {
|
if (rec.__op === "remove") {
|
||||||
const k = key(rec.name, rec.schema);
|
const k = varKey(rec.name, rec.schema);
|
||||||
records.delete(k);
|
records.delete(k);
|
||||||
removeIndex(rec.name, k);
|
removeNameIndex(byName, rec.name, k);
|
||||||
} else {
|
} else {
|
||||||
const k = key(rec.name, rec.schema);
|
const k = varKey(rec.name, rec.schema);
|
||||||
records.set(k, rec);
|
records.set(k, rec);
|
||||||
addIndex(rec.name, k);
|
addNameIndex(byName, rec.name, k);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// skip malformed
|
// skip malformed
|
||||||
@@ -123,43 +86,17 @@ export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractSchema(hash: Hash): Hash {
|
|
||||||
const node = cas.get(hash);
|
|
||||||
if (node === null) throw new CasNodeNotFoundError(hash);
|
|
||||||
return node.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkConflict(tags: Record<string, string>, labels: string[]): void {
|
|
||||||
for (const tk of Object.keys(tags)) {
|
|
||||||
if (labels.includes(tk))
|
|
||||||
throw new TagLabelConflictError(tk, "label", "tag");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushHistory(rec: VarRecord, value: Hash, now: number): boolean {
|
|
||||||
if (rec.history.length > 0 && rec.history[0]?.value === value) return false;
|
|
||||||
const existingIdx = rec.history.findIndex((e) => e.value === value);
|
|
||||||
if (existingIdx > 0) rec.history.splice(existingIdx, 1);
|
|
||||||
rec.history.unshift({ value, position: 0, setAt: now });
|
|
||||||
if (rec.history.length > MAX_HISTORY) rec.history.length = MAX_HISTORY;
|
|
||||||
for (let i = 0; i < rec.history.length; i++) {
|
|
||||||
const entry = rec.history[i];
|
|
||||||
if (entry !== undefined) entry.position = i;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
set(name, hash, options) {
|
set(name, hash, options) {
|
||||||
validateName(name);
|
validateName(name);
|
||||||
const schema = extractSchema(hash);
|
const schema = extractSchema(cas, hash);
|
||||||
const k = key(name, schema);
|
const k = varKey(name, schema);
|
||||||
const existing = records.get(k);
|
const existing = records.get(k);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const tags = options?.tags ?? existing.tags;
|
const tags = options?.tags ?? existing.tags;
|
||||||
const labels = options?.labels ?? existing.labels;
|
const labels = options?.labels ?? existing.labels;
|
||||||
if (options !== undefined) checkConflict(tags, labels);
|
if (options !== undefined) checkTagLabelConflict(tags, labels);
|
||||||
const changed = pushHistory(existing, hash, now);
|
const changed = pushHistory(existing, hash, now);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
existing.value = hash;
|
existing.value = hash;
|
||||||
@@ -170,11 +107,11 @@ export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
|||||||
existing.labels = [...labels];
|
existing.labels = [...labels];
|
||||||
}
|
}
|
||||||
persistFull();
|
persistFull();
|
||||||
return cloneVar(existing);
|
return cloneVarRecord(existing);
|
||||||
}
|
}
|
||||||
const tags = options?.tags ?? {};
|
const tags = options?.tags ?? {};
|
||||||
const labels = options?.labels ?? [];
|
const labels = options?.labels ?? [];
|
||||||
checkConflict(tags, labels);
|
checkTagLabelConflict(tags, labels);
|
||||||
const rec: VarRecord = {
|
const rec: VarRecord = {
|
||||||
name,
|
name,
|
||||||
schema,
|
schema,
|
||||||
@@ -186,33 +123,33 @@ export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
|||||||
history: [{ value: hash, position: 0, setAt: now }],
|
history: [{ value: hash, position: 0, setAt: now }],
|
||||||
};
|
};
|
||||||
records.set(k, rec);
|
records.set(k, rec);
|
||||||
addIndex(name, k);
|
addNameIndex(byName, name, k);
|
||||||
appendRecord(rec);
|
appendRecord(rec);
|
||||||
return cloneVar(rec);
|
return cloneVarRecord(rec);
|
||||||
},
|
},
|
||||||
|
|
||||||
get(name, schema) {
|
get(name, schema) {
|
||||||
if (schema !== undefined) {
|
if (schema !== undefined) {
|
||||||
const rec = records.get(key(name, schema));
|
const rec = records.get(varKey(name, schema));
|
||||||
return rec ? cloneVar(rec) : null;
|
return rec ? cloneVarRecord(rec) : null;
|
||||||
}
|
}
|
||||||
const set = byName.get(name);
|
const set = byName.get(name);
|
||||||
if (!set || set.size !== 1) return null;
|
if (!set || set.size !== 1) return null;
|
||||||
const onlyKey = set.values().next().value;
|
const onlyKey = set.values().next().value;
|
||||||
if (onlyKey === undefined) return null;
|
if (onlyKey === undefined) return null;
|
||||||
const rec = records.get(onlyKey);
|
const rec = records.get(onlyKey);
|
||||||
return rec ? cloneVar(rec) : null;
|
return rec ? cloneVarRecord(rec) : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(name, schema) {
|
remove(name, schema) {
|
||||||
if (schema !== undefined) {
|
if (schema !== undefined) {
|
||||||
const k = key(name, schema);
|
const k = varKey(name, schema);
|
||||||
const rec = records.get(k);
|
const rec = records.get(k);
|
||||||
if (!rec) return [];
|
if (!rec) return [];
|
||||||
records.delete(k);
|
records.delete(k);
|
||||||
removeIndex(name, k);
|
removeNameIndex(byName, name, k);
|
||||||
appendRemoval(name, schema);
|
appendRemoval(name, schema);
|
||||||
return [cloneVar(rec)];
|
return [cloneVarRecord(rec)];
|
||||||
}
|
}
|
||||||
const set = byName.get(name);
|
const set = byName.get(name);
|
||||||
if (!set) return [];
|
if (!set) return [];
|
||||||
@@ -220,7 +157,7 @@ export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
|||||||
for (const k of [...set]) {
|
for (const k of [...set]) {
|
||||||
const rec = records.get(k);
|
const rec = records.get(k);
|
||||||
if (rec) {
|
if (rec) {
|
||||||
removed.push(cloneVar(rec));
|
removed.push(cloneVarRecord(rec));
|
||||||
records.delete(k);
|
records.delete(k);
|
||||||
appendRemoval(rec.name, rec.schema);
|
appendRemoval(rec.name, rec.schema);
|
||||||
}
|
}
|
||||||
@@ -231,11 +168,11 @@ export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
|||||||
|
|
||||||
update(name, hash, options) {
|
update(name, hash, options) {
|
||||||
validateName(name);
|
validateName(name);
|
||||||
const newSchema = extractSchema(hash);
|
const newSchema = extractSchema(cas, hash);
|
||||||
const set = byName.get(name);
|
const set = byName.get(name);
|
||||||
if (!set || set.size === 0)
|
if (!set || set.size === 0)
|
||||||
throw new VariableNotFoundError(name, newSchema);
|
throw new VariableNotFoundError(name, newSchema);
|
||||||
const k = key(name, newSchema);
|
const k = varKey(name, newSchema);
|
||||||
const existing = records.get(k);
|
const existing = records.get(k);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
for (const ek of set) {
|
for (const ek of set) {
|
||||||
@@ -247,7 +184,7 @@ export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const tags = options?.tags ?? existing.tags;
|
const tags = options?.tags ?? existing.tags;
|
||||||
const labels = options?.labels ?? existing.labels;
|
const labels = options?.labels ?? existing.labels;
|
||||||
if (options !== undefined) checkConflict(tags, labels);
|
if (options !== undefined) checkTagLabelConflict(tags, labels);
|
||||||
const changed = pushHistory(existing, hash, now);
|
const changed = pushHistory(existing, hash, now);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
existing.value = hash;
|
existing.value = hash;
|
||||||
@@ -258,7 +195,7 @@ export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
|||||||
existing.labels = [...labels];
|
existing.labels = [...labels];
|
||||||
}
|
}
|
||||||
persistFull();
|
persistFull();
|
||||||
return cloneVar(existing);
|
return cloneVarRecord(existing);
|
||||||
},
|
},
|
||||||
|
|
||||||
list(options?: VarListOptions) {
|
list(options?: VarListOptions) {
|
||||||
@@ -312,12 +249,12 @@ export function createFsVarStoreFor(dir: string, cas: CasStore): VarStore {
|
|||||||
});
|
});
|
||||||
if (offset > 0) results = results.slice(offset);
|
if (offset > 0) results = results.slice(offset);
|
||||||
if (limit !== undefined) results = results.slice(0, limit);
|
if (limit !== undefined) results = results.slice(0, limit);
|
||||||
return results.map(cloneVar);
|
return results.map(cloneVarRecord);
|
||||||
},
|
},
|
||||||
|
|
||||||
history(name, schema) {
|
history(name, schema) {
|
||||||
if (schema !== undefined) {
|
if (schema !== undefined) {
|
||||||
const rec = records.get(key(name, schema));
|
const rec = records.get(varKey(name, schema));
|
||||||
return rec ? rec.history.map((e) => ({ ...e })) : [];
|
return rec ? rec.history.map((e) => ({ ...e })) : [];
|
||||||
}
|
}
|
||||||
const set = byName.get(name);
|
const set = byName.get(name);
|
||||||
|
|||||||
Reference in New Issue
Block a user