Nextgraph read tests

main
Jackson Morgan 5 months ago
parent 108a096c94
commit 3fcb4078fc
  1. 8
      package-lock.json
  2. 2
      packages/connected-nextgraph/package.json
  3. 29
      packages/connected-nextgraph/src/NextGraphConnectedPlugin.ts
  4. 137
      packages/connected-nextgraph/src/resources/NextGraphResource.ts
  5. 6
      packages/connected-nextgraph/src/results/NextGraphReadSuccess.ts
  6. 8
      packages/connected-nextgraph/src/results/NoNextGraphStoreError.ts
  7. 120
      packages/connected-nextgraph/test/integration.test.ts
  8. 6
      packages/connected/src/ConnectedLdoDataset.ts

8
package-lock.json generated

@ -18782,9 +18782,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nextgraph": { "node_modules/nextgraph": {
"version": "0.1.1-alpha.4", "version": "0.1.1-alpha.5",
"resolved": "https://registry.npmjs.org/nextgraph/-/nextgraph-0.1.1-alpha.4.tgz", "resolved": "https://registry.npmjs.org/nextgraph/-/nextgraph-0.1.1-alpha.5.tgz",
"integrity": "sha512-207fqu1RJ/ekN9iKJW43KOZfU7vJGeRKy/GTUwQgJzgumJ43HpMgOgruNhJQFZpA7d1Ycn3AysYQ1qfNbFBYQg==", "integrity": "sha512-RtJ/Oy+PfvjwnwmTpIeIjesi9y71AuZ9MbIjZ6TKb7aZwYvhabFKhx1CTpenvTQJ077DxbNUptpOMPvmIdMeIQ==",
"license": "MIT/Apache-2.0" "license": "MIT/Apache-2.0"
}, },
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
@ -26459,7 +26459,7 @@
"@solid-notifications/subscription": "^0.1.2", "@solid-notifications/subscription": "^0.1.2",
"cross-fetch": "^3.1.6", "cross-fetch": "^3.1.6",
"http-link-header": "^1.1.1", "http-link-header": "^1.1.1",
"nextgraph": "^0.1.1-alpha.4", "nextgraph": "^0.1.1-alpha.5",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {

@ -48,7 +48,7 @@
"@solid-notifications/subscription": "^0.1.2", "@solid-notifications/subscription": "^0.1.2",
"cross-fetch": "^3.1.6", "cross-fetch": "^3.1.6",
"http-link-header": "^1.1.1", "http-link-header": "^1.1.1",
"nextgraph": "^0.1.1-alpha.4", "nextgraph": "^0.1.1-alpha.5",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"files": [ "files": [

@ -3,9 +3,13 @@ import type { NextGraphUri } from "./types";
import { NextGraphResource } from "./resources/NextGraphResource"; import { NextGraphResource } from "./resources/NextGraphResource";
import ng from "nextgraph"; import ng from "nextgraph";
import { isNextGraphUri } from "./util/isNextGraphUri"; import { isNextGraphUri } from "./util/isNextGraphUri";
import { NoNextGraphStoreError } from "./results/NoNextGraphStoreError";
export interface NextGraphConnectedContext { export interface NextGraphConnectedContext {
sessionId?: string; sessionId?: string;
protectedStoreId?: string;
privateStoreId?: string;
publicStoreId?: string;
} }
export interface NextGraphCreateResourceOptions { export interface NextGraphCreateResourceOptions {
@ -26,7 +30,9 @@ export interface NextGraphConnectedPlugin
uri: NextGraphUri, uri: NextGraphUri,
context: ConnectedContext<this[]>, context: ConnectedContext<this[]>,
) => NextGraphResource; ) => NextGraphResource;
createResource(context: ConnectedContext<this[]>): Promise<NextGraphResource>; createResource(
context: ConnectedContext<this[]>,
): Promise<NextGraphResource | NoNextGraphStoreError>;
} }
export const nextGraphConnectedPlugin: NextGraphConnectedPlugin = { export const nextGraphConnectedPlugin: NextGraphConnectedPlugin = {
@ -36,17 +42,26 @@ export const nextGraphConnectedPlugin: NextGraphConnectedPlugin = {
uri: NextGraphUri, uri: NextGraphUri,
context: ConnectedContext<NextGraphConnectedPlugin[]>, context: ConnectedContext<NextGraphConnectedPlugin[]>,
): NextGraphResource { ): NextGraphResource {
// NIKO: Do I need to split into "base?" Remind me again of why I need base?
return new NextGraphResource(uri, context); return new NextGraphResource(uri, context);
}, },
createResource: async function ( createResource: async function (
context: ConnectedContext<NextGraphConnectedPlugin[]>, context: ConnectedContext<NextGraphConnectedPlugin[]>,
options?: NextGraphCreateResourceOptions, options?: NextGraphCreateResourceOptions,
): Promise<NextGraphResource> { ): Promise<NextGraphResource | NoNextGraphStoreError> {
const storeType = options?.storeType ?? "protected"; const storeType = options?.storeType ?? "protected";
// TODO: determine the name of the store repo from the session id. const storeRepo =
const storeRepo = options?.storeRepo ?? ""; options?.storeRepo ??
(storeType === "protected"
? context.nextgraph.protectedStoreId
: storeType === "public"
? context.nextgraph.publicStoreId
: storeType === "private"
? context.nextgraph.privateStoreId
: undefined);
if (!storeRepo) {
return new NoNextGraphStoreError();
}
const nuri: NextGraphUri = await ng.doc_create( const nuri: NextGraphUri = await ng.doc_create(
context.nextgraph.sessionId, context.nextgraph.sessionId,
@ -56,7 +71,9 @@ export const nextGraphConnectedPlugin: NextGraphConnectedPlugin = {
storeRepo, storeRepo,
"store", "store",
); );
return new NextGraphResource(nuri, context); const newResource = new NextGraphResource(nuri, context);
await newResource.read();
return newResource;
}, },
isUriValid: function (uri: string): uri is NextGraphUri { isUriValid: function (uri: string): uri is NextGraphUri {

@ -1,9 +1,5 @@
import type { import type { ConnectedContext, SubscriptionCallbacks } from "@ldo/connected";
ConnectedContext, import { UnexpectedResourceError, UpdateSuccess } from "@ldo/connected";
ReadSuccess,
SubscriptionCallbacks,
UpdateSuccess,
} from "@ldo/connected";
import { import {
Unfetched, Unfetched,
type ConnectedResult, type ConnectedResult,
@ -14,8 +10,11 @@ import type { NextGraphUri } from "../types";
import EventEmitter from "events"; import EventEmitter from "events";
import type { NextGraphConnectedPlugin } from "../NextGraphConnectedPlugin"; import type { NextGraphConnectedPlugin } from "../NextGraphConnectedPlugin";
import ng from "nextgraph"; import ng from "nextgraph";
import type { DatasetChanges } from "@ldo/rdf-utils"; import { changesToSparqlUpdate, type DatasetChanges } from "@ldo/rdf-utils";
import type { NextGraphNotificationMessage } from "../notifications/NextGraphNotificationMessage"; import type { NextGraphNotificationMessage } from "../notifications/NextGraphNotificationMessage";
import type { Quad } from "@rdfjs/types";
import { namedNode, quad as createQuad } from "@rdfjs/data-model";
import { NextGraphReadSuccess } from "../results/NextGraphReadSuccess";
export class NextGraphResource export class NextGraphResource
extends (EventEmitter as new () => ResourceEventEmitter) extends (EventEmitter as new () => ResourceEventEmitter)
@ -27,6 +26,10 @@ export class NextGraphResource
public status: ConnectedResult; public status: ConnectedResult;
protected context: ConnectedContext<NextGraphConnectedPlugin[]>; protected context: ConnectedContext<NextGraphConnectedPlugin[]>;
private fetched: boolean = false;
private loading: boolean = false;
private present: boolean | undefined = undefined;
constructor( constructor(
uri: NextGraphUri, uri: NextGraphUri,
context: ConnectedContext<NextGraphConnectedPlugin[]>, context: ConnectedContext<NextGraphConnectedPlugin[]>,
@ -38,45 +41,131 @@ export class NextGraphResource
} }
isLoading(): boolean { isLoading(): boolean {
throw new Error("Method not implemented."); return this.loading;
} }
isFetched(): boolean { isFetched(): boolean {
throw new Error("Method not implemented."); return this.fetched;
} }
isUnfetched(): boolean { isUnfetched(): boolean {
throw new Error("Method not implemented."); return !this.fetched;
} }
isDoingInitialFetch(): boolean { isDoingInitialFetch(): boolean {
throw new Error("Method not implemented."); return this.loading && !this.fetched;
} }
isPresent(): boolean { isPresent(): boolean | undefined {
throw new Error("Method not implemented."); return this.present;
} }
isAbsent(): boolean { isAbsent(): boolean | undefined {
throw new Error("Method not implemented."); return !this.present;
} }
isSubscribedToNotifications(): boolean { isSubscribedToNotifications(): boolean {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
read(): Promise<ReadSuccess<NextGraphResource>> { private handleThrownError(
throw new Error("Method not implemented."); err: unknown,
): UnexpectedResourceError<NextGraphResource> {
const error = UnexpectedResourceError.fromThrown(this, err);
this.loading = false;
this.status = error;
this.emit("update");
return error;
} }
readIfUnfetched(): Promise<ReadSuccess<NextGraphResource>> { async read(): Promise<
throw new Error("Method not implemented."); NextGraphReadSuccess | UnexpectedResourceError<NextGraphResource>
> {
try {
this.loading = true;
this.emit("update");
// Get the data
const sparqlResult = await ng.sparql_query(
this.context.nextgraph.sessionId,
`CONSTRUCT { ?s ?p ?o } WHERE { GRAPH <${this.uri}> { ?s ?p ?o } }`,
undefined,
this.uri,
);
// Update the dataset
const graphNode = namedNode(this.uri);
const dataset = this.context.dataset;
dataset.deleteMatches(undefined, undefined, undefined, graphNode);
dataset.addAll(
sparqlResult.map((ngQuad) => {
return createQuad(
ngQuad.subject,
ngQuad.predicate,
ngQuad.object,
graphNode,
);
}),
);
// Update statuses
const result = new NextGraphReadSuccess(this, false);
this.loading = false;
this.fetched = true;
this.present = true;
this.status = result;
this.emit("update");
return result;
} catch (err) {
if (err === "RepoNotFound") {
const result = new NextGraphReadSuccess(this, false);
this.loading = false;
this.fetched = true;
this.present = false;
this.status = result;
this.emit("update");
return result;
}
return this.handleThrownError(err);
}
}
async readIfUnfetched(): Promise<
NextGraphReadSuccess | UnexpectedResourceError<NextGraphResource>
> {
if (this.isFetched()) {
return new NextGraphReadSuccess(this, true);
}
return this.read();
} }
update( async update(
_datasetChanges: DatasetChanges, datasetChanges: DatasetChanges<Quad>,
): Promise<UpdateSuccess<NextGraphResource>> { ): Promise<
throw new Error("Method Not Implemented"); | UpdateSuccess<NextGraphResource>
| UnexpectedResourceError<NextGraphResource>
> {
this.loading = true;
this.emit("update");
// Optimistically apply updates
this.context.dataset.bulk(datasetChanges);
try {
// Perform Update with remote
await ng.sparql_update(
this.context.nextgraph.sessionId,
await changesToSparqlUpdate(datasetChanges),
this.uri,
);
return new UpdateSuccess(this);
} catch (err) {
// Revert data on error
this.context.dataset.bulk({
added: datasetChanges.removed,
removed: datasetChanges.added,
});
return this.handleThrownError(err);
}
} }
protected async onNotification(_message: unknown) { protected async onNotification(_message: unknown) {
@ -89,7 +178,7 @@ export class NextGraphResource
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
unsubscribeFromNotifications(subscriptionId: string): Promise<void> { unsubscribeFromNotifications(_subscriptionId: string): Promise<void> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }

@ -0,0 +1,6 @@
import { ReadSuccess } from "@ldo/connected";
import type { NextGraphResource } from "../resources/NextGraphResource";
export class NextGraphReadSuccess extends ReadSuccess<NextGraphResource> {
type = "nextGraphReadSuccess" as const;
}

@ -0,0 +1,8 @@
import { ErrorResult } from "@ldo/connected";
export class NoNextGraphStoreError extends ErrorResult {
type = "noNextGraphStore" as const;
constructor(message?: string) {
super(message ?? "No NextGraph store was provided.");
}
}

@ -1,24 +1,41 @@
import type { ConnectedLdoDataset } from "@ldo/connected"; import type { ConnectedLdoDataset } from "@ldo/connected";
import ng from "nextgraph"; import ng from "nextgraph";
import type { NextGraphConnectedPlugin } from "../src"; import type {
NextGraphConnectedPlugin,
NextGraphResource,
NextGraphUri,
} from "../src";
import { createNextGraphLdoDataset } from "../src/createNextGraphLdoDataset"; import { createNextGraphLdoDataset } from "../src/createNextGraphLdoDataset";
import { parseRdf } from "@ldo/ldo";
import { namedNode } from "@rdfjs/data-model";
import type { NextGraphReadSuccess } from "../src/results/NextGraphReadSuccess";
console.log("Running tests"); const SAMPLE_TTL = `@base <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rel: <http://www.perceive.net/schemas/relationship/> .
<#green-goblin>
rel:enemyOf <#spiderman> ;
a foaf:Person ; # in the context of the Marvel universe
foaf:name "Green Goblin" .
<#spiderman>
rel:enemyOf <#green-goblin> ;
a foaf:Person ;
foaf:name "Spiderman", "Человек-паук"@ru .`;
describe("NextGraph Plugin", () => { describe("NextGraph Plugin", () => {
let nextgraphLdoDataset: ConnectedLdoDataset<NextGraphConnectedPlugin[]>; let nextgraphLdoDataset: ConnectedLdoDataset<NextGraphConnectedPlugin[]>;
beforeEach(async () => { beforeEach(async () => {
// Generate a wallet // Generate a wallet
console.log("gen wallet"); const [wallet, mnemonic] = await ng.gen_wallet_for_test(
const [walletBinary, mnemonic] = await ng.gen_wallet_for_test(
"lL2mo9Jtgz8yWN5PSaEMMftDGXyKJNbv9atQOygmeTcA", "lL2mo9Jtgz8yWN5PSaEMMftDGXyKJNbv9atQOygmeTcA",
); );
console.log("read wallet file");
const wallet = await ng.wallet_read_file(walletBinary);
console.log("open wallet");
const openedWallet = await ng.wallet_open_with_mnemonic_words( const openedWallet = await ng.wallet_open_with_mnemonic_words(
wallet, wallet.wallet,
mnemonic, mnemonic,
[1, 2, 1, 2], [1, 2, 1, 2],
); );
@ -26,15 +43,92 @@ describe("NextGraph Plugin", () => {
const walletName = openedWallet.V0.wallet_id; const walletName = openedWallet.V0.wallet_id;
const session = await ng.session_in_memory_start(walletName, userId); const session = await ng.session_in_memory_start(walletName, userId);
const sessionId = session.session_id; const sessionId = session.session_id;
console.log("after open wallet"); const protectedStoreId = session.protected_store_id.substring(2, 46);
const publicStoreId = session.protected_store_id.substring(2, 46);
const privateStoreId = session.protected_store_id.substring(2, 46);
// Get SessionId for that wallet // Get SessionId for that wallet
nextgraphLdoDataset = createNextGraphLdoDataset(); nextgraphLdoDataset = createNextGraphLdoDataset();
nextgraphLdoDataset.setContext("nextgraph", { sessionId }); nextgraphLdoDataset.setContext("nextgraph", {
console.log("After ldo dataset"); sessionId,
protectedStoreId,
publicStoreId,
privateStoreId,
});
});
describe("createResource", () => {
it("creates a resource by assuming the protected store", async () => {
const resource = await nextgraphLdoDataset.createResource("nextgraph");
expect(resource.isError).toBe(false);
const resourceAsR = resource as NextGraphResource;
expect(resourceAsR.uri).toBeDefined();
expect(resourceAsR.isFetched()).toBe(true);
expect(resourceAsR.isPresent()).toBe(true);
});
}); });
it("trivial", () => { describe("readResource", () => {
expect(true).toBe(true); let populatedResourceUri: NextGraphUri;
beforeEach(async () => {
const resource = (await nextgraphLdoDataset.createResource(
"nextgraph",
)) as NextGraphResource;
await resource.update({
added: await parseRdf(SAMPLE_TTL),
});
nextgraphLdoDataset.forgetAllResources();
nextgraphLdoDataset.deleteMatches(
undefined,
undefined,
undefined,
undefined,
);
populatedResourceUri = resource.uri;
});
it("reads a resource that exists", async () => {
expect(nextgraphLdoDataset.size).toBe(0);
const resource = nextgraphLdoDataset.getResource(populatedResourceUri);
const result = await resource.read();
expect(result.isError).toBe(false);
expect(result.type).toBe("nextGraphReadSuccess");
expect(resource.isAbsent()).toBe(false);
expect(resource.isPresent()).toBe(true);
expect(resource.isLoading()).toBe(false);
expect(nextgraphLdoDataset.size).toBe(7);
expect(
nextgraphLdoDataset.match(
namedNode("http://example.org/#spiderman"),
namedNode("http://www.perceive.net/schemas/relationship/enemyOf"),
namedNode("http://example.org/#green-goblin"),
namedNode(resource.uri),
).size,
).toBe(1);
});
it("reads a resource that is absent", async () => {
const nuri =
"did:ng:o:W6GCQRfQkNTLtSS_2-QhKPJPkhEtLVh-B5lzpWMjGNEA:v:h8ViqyhCYMS2I6IKwPrY6UZi4ougUm1gpM4QnxlmNMQA";
const resource = nextgraphLdoDataset.getResource(nuri);
const readResult = await resource.read();
expect(resource.uri).toBe(nuri);
expect(readResult.type).toBe("nextGraphReadSuccess");
expect(nextgraphLdoDataset.size).toBe(0);
expect(resource.isLoading()).toBe(false);
expect(resource.isAbsent()).toBe(true);
});
it("Reads a resource from memory.", async () => {
const resource = nextgraphLdoDataset.getResource(populatedResourceUri);
await resource.read();
const result2 = await resource.readIfUnfetched();
expect(result2.isError).toBe(false);
const result = result2 as NextGraphReadSuccess;
expect(result.type).toBe("nextGraphReadSuccess");
expect(result.recalledFromMemory).toBe(true);
});
}); });
}); });
// Errors if it doesn't exist and an update is attempted

@ -182,7 +182,9 @@ export class ConnectedLdoDataset<
pluginName: Name, pluginName: Name,
createResourceOptions?: Plugin["types"]["createResourceOptions"], createResourceOptions?: Plugin["types"]["createResourceOptions"],
): Promise<ReturnType<Plugin["createResource"]>> { ): Promise<ReturnType<Plugin["createResource"]>> {
const validPlugin = this.plugins.find((plugin) => name === plugin.name)!; const validPlugin = this.plugins.find(
(plugin) => pluginName === plugin.name,
)!;
const newResourceResult = await validPlugin.createResource( const newResourceResult = await validPlugin.createResource(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore I have no idea why this doesn't work // @ts-ignore I have no idea why this doesn't work
@ -272,7 +274,7 @@ export class ConnectedLdoDataset<
>(pluginName: Name, context: Plugin["types"]["context"]) { >(pluginName: Name, context: Plugin["types"]["context"]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.context[name] = context; this.context[pluginName] = { ...this.context[pluginName], ...context };
} }
public startTransaction(): ConnectedLdoTransactionDataset<Plugins> { public startTransaction(): ConnectedLdoTransactionDataset<Plugins> {

Loading…
Cancel
Save