From e4f434eeb3f697d5286afe73cfa81176af45f8df Mon Sep 17 00:00:00 2001 From: Jackson Morgan Date: Wed, 30 Apr 2025 13:36:27 -0400 Subject: [PATCH] Read theoretically works --- packages/connected/src/ConnectedLdoBuilder.ts | 10 +- .../src/ConnectedLdoTransactionDataset.ts | 12 +- packages/connected/src/ResourceLinkQuery.ts | 43 ------ packages/connected/src/index.ts | 4 + .../src/linkTraversal/ResourceLinkQuery.ts | 75 ++++++++++ .../src/linkTraversal/exploreLinks.ts | 135 ++++++++++++++++++ .../notifications/NotificationSubscription.ts | 2 +- .../connected/src/types/ConnectedContext.ts | 6 +- packages/connected/src/types/ILinkQuery.ts | 69 ++++++--- 9 files changed, 285 insertions(+), 71 deletions(-) delete mode 100644 packages/connected/src/ResourceLinkQuery.ts create mode 100644 packages/connected/src/linkTraversal/ResourceLinkQuery.ts create mode 100644 packages/connected/src/linkTraversal/exploreLinks.ts diff --git a/packages/connected/src/ConnectedLdoBuilder.ts b/packages/connected/src/ConnectedLdoBuilder.ts index 8ce4d7b..04ab67f 100644 --- a/packages/connected/src/ConnectedLdoBuilder.ts +++ b/packages/connected/src/ConnectedLdoBuilder.ts @@ -4,9 +4,9 @@ import type { IConnectedLdoBuilder } from "./types/IConnectedLdoBuilder"; import type { JsonldDatasetProxyBuilder } from "@ldo/jsonld-dataset-proxy"; import type { SubjectNode } from "@ldo/rdf-utils"; import type { LQInput, ILinkQuery } from "./types/ILinkQuery"; -import { ResourceLinkQuery } from "./ResourceLinkQuery"; -import type { ConnectedLdoDataset } from "./ConnectedLdoDataset"; +import { ResourceLinkQuery } from "./linkTraversal/ResourceLinkQuery"; import type { ConnectedPlugin } from "./types/ConnectedPlugin"; +import type { IConnectedLdoDataset } from "./types/IConnectedLdoDataset"; export class ConnectedLdoBuilder< Type extends LdoBase, @@ -15,10 +15,10 @@ export class ConnectedLdoBuilder< extends LdoBuilder implements IConnectedLdoBuilder { - protected parentDataset: ConnectedLdoDataset; + protected parentDataset: IConnectedLdoDataset; constructor( - parentDataset: ConnectedLdoDataset, + parentDataset: IConnectedLdoDataset, jsonldDatasetProxyBuilder: JsonldDatasetProxyBuilder, shapeType: ShapeType, ) { @@ -34,7 +34,7 @@ export class ConnectedLdoBuilder< return new ResourceLinkQuery( this.parentDataset, this.shapeType, - this.jsonldDatasetProxyBuilder, + this, startingResource, startingSubject, linkQueryInput, diff --git a/packages/connected/src/ConnectedLdoTransactionDataset.ts b/packages/connected/src/ConnectedLdoTransactionDataset.ts index 74dc142..2c3acf1 100644 --- a/packages/connected/src/ConnectedLdoTransactionDataset.ts +++ b/packages/connected/src/ConnectedLdoTransactionDataset.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { LdoBase, ShapeType } from "@ldo/ldo"; import { LdoTransactionDataset } from "@ldo/ldo"; import type { DatasetFactory, Quad } from "@rdfjs/types"; import { @@ -7,7 +8,7 @@ import { } from "@ldo/subscribable-dataset"; import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils"; import type { ConnectedPlugin } from "./types/ConnectedPlugin"; -import type { ConnectedContext } from "./ConnectedContext"; +import type { ConnectedContext } from "./types/ConnectedContext"; import type { GetResourceReturnType, IConnectedLdoDataset, @@ -21,6 +22,8 @@ import type { AggregateSuccess, SuccessResult, } from "./results/success/SuccessResult"; +import { ConnectedLdoBuilder } from "./ConnectedLdoBuilder"; +import jsonldDatasetProxy from "@ldo/jsonld-dataset-proxy"; /** * A ConnectedLdoTransactionDataset has all the functionality of a @@ -225,4 +228,11 @@ export class ConnectedLdoTransactionDataset results: results.map((result) => result[2]) as any, }; } + + public usingType( + shapeType: ShapeType, + ): ConnectedLdoBuilder { + const proxyBuilder = jsonldDatasetProxy(this, shapeType.context); + return new ConnectedLdoBuilder(this, proxyBuilder, shapeType); + } } diff --git a/packages/connected/src/ResourceLinkQuery.ts b/packages/connected/src/ResourceLinkQuery.ts deleted file mode 100644 index 96ec6d7..0000000 --- a/packages/connected/src/ResourceLinkQuery.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { LdoBase, ShapeType } from "@ldo/ldo"; -import type { - ExpandDeep, - ILinkQuery, - LQInput, - LQReturn, -} from "./types/ILinkQuery"; -import type { ConnectedPlugin } from "./types/ConnectedPlugin"; -import type { JsonldDatasetProxyBuilder } from "@ldo/jsonld-dataset-proxy"; -import type { ConnectedLdoDataset } from "./ConnectedLdoDataset"; -import type { SubjectNode } from "@ldo/rdf-utils"; - -export class ResourceLinkQuery< - Type extends LdoBase, - QueryInput extends LQInput, - Plugins extends ConnectedPlugin[], -> implements ILinkQuery -{ - constructor( - protected parentDataset: ConnectedLdoDataset, - protected shapeType: ShapeType, - protected jsonldDatasetProxyBuilder: JsonldDatasetProxyBuilder, - protected startingResource: Plugins[number]["types"]["resource"], - protected startingSubject: SubjectNode | string, - protected linkQueryInput: QueryInput, - ) {} - - run(): Promise>> { - throw new Error("Method not implemented."); - } - - subscribe(): Promise { - throw new Error("Method not implemented."); - } - - unsubscribe(): void { - throw new Error("Method not implemented."); - } - - fromSubject(): ExpandDeep> { - throw new Error("Method not implemented."); - } -} diff --git a/packages/connected/src/index.ts b/packages/connected/src/index.ts index 806e8ec..b8cec59 100644 --- a/packages/connected/src/index.ts +++ b/packages/connected/src/index.ts @@ -1,4 +1,5 @@ export * from "./types/IConnectedLdoDataset"; +export * from "./ConnectedLdoBuilder"; export * from "./ConnectedLdoDataset"; export * from "./ConnectedLdoTransactionDataset"; export * from "./types/ConnectedPlugin"; @@ -21,3 +22,6 @@ export * from "./results/success/ReadSuccess"; export * from "./results/success/UpdateSuccess"; export * from "./notifications/NotificationSubscription"; + +export * from "./linkTraversal/ResourceLinkQuery"; +export * from "./linkTraversal/exploreLinks"; diff --git a/packages/connected/src/linkTraversal/ResourceLinkQuery.ts b/packages/connected/src/linkTraversal/ResourceLinkQuery.ts new file mode 100644 index 0000000..54d9380 --- /dev/null +++ b/packages/connected/src/linkTraversal/ResourceLinkQuery.ts @@ -0,0 +1,75 @@ +import type { LdoBase, ShapeType } from "@ldo/ldo"; +import type { + ExpandDeep, + ILinkQuery, + LQInput, + LQReturn, +} from "../types/ILinkQuery"; +import type { ConnectedPlugin } from "../types/ConnectedPlugin"; +import type { SubjectNode } from "@ldo/rdf-utils"; +import { exploreLinks } from "./exploreLinks"; +import type { IConnectedLdoDataset } from "../types/IConnectedLdoDataset"; +import type { IConnectedLdoBuilder } from "../types/IConnectedLdoBuilder"; + +export class ResourceLinkQuery< + Type extends LdoBase, + QueryInput extends LQInput, + Plugins extends ConnectedPlugin[], +> implements ILinkQuery +{ + protected trackedResources: Set = + new Set(); + // uri -> unsubscribeId + protected resourceUnsubscribeIds: Record = {}; + protected thisUnsubscribeIds: Set = new Set(); + + constructor( + protected parentDataset: IConnectedLdoDataset, + protected shapeType: ShapeType, + protected ldoBuilder: IConnectedLdoBuilder, + protected startingResource: Plugins[number]["types"]["resource"], + protected startingSubject: SubjectNode | string, + protected linkQueryInput: QueryInput, + ) {} + + async run(options?: { + reload?: boolean; + }): Promise>> { + await exploreLinks( + this.parentDataset, + this.shapeType, + this.startingResource, + this.startingSubject, + this.linkQueryInput, + { shouldRefreshResources: options?.reload }, + ); + return this.fromSubject(); + } + + subscribe(): Promise { + throw new Error("Method not implemented."); + } + + private async fullUnsubscribe(): Promise { + // TODO + } + + async unsubscribe(unsubscribeId: string): Promise { + this.thisUnsubscribeIds.delete(unsubscribeId); + if (this.thisUnsubscribeIds.size === 0) { + await this.fullUnsubscribe(); + } + } + + fromSubject(): ExpandDeep> { + return this.ldoBuilder.fromSubject( + this.startingSubject, + ) as unknown as ExpandDeep>; + } + + getSubscribedResources(): Plugins[number]["types"]["resource"][] { + return Object.keys(this.resourceUnsubscribeIds).map((uri) => + this.parentDataset.getResource(uri), + ); + } +} diff --git a/packages/connected/src/linkTraversal/exploreLinks.ts b/packages/connected/src/linkTraversal/exploreLinks.ts new file mode 100644 index 0000000..9e9500c --- /dev/null +++ b/packages/connected/src/linkTraversal/exploreLinks.ts @@ -0,0 +1,135 @@ +import type { LdoBase, ShapeType } from "@ldo/ldo"; +import type { ConnectedPlugin } from "../types/ConnectedPlugin"; +import type { SubjectNode } from "@ldo/rdf-utils"; +import type { LQInput } from "../types/ILinkQuery"; +import { BasicLdSet } from "@ldo/jsonld-dataset-proxy"; +import type { IConnectedLdoDataset } from "../types/IConnectedLdoDataset"; + +interface ExploreLinksOptions { + onResourceEncountered?: ( + resource: Plugins[number]["types"]["resource"], + ) => void; + shouldRefreshResources?: boolean; +} + +export async function exploreLinks< + Type extends LdoBase, + Plugins extends ConnectedPlugin[], +>( + dataset: IConnectedLdoDataset, + shapeType: ShapeType, + startingResource: Plugins[number]["types"]["resource"], + startingSubject: SubjectNode | string, + queryInput: LQInput, + options?: ExploreLinksOptions, +): Promise { + // Do an initial check of the resources. + const readResult = options?.shouldRefreshResources + ? await startingResource.read() + : await startingResource.readIfUnfetched(); + if (readResult.isError) return; + + const ldObject = dataset.usingType(shapeType).fromSubject(startingSubject); + + const fetchedDuringThisExploration = new Set([startingResource.uri]); + + // Recursively explore the rest + await exploreLinksRecursive( + dataset, + ldObject, + queryInput, + fetchedDuringThisExploration, + options, + ); +} + +export async function exploreLinksRecursive< + Type extends LdoBase, + Plugins extends ConnectedPlugin[], +>( + dataset: IConnectedLdoDataset, + ldObject: Type, + queryInput: LQInput, + fetchedDuringThisExploration: Set, + options?: ExploreLinksOptions, +): Promise { + const shouldFetch = shouldFetchResource( + dataset, + ldObject, + queryInput, + fetchedDuringThisExploration, + ); + if (shouldFetch) { + const resourceToFetch = dataset.getResource(ldObject["@id"]); + const readResult = options?.shouldRefreshResources + ? await resourceToFetch.read() + : await resourceToFetch.readIfUnfetched(); + // If there was an error with the read, the traversal is done. + if (readResult.isError) { + return; + } + fetchedDuringThisExploration.add(resourceToFetch.uri); + } + // Recurse through the other elemenets + await Promise.all( + Object.entries(queryInput).map(async ([queryKey, queryValue]) => { + if ( + queryValue != undefined && + queryValue !== true && + ldObject[queryKey] != undefined + ) { + if (ldObject[queryKey] instanceof BasicLdSet) { + await Promise.all( + ldObject[queryKey].map(async (item) => { + await exploreLinksRecursive( + dataset, + item, + queryValue, + fetchedDuringThisExploration, + options, + ); + }), + ); + } + await exploreLinksRecursive( + dataset, + ldObject[queryKey], + queryValue, + fetchedDuringThisExploration, + options, + ); + } + }), + ); +} + +/** + * Determines if a resource needs to be fetched based on given data + */ +export function shouldFetchResource< + Type extends LdoBase, + Plugins extends ConnectedPlugin[], +>( + dataset: IConnectedLdoDataset, + ldObject: Type, + queryInput: LQInput, + fetchedDuringThisExploration: Set, +): boolean { + const linkedResourceUri: string | undefined = ldObject["@id"]; + // If it's a blank node, no need to fetch + if (!linkedResourceUri) return false; + const linkedResource = dataset.getResource(linkedResourceUri); + // If we've already explored the resource in this exporation, do not fetch + if (fetchedDuringThisExploration.has(linkedResource.uri)) return false; + + return Object.entries(queryInput).some(([queryKey, queryValue]) => { + // If value is undefined then no need to fetch + if (!queryValue) return false; + // Always fetch if there's a set in the object + if (ldObject[queryKey] instanceof BasicLdSet) return true; + // Fetch if a singleton set is not present + if (ldObject[queryKey] == undefined) return true; + // Otherwise no need t to fetch + return false; + }); +} diff --git a/packages/connected/src/notifications/NotificationSubscription.ts b/packages/connected/src/notifications/NotificationSubscription.ts index 11989dc..0945aa4 100644 --- a/packages/connected/src/notifications/NotificationSubscription.ts +++ b/packages/connected/src/notifications/NotificationSubscription.ts @@ -1,6 +1,6 @@ import { v4 } from "uuid"; import type { ConnectedPlugin } from "../types/ConnectedPlugin"; -import type { ConnectedContext } from "../ConnectedContext"; +import type { ConnectedContext } from "../types/ConnectedContext"; import type { SubscriptionCallbacks } from "./SubscriptionCallbacks"; import type { NotificationCallbackError } from "../results/error/NotificationErrors"; diff --git a/packages/connected/src/types/ConnectedContext.ts b/packages/connected/src/types/ConnectedContext.ts index 18db26e..d3f5fc6 100644 --- a/packages/connected/src/types/ConnectedContext.ts +++ b/packages/connected/src/types/ConnectedContext.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { ConnectedLdoDataset } from "./ConnectedLdoDataset"; -import type { ConnectedPlugin } from "./types/ConnectedPlugin"; +import type { ConnectedPlugin } from "./ConnectedPlugin"; +import type { IConnectedLdoDataset } from "./IConnectedLdoDataset"; /** * Each Plugin comes with a context. This is the aggregate of all those contexts @@ -9,7 +9,7 @@ import type { ConnectedPlugin } from "./types/ConnectedPlugin"; export type ConnectedContext< Plugins extends ConnectedPlugin[], > = { - dataset: ConnectedLdoDataset; + dataset: IConnectedLdoDataset; } & { [P in Plugins[number] as P["name"]]: P["types"]["context"]; }; diff --git a/packages/connected/src/types/ILinkQuery.ts b/packages/connected/src/types/ILinkQuery.ts index efeb229..d62f6b2 100644 --- a/packages/connected/src/types/ILinkQuery.ts +++ b/packages/connected/src/types/ILinkQuery.ts @@ -3,45 +3,54 @@ // If I ever want to implement a global query interface, this is a good place // to start. -import type { LdoBase, LdSet } from "@ldo/ldo"; +import type { LdoBase, LdSet, ShapeType } from "@ldo/ldo"; +import { ProfileShapeType } from "packages/ldo/test/profileData"; /** * Link Query Input */ +export type LQInput = LQInputObject; + export type LQInputObject = Partial<{ - [key in keyof Type]: LQInput; + [key in keyof Type]: LQInputFlattenSet; }>; export type LQInputSubSet = Type extends object ? LQInputObject : true; -export type LQInput = Type extends LdSet +export type LQInputFlattenSet = Type extends LdSet ? LQInputSubSet : LQInputSubSet; /** * Link Query Input Default */ -export type LQInputDefaultType = { - [key in keyof Type]: Type[key] extends object ? undefined : true; -}; +// TODO: I don't remember why I need this. Delete if unneeded +// export type LQInputDefaultType = { +// [key in keyof Type]: Type[key] extends object ? undefined : true; +// }; -export type LQInputDefault = - LQInputDefaultType extends LQInput - ? LQInputDefaultType - : never; +// export type LQInputDefault = +// LQInputDefaultType extends LQInput +// ? LQInputDefaultType +// : never; /** * Link Query Return */ +export type LQReturn> = LQReturnObject< + Type, + Input +>; + export type LQReturnObject> = { [key in keyof Required as undefined extends Input[key] ? never - : key]: Input[key] extends LQInput + : key]: Input[key] extends LQInputFlattenSet ? undefined extends Type[key] - ? LQReturn | undefined - : LQReturn + ? LQReturnExpandSet | undefined + : LQReturnExpandSet : never; }; @@ -51,9 +60,9 @@ export type LQReturnSubSet = Input extends LQInputSubSet : Type : never; -export type LQReturn< +export type LQReturnExpandSet< Type, - Input extends LQInput, + Input extends LQInputFlattenSet, > = NonNullable extends LdSet ? LdSet> : LQReturnSubSet; @@ -67,9 +76,33 @@ export type ExpandDeep = T extends LdSet /** * ILinkQuery: Manages resources in a link query */ +export interface LinkQueryRunOptions { + reload?: boolean; +} + export interface ILinkQuery> { - run(): Promise>>; - subscribe(): Promise; - unsubscribe(): void; + run( + options?: LinkQueryRunOptions, + ): Promise>>; + subscribe(): Promise; + unsubscribe(subscriptionId: string): void; fromSubject(): ExpandDeep>; } + +// TODO: Remove test functions +// function test>( +// _shapeType: ShapeType, +// _input: Input, +// ): ExpandDeep> { +// throw new Error("Not Implemeneted"); +// } +// const result = test(ProfileShapeType, { +// fn: true, +// name: true, +// hasTelephone: { +// type: { +// "@id": true, +// }, +// value: true, +// }, +// });