Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ ex:person1 ex:name "Alice" .
Class usage:

```javascript
Comment thread
jeswr marked this conversation as resolved.
const person1 = new Person("https://example.org/person1", dataset_x, DataFactory)
import { DataFactory } from "n3"
const person1 = Person.from("https://example.org/person1", dataset_x, DataFactory)

// Get property
console.log(person1.name)
Expand Down Expand Up @@ -149,7 +150,7 @@ ex:person2
Class usage:

```javascript
const person2 = new Person("https://example.org/person2", dataset_z, DataFactory)
const person2 = Person.from("https://example.org/person2", dataset_z, DataFactory)

// Get property
console.log(person2.name)
Expand All @@ -160,7 +161,7 @@ console.log(person2.mum.name)
// outputs "Alice"

// Set class properties
const person3 = new Person("https://example.org/person3", dataset_z, DataFactory)
const person3 = Person.from("https://example.org/person3", dataset_z, DataFactory)
person3.name = "Joanne"
person1.mum = person3
console.log(person1.mum.name)
Expand Down
113 changes: 47 additions & 66 deletions src/TermWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { BaseQuad, DataFactory, DatasetCore, Literal, NamedNode, Quad_Subject, Term } from "@rdfjs/types"
import type { IRdfJsTerm } from "./type/IRdfJsTerm.js"
import type { DataFactory, DatasetCore, NamedNode, Term } from "@rdfjs/types"

