From a283649c77634316c607b44f758cf0cba4cb3c5b Mon Sep 17 00:00:00 2001 From: Ladd Date: Mon, 23 Dec 2024 23:30:54 -0600 Subject: [PATCH] added lossy view --- __tests__/lossy.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++++ src/lossy.ts | 41 ++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 __tests__/lossy.ts create mode 100644 src/lossy.ts diff --git a/__tests__/lossy.ts b/__tests__/lossy.ts new file mode 100644 index 0000000..0e3959b --- /dev/null +++ b/__tests__/lossy.ts @@ -0,0 +1,80 @@ +import Debug from "debug"; +import {Lossless, LosslessViewMany} from "../src/lossless"; +import {Lossy, firstValueFromLosslessViewOne, firstValueFromCollapsedDelta} from "../src/lossy"; +const debug = Debug('test:lossy'); + +describe('Lossy', () => { + describe('se a provided function to resolve entity views', () => { + const lossless = new Lossless(); + const lossy = new Lossy(lossless); + + beforeAll(() => { + lossless.ingestDelta({ + creator: 'a', + host: 'h', + pointers: [{ + localContext: "actor", + target: "keanu", + targetContext: "roles" + }, { + localContext: "role", + target: "neo", + targetContext: "actor" + }, { + localContext: "film", + target: "the_matrix", + targetContext: "cast" + }, { + localContext: "base_salary", + target: 1000000 + }, { + localContext: "salary_currency", + target: "usd" + }] + }); + }); + + it('example summary', () => { + type Role = { + actor: string, + film: string, + role: string + }; + + type Summary = { + roles: Role[]; + }; + + const resolver = (losslessView: LosslessViewMany): Summary => { + const roles: Role[] = []; + debug('resolving roles'); + for (const [id, ent] of Object.entries(losslessView)) { + if (ent.referencedAs.includes("role")) { + const {delta, value: actor} = firstValueFromLosslessViewOne(ent, "actor") ?? {}; + if (!delta) continue; // TODO: panic + if (!actor) continue; // TODO: panic + const film = firstValueFromCollapsedDelta(delta, "film"); + debug(`role ${id}`, {actor, film}); + if (!film) continue; // TODO: panic + roles.push({ + role: id, + actor, + film + }); + } + } + return {roles}; + } + + const result = lossy.resolve(resolver); + expect(result).toEqual({ + roles: [{ + film: "the_matrix", + role: "neo", + actor: "keanu" + }] + }); + }); + }); + +}); diff --git a/src/lossy.ts b/src/lossy.ts new file mode 100644 index 0000000..83f1b66 --- /dev/null +++ b/src/lossy.ts @@ -0,0 +1,41 @@ +// We have the lossless transformation of the delta stream. +// We want to enable transformations from the lossless view, +// into various possible "lossy" views that combine or exclude some information. +// +// We can achieve this via functional expression, encoded as JSON-Logic. +// Fields in the output can be described as transformations + +import {CollapsedDelta, Lossless, LosslessViewMany, LosslessViewOne} from "./lossless"; +import {DeltaFilter} from "./types"; + +type Resolver = (losslessView: LosslessViewMany) => unknown; + +export function firstValueFromCollapsedDelta(delta: CollapsedDelta, key: string): string | undefined { + const pointers = delta.pointers; + for (const pointer of pointers || []) { + const [[k, value]] = Object.entries(pointer); + if (k === key && typeof value === "string") { + return value; + } + } +} + +export function firstValueFromLosslessViewOne(ent: LosslessViewOne, key: string): {delta: CollapsedDelta, value: string} | undefined { + for (const delta of ent.properties[key] || []) { + const value = firstValueFromCollapsedDelta(delta, key); + if (value) return {delta, value}; + } +} + +export class Lossy { + lossless: Lossless; + + constructor(lossless: Lossless) { + this.lossless = lossless; + } + + resolve(fn: Resolver, deltaFilter?: DeltaFilter) { + return fn(this.lossless.view(deltaFilter)); + } +} +