UNPKG

11.4 kBPlain TextView Raw
1/**
2 * Copyright 2020 Inrupt Inc.
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal in
6 * the Software without restriction, including without limitation the rights to use,
7 * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8 * Software, and to permit persons to whom the Software is furnished to do so,
9 * subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 */
21
22import { NamedNode, Quad } from "rdf-js";
23import { dataset, filter, clone } from "./rdfjs";
24import {
25 isLocalNode,
26 isEqual,
27 isNamedNode,
28 getLocalNode,
29 asNamedNode,
30 resolveLocalIri,
31} from "./datatypes";
32import {
33 LitDataset,
34 UrlString,
35 Thing,
36 Url,
37 ThingLocal,
38 LocalNode,
39 ThingPersisted,
40 ChangeLog,
41 DatasetInfo,
42 hasChangelog,
43 hasDatasetInfo,
44} from "./interfaces";
45
46/**
47 * @hidden Scopes are not yet consistently used in Solid and hence not properly implemented in this library yet (the add*() and set*() functions do not respect it yet), so we're not exposing these to developers at this point in time.
48 */
49export interface GetThingOptions {
50 /**
51 * Which Named Graph to extract the Thing from.
52 *
53 * If not specified, the Thing will include Quads from all Named Graphs in the given
54 * [[LitDataset]].
55 **/
56 scope?: Url | UrlString;
57}
58/**
59 * Extract Quads with a given Subject from a [[LitDataset]] into a [[Thing]].
60 *
61 * @param litDataset The [[LitDataset]] to extract the [[Thing]] from.
62 * @param thingUrl The URL of the desired [[Thing]].
63 * @param options Not yet implemented.
64 */
65export function getThingOne(
66 litDataset: LitDataset,
67 thingUrl: UrlString | Url | LocalNode,
68 options: GetThingOptions = {}
69): Thing {
70 const subject = isLocalNode(thingUrl) ? thingUrl : asNamedNode(thingUrl);
71 const scope: NamedNode | null = options.scope
72 ? asNamedNode(options.scope)
73 : null;
74
75 const thingDataset = litDataset.match(subject, null, null, scope);
76
77 if (isLocalNode(subject)) {
78 const thing: ThingLocal = Object.assign(thingDataset, {
79 name: subject.name,
80 });
81
82 return thing;
83 } else {
84 const thing: Thing = Object.assign(thingDataset, {
85 url: subject.value,
86 });
87
88 return thing;
89 }
90}
91
92/**
93 * Get all [[Thing]]s about which a [[LitDataset]] contains Quads.
94 *
95 * @param litDataset The [[LitDataset]] to extract the [[Thing]]s from.
96 * @param options Not yet implemented.
97 */
98export function getThingAll(
99 litDataset: LitDataset,
100 options: GetThingOptions = {}
101): Thing[] {
102 const subjectNodes = new Array<Url | LocalNode>();
103 for (const quad of litDataset) {
104 // Because NamedNode objects with the same IRI are actually different
105 // object instances, we have to manually check whether `subjectNodes` does
106 // not yet include `quadSubject` before adding it.
107 const quadSubject = quad.subject;
108 if (
109 isNamedNode(quadSubject) &&
110 !subjectNodes.some((subjectNode) => isEqual(subjectNode, quadSubject))
111 ) {
112 subjectNodes.push(quadSubject);
113 }
114 if (
115 isLocalNode(quadSubject) &&
116 !subjectNodes.some((subjectNode) => isEqual(subjectNode, quadSubject))
117 ) {
118 subjectNodes.push(quadSubject);
119 }
120 }
121
122 const things: Thing[] = subjectNodes.map((subjectNode) =>
123 getThingOne(litDataset, subjectNode, options)
124 );
125
126 return things;
127}
128
129/**
130 * Insert a [[Thing]] into a [[LitDataset]], replacing previous instances of that Thing.
131 *
132 * @param litDataset The LitDataset to insert a Thing into.
133 * @param thing The Thing to insert into the given LitDataset.
134 * @returns A new LitDataset equal to the given LitDataset, but with the given Thing.
135 */
136export function setThing<Dataset extends LitDataset>(
137 litDataset: Dataset,
138 thing: Thing
139): Dataset & ChangeLog {
140 const newDataset = removeThing(litDataset, thing);
141
142 for (const quad of thing) {
143 newDataset.add(quad);
144 newDataset.changeLog.additions.push(quad);
145 }
146
147 return newDataset;
148}
149
150/**
151 * Remove a Thing from a LitDataset.
152 *
153 * @param litDataset The LitDataset to remove a Thing from.
154 * @param thing The Thing to remove from `litDataset`.
155 * @returns A new [[LitDataset]] equal to the input LitDataset, excluding the given Thing.
156 */
157export function removeThing<Dataset extends LitDataset>(
158 litDataset: Dataset,
159 thing: UrlString | Url | LocalNode | Thing
160): Dataset & ChangeLog {
161 const newLitDataset = withChangeLog(cloneLitStructs(litDataset));
162 const resourceIri: UrlString | undefined = hasDatasetInfo(newLitDataset)
163 ? newLitDataset.datasetInfo.fetchedFrom
164 : undefined;
165
166 const thingSubject = toNode(thing);
167 for (const quad of litDataset) {
168 if (!isNamedNode(quad.subject) && !isLocalNode(quad.subject)) {
169 // This data is unexpected, and hence unlikely to be added by us. Thus, leave it intact:
170 newLitDataset.add(quad);
171 } else if (
172 !isEqual(thingSubject, quad.subject, { resourceIri: resourceIri })
173 ) {
174 newLitDataset.add(quad);
175 } else {
176 newLitDataset.changeLog.deletions.push(quad);
177 }
178 }
179 return newLitDataset;
180}
181
182function withChangeLog<Dataset extends LitDataset>(
183 litDataset: Dataset
184): Dataset & ChangeLog {
185 const newLitDataset: Dataset & ChangeLog = hasChangelog(litDataset)
186 ? litDataset
187 : Object.assign(litDataset, {
188 changeLog: { additions: [], deletions: [] },
189 });
190 return newLitDataset;
191}
192
193function cloneLitStructs<Dataset extends LitDataset>(
194 litDataset: Dataset
195): Dataset {
196 const freshDataset = dataset();
197 if (hasChangelog(litDataset)) {
198 (freshDataset as LitDataset & ChangeLog).changeLog = {
199 additions: [...litDataset.changeLog.additions],
200 deletions: [...litDataset.changeLog.deletions],
201 };
202 }
203 if (hasDatasetInfo(litDataset)) {
204 (freshDataset as LitDataset & DatasetInfo).datasetInfo = {
205 ...litDataset.datasetInfo,
206 };
207 }
208
209 return freshDataset as Dataset;
210}
211
212interface CreateThingLocalOptions {
213 /**
214 * The name that should be used for this [[Thing]] when constructing its URL.
215 *
216 * If not provided, a random one will be generated.
217 */
218 name?: string;
219}
220interface CreateThingPersistedOptions {
221 /**
222 * The URL of the newly created [[Thing]].
223 */
224 url: UrlString;
225}
226export type CreateThingOptions =
227 | CreateThingLocalOptions
228 | CreateThingPersistedOptions;
229/**
230 * Initialise a new [[Thing]] in memory.
231 *
232 * @param options See [[CreateThingOptions]].
233 */
234export function createThing(
235 options: CreateThingPersistedOptions
236): ThingPersisted;
237export function createThing(options?: CreateThingLocalOptions): ThingLocal;
238export function createThing(options: CreateThingOptions = {}): Thing {
239 if (typeof (options as CreateThingPersistedOptions).url !== "undefined") {
240 const url = (options as CreateThingPersistedOptions).url;
241 /* istanbul ignore else [URL is defined is the testing environment, so we cannot test this] */
242 if (typeof URL !== "undefined") {
243 // Throws an error if the IRI is invalid:
244 new URL(url);
245 }
246 const thing: ThingPersisted = Object.assign(dataset(), { url: url });
247 return thing;
248 }
249 const name = (options as CreateThingLocalOptions).name ?? generateName();
250 const thing: ThingLocal = Object.assign(dataset(), { name: name });
251 return thing;
252}
253
254/**
255 * Get the URL to a given [[Thing]].
256 *
257 * @param thing The [[Thing]] you want to obtain the URL from.
258 * @param baseUrl If `thing` is not persisted yet, the base URL that should be used to construct this [[Thing]]'s URL.
259 */
260export function asUrl(thing: ThingLocal, baseUrl: UrlString): UrlString;
261export function asUrl(thing: ThingPersisted): UrlString;
262export function asUrl(thing: Thing, baseUrl?: UrlString): UrlString {
263 if (isThingLocal(thing)) {
264 if (typeof baseUrl === "undefined") {
265 throw new Error(
266 "The URL of a Thing that has not been persisted cannot be determined without a base URL."
267 );
268 }
269 return resolveLocalIri(thing.name, baseUrl);
270 }
271
272 return thing.url;
273}
274/** @hidden Alias of [[asUrl]] for those who prefer IRI terminology. */
275export const asIri = asUrl;
276
277/**
278 * @param thing The [[Thing]] of which a URL might or might not be known.
279 * @return Whether `thing` has no known URL yet.
280 */
281export function isThingLocal(
282 thing: ThingPersisted | ThingLocal
283): thing is ThingLocal {
284 return (
285 typeof (thing as ThingLocal).name === "string" &&
286 typeof (thing as ThingPersisted).url === "undefined"
287 );
288}
289/**
290 * @internal
291 * @param thing The Thing whose Subject Node you're interested in.
292 * @returns A Node that can be used as the Subject for this Thing's Quads.
293 */
294export function toNode(
295 thing: UrlString | Url | LocalNode | Thing
296): NamedNode | LocalNode {
297 if (isNamedNode(thing) || isLocalNode(thing)) {
298 return thing;
299 }
300 if (typeof thing === "string") {
301 return asNamedNode(thing);
302 }
303 if (isThingLocal(thing)) {
304 return getLocalNode(thing.name);
305 }
306 return asNamedNode(asUrl(thing));
307}
308
309/**
310 * @internal
311 * @param thing Thing to clone.
312 * @returns A new Thing with the same Quads as `input`.
313 */
314export function cloneThing<T extends Thing>(
315 thing: T
316): T extends ThingLocal ? ThingLocal : ThingPersisted;
317export function cloneThing(thing: Thing): Thing {
318 const cloned = clone(thing);
319 if (isThingLocal(thing)) {
320 (cloned as ThingLocal).name = thing.name;
321 return cloned as ThingLocal;
322 }
323 (cloned as ThingPersisted).url = thing.url;
324 return cloned as ThingPersisted;
325}
326
327/**
328 * @internal
329 * @param thing Thing to clone.
330 * @param callback Function that takes a Quad, and returns a boolean indicating whether that Quad should be included in the cloned Dataset.
331 * @returns A new Thing with the same Quads as `input`, excluding the ones for which `callback` returned `false`.
332 */
333export function filterThing<T extends Thing>(
334 thing: T,
335 callback: (quad: Quad) => boolean
336): T extends ThingLocal ? ThingLocal : ThingPersisted;
337export function filterThing(
338 thing: Thing,
339 callback: (quad: Quad) => boolean
340): Thing {
341 const filtered = filter(thing, callback);
342 if (isThingLocal(thing)) {
343 (filtered as ThingLocal).name = thing.name;
344 return filtered as ThingLocal;
345 }
346 (filtered as ThingPersisted).url = thing.url;
347 return filtered as ThingPersisted;
348}
349
350/**
351 * Generate a string that can be used as the unique identifier for a Thing
352 *
353 * This function works by starting with a date string (so that Things can be
354 * sorted chronologically), followed by a random number generated by taking a
355 * random number between 0 and 1, and cutting off the `0.`.
356 *
357 * @internal
358 * @returns An string that's likely to be unique
359 */
360const generateName = () => {
361 return (
362 Date.now().toString() + Math.random().toString().substring("0.".length)
363 );
364};