/**
* `TermWrapper` is one of the two central constructs of this library. It is the base class of all models that represent a mapping from RDF to JavaScript. It _is_ an {@link Term | RDF/JS term} (or node) that also has a reference to both the dataset (or graph) that is the context of (i.e. contains) the term and to a factory that can be used to create additional terms.
Expand Down Expand Up @@ -31,46 +30,30 @@ import type { IRdfJsTerm } from "./type/IRdfJsTerm.js"
* We can work with this data in JavaScript and TypeScript as follows:
* ```ts
* const dataset: DatasetCore // which has the RDF above loaded
* const instance = new SomeClass("http://example.com/someSubject", dataset, DataFactory)
* const instance = SomeClass.from("http://example.com/someSubject", dataset, DataFactory)
*
* const value = instance.someProperty // contains "some value"
*
* instance.someProperty = "some other value" // underlying RDF is now <someSubject> <someProperty> "some other value" .
* ```
*
* @example Using instances of TermWrapper as instances of RDF/JS Term
* Since this class implements all members of all term types (named nodes, literals, blank nodes etc.), it can be cast to an RDF/JS Term:
* Instances created via {@link TermWrapper.from} are typed as both the wrapper and the underlying RDF/JS {@link Term}, so they can be passed directly anywhere a `Term` is expected — no casts required:
* ```ts
* let instance: TermWrapper
* const instance = SomeClass.from("http://example.com/someSubject", dataset, factory)
*
* // Our instance cast as Term
* const term = instance as Term
* ```
*
* @example Using instances of TermWrapper to create quads
* Instances of this class can be used anywhere an RDF/JS Term can be used, which includes creating quads:
* ```ts
* let instance: TermWrapper
* let factory: DataFactory
* const predicate = factory.namedNode("http://example.com/p")
* const object = factory.literal("o")
* // Used as subject when creating a quad
* factory.quad(instance, predicate, object)
*
* // Our instance used as subject when creating a quad
* factory.quad(instance as Quad_Subject, predicate, object)
* // Used as subject when matching statements in a dataset
* dataset.match(instance)
* ```
*
* @example Using instances of TermWrapper to match graph patterns
* Instances of this class can be used anywhere an RDF/JS Term can be used, which includes matching quads in a dataset:
* ```ts
* let instance: TermWrapper
* let dataset: DatasetCore
*
* // Our instance used as subject when matching statements in a dataset
* dataset.match(instance as Term)
* ```
* @see
* - {@link TermWrapper.from}
*/
export class TermWrapper implements IRdfJsTerm {
private readonly original: Term
export class TermWrapper<T extends Term = Term> {
private readonly original: T
private readonly _dataset: DatasetCore
private readonly _factory: DataFactory

Expand All @@ -93,7 +76,7 @@ export class TermWrapper implements IRdfJsTerm {
constructor(term: Term, dataset: DatasetCore, factory: DataFactory)

constructor(term: string | Term, dataset: DatasetCore, factory: DataFactory) {
this.original = typeof term === "string" ? factory.namedNode(term) : term
this.original = (typeof term === "string" ? factory.namedNode(term) : term) as T
this._dataset = dataset
this._factory = factory
}
Expand Down Expand Up @@ -189,7 +172,7 @@ export class TermWrapper implements IRdfJsTerm {

//#region Implementation of RDF/JS Term

get termType(): Term["termType"] {
get termType(): T["termType"] {
return this.original.termType
}

Expand All @@ -201,41 +184,39 @@ export class TermWrapper implements IRdfJsTerm {
return this.original.equals(other)
}

//#region Implementation of RDF/JS Literal

get language(): string {
return (this.original as Literal).language
}

get direction(): Literal["direction"] {
return (this.original as Literal).direction
}

get datatype(): NamedNode {
return (this.original as Literal).datatype
}

//#endregion

//#region Implementation of RDF/JS Quad

get subject(): Term {
return (this.original as BaseQuad).subject
}

get predicate(): Term {
return (this.original as BaseQuad).predicate
}

get object(): Term {
return (this.original as BaseQuad).object
}

get graph(): Term {
return (this.original as BaseQuad).graph
/**
* Creates a new instance of this class (or subclass), typed as both the wrapper and the underlying RDF/JS {@link Term}.
*
* @remarks
* Equivalent to invoking the constructor directly, but the returned value is typed as the intersection of the (sub)class instance type and the term type. When called on a subclass (e.g. `Child.from(...)`), the returned value is `Child & T`, where `T` is inferred from the `term` argument (defaults to {@link NamedNode} when a string is passed).
*
* @example
* ```ts
* const child = Child.from("http://example.com/x", dataset, factory)
* // typeof child === Child & NamedNode<string>
* ```
*/
public static from<This extends new (term: string, dataset: DatasetCore, factory: DataFactory) => any>(
this: This,
term: string,
dataset: DatasetCore,
factory: DataFactory,
): InstanceType<This> & NamedNode<string>
public static from<T extends Term, This extends new (term: T, dataset: DatasetCore, factory: DataFactory) => any>(
this: This,
term: T,
dataset: DatasetCore,
factory: DataFactory,
): InstanceType<This> & T
public static from(this: any, term: string | Term, dataset: DatasetCore, factory: DataFactory): any {
return new this(term, dataset, factory)
}
}

//#endregion

//#endregion
for (const prop of ['language', 'direction', 'datatype', 'subject', 'predicate', 'object', 'graph'] as const) {
Object.defineProperty(TermWrapper.prototype, prop, {
get(this: TermWrapper) { return (this as any).original[prop] },
enumerable: false,
configurable: true,
})
}
Comment on lines +216 to 222
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These RDF/JS-term properties are being added at runtime via Object.defineProperty, but the class doesn’t declare them in its TypeScript shape anymore. That forces downstream code to use as unknown as Literal casts and is a public typing regression vs the previous IRdfJsTerm interface. Consider adding declare readonly language/direction/datatype/subject/predicate/object/graph (or a replacement interface) so consumers and internal mappers can access them without unsafe casts while keeping the runtime implementation as-is.

Copilot uses AI. Check for mistakes.
10 changes: 5 additions & 5 deletions src/ensure.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { DefaultGraph, Literal, Quad, Term } from "@rdfjs/types"
import { TermTypeError } from "./errors/TermTypeError.js"
import { LiteralDatatypeError } from "./errors/LiteralDatatypeError.js"
import type { IRdfJsTerm } from "./type/IRdfJsTerm.js"
import { RDF } from "./vocabulary/RDF.js"
import { ListRootError } from "./errors/ListRootError.js"
import { NamedGraphError } from "./errors/NamedGraphError.js"
import { TermWrapper } from "./TermWrapper.js"

export function ensurePresent(object: any) {
if (object !== undefined && object !== null) {
Expand All @@ -30,15 +30,15 @@ export function ensureTermType(term: { termType: Term["termType"] }, type: Term[
throw new TermTypeError(term as Term, type)
}

export function ensureDatatype(term: IRdfJsTerm, ...datatypes: string[]) {
if (datatypes.includes(term.datatype.value)) {
export function ensureDatatype(term: TermWrapper, ...datatypes: string[]) {
if (datatypes.includes((term as unknown as Literal).datatype.value)) {
return
}

throw new LiteralDatatypeError(term as Literal, datatypes)
throw new LiteralDatatypeError(term as unknown as Literal, datatypes)
Comment thread
jeswr marked this conversation as resolved.
}

export function ensureListRoot(term: IRdfJsTerm) {
export function ensureListRoot(term: TermWrapper) {
if (term.termType === "NamedNode" && term.value === RDF.nil) {
return
}
Expand Down
8 changes: 4 additions & 4 deletions src/mapping/LiteralAs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export namespace LiteralAs {
ensureDatatype(term, RDF.langString)

// TODO: Direction
return {lang: term.language, string: term.value}
return {lang: (term as unknown as Literal).language, string: term.value}
Comment thread
jeswr marked this conversation as resolved.
}

export function number(term: TermWrapper): number {
Expand Down Expand Up @@ -152,7 +152,7 @@ export namespace LiteralAs {
ensureTermType(term, "Literal")
ensureDatatype(term, ...byteArrayDatatypes)

switch (term.datatype.value) {
switch ((term as unknown as Literal).datatype.value) {
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uInt8Array uses (term as unknown as Literal).datatype.value after the term-type check. If TermWrapper re-declares datatype in its TS shape (or ensureTermType becomes an asserts-style type guard), this cast can be removed and the mapper stays type-safe.

Copilot uses AI. Check for mistakes.
case XSD.hexBinary:
// TODO: When Node 25 - return Uint8Array.fromHex(term.value)
return Uint8Array.from(Buffer.from(term.value, "hex"))
Expand All @@ -179,15 +179,15 @@ export namespace LiteralAs {
ensureTermType(term, "Literal")
ensureDatatype(term, RDF.langString)

return [term.language, term.value]
return [(term as unknown as Literal).language, term.value]
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

langTuple is using term as unknown as Literal just to access .language. To keep this mapper clean and type-safe, consider restoring language on TermWrapper’s declared type (or returning a narrowed literal type from ensureTermType) so this doesn’t require an unsafe cast.

Copilot uses AI. Check for mistakes.
}

export function datatypeTuple(term: TermWrapper): [string, string] {
ensurePresent(term)
ensureIs(term, TermWrapper)
ensureTermType(term, "Literal")

return [term.datatype.value, term.value]
return [(term as unknown as Literal).datatype.value, term.value]
Comment thread
jeswr marked this conversation as resolved.
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/mapping/TermFrom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DataFactory, Term } from "@rdfjs/types"
import type { IRdfJsTerm } from "../type/IRdfJsTerm.js"
import { TermWrapper } from "../TermWrapper.js"

/**
* A collection of {@link ITermAsValueMapping | mappers} that create RDF/JS terms from JavaScript primitives.
Expand All @@ -9,7 +9,7 @@ import type { IRdfJsTerm } from "../type/IRdfJsTerm.js"
* - [Nodes in RDF 1.1 Concepts and Abstract Syntax](https://www.w3.org/TR/rdf11-concepts/#dfn-node)
*/
export namespace TermFrom {
export function instance(value: IRdfJsTerm, factory: DataFactory): Term {
export function instance(value: TermWrapper, factory: DataFactory): Term {
return itself(value as Term, factory)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TermFrom.instance casts value as Term, but TermWrapper is already structurally compatible with Term (it exposes termType, value, and equals). Dropping the cast and passing value directly would keep this mapper simpler and avoid masking future typing issues.

Suggested change
return itself(value as Term, factory)
return itself(value, factory)

Copilot uses AI. Check for mistakes.
}

Expand Down
63 changes: 0 additions & 63 deletions src/type/IRdfJsTerm.ts

This file was deleted.

Loading