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

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

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

@ -1,9 +1,5 @@
import type {
ConnectedContext,
ReadSuccess,
SubscriptionCallbacks,
UpdateSuccess,
} from "@ldo/connected";
import type { ConnectedContext, SubscriptionCallbacks } from "@ldo/connected";
import { UnexpectedResourceError, UpdateSuccess } from "@ldo/connected";
import {
Unfetched,
type ConnectedResult,
@ -14,8 +10,11 @@ import type { NextGraphUri } from "../types";
import EventEmitter from "events";
import type { NextGraphConnectedPlugin } from "../NextGraphConnectedPlugin";
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 { Quad } from "@rdfjs/types";
import { namedNode, quad as createQuad } from "@rdfjs/data-model";
import { NextGraphReadSuccess } from "../results/NextGraphReadSuccess";
export class NextGraphResource
extends (EventEmitter as new () => ResourceEventEmitter)
@ -27,6 +26,10 @@ export class NextGraphResource
public status: ConnectedResult;
protected context: ConnectedContext<NextGraphConnectedPlugin[]>;
private fetched: boolean = false;
private loading: boolean = false;
private present: boolean | undefined = undefined;
constructor(
uri: NextGraphUri,
context: ConnectedContext<NextGraphConnectedPlugin[]>,
@ -38,45 +41,131 @@ export class NextGraphResource
}
isLoading(): boolean {
throw new Error("Method not implemented.");
return this.loading;
}
isFetched(): boolean {
throw new Error("Method not implemented.");
return this.fetched;
}
isUnfetched(): boolean {
throw new Error("Method not implemented.");
return !this.fetched;
}
isDoingInitialFetch(): boolean {
throw new Error("Method not implemented.");
return this.loading && !this.fetched;
}
isPresent(): boolean {
throw new Error("Method not implemented.");
isPresent(): boolean | undefined {
return this.present;
}
isAbsent(): boolean {
throw new Error("Method not implemented.");
isAbsent(): boolean | undefined {
return !this.present;
}
isSubscribedToNotifications(): boolean {
throw new Error("Method not implemented.");
}
read(): Promise<ReadSuccess<NextGraphResource>> {
throw new Error("Method not implemented.");
private handleThrownError(
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>> {
throw new Error("Method not implemented.");
async read(): Promise<
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);
}
}
update(
_datasetChanges: DatasetChanges,
): Promise<UpdateSuccess<NextGraphResource>> {
throw new Error("Method Not Implemented");
async readIfUnfetched(): Promise<
NextGraphReadSuccess | UnexpectedResourceError<NextGraphResource>
> {
if (this.isFetched()) {
return new NextGraphReadSuccess(this, true);
}
return this.read();
}
async update(
datasetChanges: DatasetChanges<Quad>,
): Promise<
| 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) {
@ -89,7 +178,7 @@ export class NextGraphResource
throw new Error("Method not implemented.");
}
unsubscribeFromNotifications(subscriptionId: string): Promise<void> {
unsubscribeFromNotifications(_subscriptionId: string): Promise<void> {
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 ng from "nextgraph";
import type { NextGraphConnectedPlugin } from "../src";
import type {
NextGraphConnectedPlugin,
NextGraphResource,
NextGraphUri,
} from "../src";
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", () => {
let nextgraphLdoDataset: ConnectedLdoDataset<NextGraphConnectedPlugin[]>;
beforeEach(async () => {
// Generate a wallet
console.log("gen wallet");
const [walletBinary, mnemonic] = await ng.gen_wallet_for_test(
const [wallet, mnemonic] = await ng.gen_wallet_for_test(
"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(
wallet,
wallet.wallet,
mnemonic,
[1, 2, 1, 2],
);
@ -26,15 +43,92 @@ describe("NextGraph Plugin", () => {
const walletName = openedWallet.V0.wallet_id;
const session = await ng.session_in_memory_start(walletName, userId);
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
nextgraphLdoDataset = createNextGraphLdoDataset();
nextgraphLdoDataset.setContext("nextgraph", { sessionId });
console.log("After ldo dataset");
nextgraphLdoDataset.setContext("nextgraph", {
sessionId,
protectedStoreId,
publicStoreId,
privateStoreId,
});
});
it("trivial", () => {
expect(true).toBe(true);
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);
});
});
describe("readResource", () => {
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,
createResourceOptions?: Plugin["types"]["createResourceOptions"],
): 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(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore I have no idea why this doesn't work
@ -272,7 +274,7 @@ export class ConnectedLdoDataset<
>(pluginName: Name, context: Plugin["types"]["context"]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.context[name] = context;
this.context[pluginName] = { ...this.context[pluginName], ...context };
}
public startTransaction(): ConnectedLdoTransactionDataset<Plugins> {

Loading…
Cancel
Save