@muze-nl/oldm-core 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/package.json +30 -0
  4. package/src/oldm.mjs +961 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Muze.nl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # @muze-nl/oldm-core
2
+
3
+ Core Object Linked Data Mapper package.
4
+
5
+ This package contains the object/graph mapping layer only. It has explicit ESM exports, no bundled parser/writer, no N3 dependency, and no global side effects.
6
+
7
+ ```javascript
8
+ import oldm, { one, many, first, Collection } from '@muze-nl/oldm-core'
9
+ import { n3Parser, n3Writer } from '@muze-nl/oldm-n3'
10
+
11
+ const context = oldm({
12
+ parser: n3Parser,
13
+ writer: n3Writer
14
+ })
15
+ ```
16
+
17
+
18
+ ## Prefix preference
19
+
20
+ OLDM shortens predicate and type IRIs with the prefixes configured on the context. Prefix declarations found in Turtle input are parser conveniences; they do not decide the JavaScript property names exposed by OLDM.
21
+
22
+ When multiple prefixes point at the same namespace, client-provided prefixes are preferred over OLDM defaults, and defaults are preferred over prefixes found in a parsed source document. For example, both `pim:` and `space:` are common aliases for `http://www.w3.org/ns/pim/space#`. Since OLDM prefers `space` for that namespace, profile data using either `pim:storage` or `space:storage` is exposed as `space$storage` in JavaScript.
23
+
24
+ ```javascript
25
+ const context = oldm({
26
+ parser: n3Parser,
27
+ prefixes: {
28
+ space: 'http://www.w3.org/ns/pim/space#'
29
+ }
30
+ })
31
+
32
+ const profile = context.parse(turtle, profileUrl, 'text/turtle')
33
+ const me = profile.subjects[`${profileUrl}#me`]
34
+
35
+ console.log(me.space$storage.id)
36
+ ```
37
+
38
+
39
+ ## Multiple graphs in one context
40
+
41
+ `Context` keeps a registry of parsed graphs and exposes a combined view over all graphs loaded into the same context.
42
+
43
+ ```javascript
44
+ const profile = context.parse(profileTurtle, profileUrl, 'text/turtle')
45
+ const settings = context.parse(settingsTurtle, settingsUrl, 'text/turtle')
46
+
47
+ profile.get(`${profileUrl}#me`) // graph-specific view
48
+ context.get(`${profileUrl}#me`) // combined context view
49
+ context.graphs // parsed graphs in load order
50
+ context.graph(profileUrl) // graph by source URL
51
+ context.data // combined subjects
52
+ context.subjects // combined subject map
53
+ context.sources(context.get(`${profileUrl}#me`))
54
+ // graphs containing that subject
55
+ context.sources(context.get(`${profileUrl}#me`), 'vcard$fn')
56
+ // graphs containing that property
57
+ context.sources(context.get(`${profileUrl}#me`), 'vcard$fn', 'Auke')
58
+ // graphs containing that specific value
59
+ profile.context.data // same combined view, starting from a graph
60
+ profile.context.subjects // same combined subject map, starting from a graph
61
+ ```
62
+
63
+ The combined context view merges named subjects by IRI. Graph-specific views remain unchanged, so code can still separate data by original resource. Blank nodes remain graph-scoped.
64
+
65
+ If a loader or middleware gives you a `Graph`, use `graph.context` to access the combined view for all graphs loaded into the same context:
66
+
67
+ ```javascript
68
+ const graph = context.parse(profileTurtle, profileUrl, 'text/turtle')
69
+
70
+ graph.data // subjects from this one resource
71
+ graph.context.data // combined subjects from the whole context
72
+ graph.context.get(id) // merged subject from the whole context
73
+ ```
74
+
75
+ For source-aware writes, use the graph-specific helpers when you know the resource you want to edit:
76
+
77
+ ```javascript
78
+ profile.set(`${profileUrl}#me`, 'vcard$fn', 'Auke')
79
+ profile.add(`${profileUrl}#me`, 'schema$knowsAbout', 'Linked Data')
80
+ profile.delete(`${profileUrl}#me`, 'schema$knowsAbout', 'Old value')
81
+ ```
82
+
83
+ Context-level helpers can choose a graph explicitly:
84
+
85
+ ```javascript
86
+ context.set(`${profileUrl}#me`, 'vcard$fn', 'Auke', { graph: profile })
87
+ context.add(`${profileUrl}#me`, 'schema$knowsAbout', 'Solid', { graph: profileUrl })
88
+ ```
89
+
90
+ When no graph is passed, `context.set/add/delete()` uses a conservative default: the subject's exact graph URL, the subject document URL without a fragment, the only graph that currently contains the subject, the configured `defaultGraph`, or the only graph in the context. Direct property assignment on a named subject from `context.get(...)` uses the same resolver, so simple edits can stay object-like:
91
+
92
+ ```javascript
93
+ const me = context.get(`${profileUrl}#me`)
94
+ me.vcard$fn = 'Auke'
95
+ delete me.vcard$nickname
96
+ ```
97
+
98
+ If there is no obvious source graph, OLDM throws and asks you to choose one explicitly with `context.set/add/delete(..., { graph })` or `graph.set/add/delete(...)`.
99
+
100
+ ## Public exports
101
+
102
+ - default `oldm(options)` context factory
103
+ - `Context`
104
+ - `Graph`
105
+ - `NamedNode`
106
+ - `BlankNode`
107
+ - `Collection`
108
+ - `one(values, whichOne)`
109
+ - `many(values)`
110
+ - `first(...values)`
111
+ - `prefixes`
112
+ - `rdfType`
113
+
114
+ ## License
115
+
116
+ MIT.
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@muze-nl/oldm-core",
3
+ "version": "0.6.0",
4
+ "description": "Core Object - Linked Data Mapper, without parser or writer dependencies",
5
+ "type": "module",
6
+ "main": "src/oldm.mjs",
7
+ "exports": {
8
+ ".": "./src/oldm.mjs"
9
+ },
10
+ "sideEffects": false,
11
+ "scripts": {
12
+ "test": "tap --disable-coverage test/*.mjs",
13
+ "coverage": "tap --allow-incomplete-coverage --show-full-coverage test/*.mjs"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/muze-nl/oldm.git",
18
+ "directory": "packages/oldm-core"
19
+ },
20
+ "author": "auke@muze.nl",
21
+ "license": "MIT",
22
+ "files": [
23
+ "src/",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20.0.0"
29
+ }
30
+ }
package/src/oldm.mjs ADDED
@@ -0,0 +1,961 @@
1
+ export default function oldm(options)
2
+ {
3
+ return new Context(options)
4
+ }
5
+
6
+ export const rdfType = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
7
+
8
+ export const prefixes = {
9
+ acl: 'http://www.w3.org/ns/auth/acl#',
10
+ acp: 'http://www.w3.org/ns/solid/acp#',
11
+ dcterms:'http://purl.org/dc/terms/',
12
+ foaf: 'http://xmlns.com/foaf/0.1/',
13
+ ldn: 'https://www.w3.org/ns/ldn#',
14
+ ldp: 'http://www.w3.org/ns/ldp#',
15
+ notify: 'http://www.w3.org/ns/solid/notifications#',
16
+ oidc: 'http://www.w3.org/ns/solid/oidc#',
17
+ owl: 'http://www.w3.org/2002/07/owl#',
18
+ pim: 'http://www.w3.org/ns/pim/space#',
19
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
20
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
21
+ schema: 'http://schema.org/',
22
+ solid: 'http://www.w3.org/ns/solid/terms#',
23
+ stat: 'http://www.w3.org/ns/posix/stat#',
24
+ turtle: 'http://www.w3.org/ns/iana/media-types/text/turtle#',
25
+ vcard: 'http://www.w3.org/2006/vcard/ns#',
26
+ xsd: 'http://www.w3.org/2001/XMLSchema#'
27
+ }
28
+
29
+ export function one(values, whichOne='last')
30
+ {
31
+ let result = values
32
+ if (Array.isArray(values)) {
33
+ if (whichOne=='last') {
34
+ result = values[values.length-1]
35
+ } else if (whichOne=='first') {
36
+ result = values[0]
37
+ } else if (typeof whichOne=='function') {
38
+ result = whichOne(values)
39
+ } else {
40
+ throw new Error('Unknown value for whichOne parameter')
41
+ }
42
+ }
43
+ return result
44
+ }
45
+
46
+ export function many(values)
47
+ {
48
+ if (Array.isArray(values)) {
49
+ return values
50
+ }
51
+ if (values == null) {
52
+ return []
53
+ }
54
+ return [values]
55
+ }
56
+
57
+ export function first(...values)
58
+ {
59
+ for (const value of values) {
60
+ if (value!==null && value!==undefined) {
61
+ return value
62
+ }
63
+ }
64
+ return null
65
+ }
66
+
67
+ function values(value)
68
+ {
69
+ if (Array.isArray(value) && !(value instanceof Collection)) {
70
+ return value
71
+ }
72
+ if (value === undefined) {
73
+ return []
74
+ }
75
+ return [value]
76
+ }
77
+
78
+ function mergeValue(existing, value)
79
+ {
80
+ const result = values(existing)
81
+ for (const item of values(value)) {
82
+ if (!result.some(existingItem => sameValue(existingItem, item))) {
83
+ result.push(item)
84
+ }
85
+ }
86
+ if (result.length == 0) {
87
+ return undefined
88
+ }
89
+ if (result.length == 1) {
90
+ return result[0]
91
+ }
92
+ return result
93
+ }
94
+
95
+ function sameValue(left, right)
96
+ {
97
+ if (left === right) {
98
+ return true
99
+ }
100
+ if (left instanceof NamedNode && right instanceof NamedNode) {
101
+ return left.id == right.id
102
+ }
103
+ if (left instanceof NamedNode && typeof right == 'string') {
104
+ return left.id == right
105
+ }
106
+ if (typeof left == 'string' && right instanceof NamedNode) {
107
+ return left == right.id
108
+ }
109
+ if (left instanceof Collection && right instanceof Collection) {
110
+ return left.length == right.length
111
+ && left.every((item, index) => sameValue(item, right[index]))
112
+ }
113
+ if (isLiteral(left) && isLiteral(right)) {
114
+ return String(left) == String(right)
115
+ && left?.type == right?.type
116
+ && left?.language == right?.language
117
+ }
118
+ return false
119
+ }
120
+
121
+
122
+ function sameSourceValue(left, right)
123
+ {
124
+ if (left === right) {
125
+ return true
126
+ }
127
+ if (left instanceof NamedNode && right instanceof NamedNode) {
128
+ return left.id == right.id
129
+ }
130
+ if (left instanceof NamedNode && typeof right == 'string') {
131
+ return left.id == right
132
+ }
133
+ if (typeof left == 'string' && right instanceof NamedNode) {
134
+ return left == right.id
135
+ }
136
+ if (left instanceof Collection && right instanceof Collection) {
137
+ return left.length == right.length
138
+ && left.every((item, index) => sameSourceValue(item, right[index]))
139
+ }
140
+ if (isLiteral(left) && isLiteral(right)) {
141
+ const leftType = left?.type
142
+ const rightType = right?.type
143
+ const leftLanguage = left?.language
144
+ const rightLanguage = right?.language
145
+ return String(left) == String(right)
146
+ && (!leftType || !rightType || leftType == rightType)
147
+ && (!leftLanguage || !rightLanguage || leftLanguage == rightLanguage)
148
+ }
149
+ return false
150
+ }
151
+
152
+ function resolveValue(value, subjects, context)
153
+ {
154
+ if (value instanceof Collection) {
155
+ const collection = new Collection(context)
156
+ for (const item of value) {
157
+ collection.push(resolveValue(item, subjects, context))
158
+ }
159
+ return collection
160
+ }
161
+ if (Array.isArray(value)) {
162
+ return value.map(item => resolveValue(item, subjects, context))
163
+ }
164
+ if (value instanceof NamedNode && subjects[value.id]) {
165
+ return subjects[value.id]
166
+ }
167
+ return value
168
+ }
169
+
170
+ function isLiteral(value)
171
+ {
172
+ return (
173
+ value instanceof String
174
+ || value instanceof Number
175
+ || typeof value == 'boolean'
176
+ || typeof value == 'string'
177
+ || typeof value == 'number'
178
+ )
179
+ }
180
+
181
+ export class Context {
182
+ #buildingSubjects = false
183
+
184
+ constructor(options)
185
+ {
186
+ const clientPrefixes = options?.prefixes ?? {}
187
+ this.prefixes = {...prefixes, ...clientPrefixes}
188
+ this.prefixOrder = [
189
+ ...Object.keys(clientPrefixes),
190
+ ...Object.keys(prefixes).filter(prefix => !(prefix in clientPrefixes))
191
+ ]
192
+ if (!this.prefixes['xsd']) {
193
+ this.prefixes['xsd'] = 'http://www.w3.org/2001/XMLSchema#'
194
+ this.prefixOrder.push('xsd')
195
+ }
196
+ this.parser = options?.parser
197
+ this.writer = options?.writer
198
+ this.graphs = []
199
+ this.graphsByUrl = Object.create(null)
200
+ this.defaultGraph = options?.defaultGraph ?? null
201
+ this.separator = options?.separator ?? '$'
202
+
203
+ Object.defineProperty(this, 'subjects', {
204
+ get() {
205
+ return this.getSubjects()
206
+ }
207
+ })
208
+
209
+ Object.defineProperty(this, 'data', {
210
+ get() {
211
+ return Object.values(this.subjects)
212
+ }
213
+ })
214
+ }
215
+
216
+ parse(input, url, type)
217
+ {
218
+ const {quads, prefixes} = this.parser(input, url, type)
219
+ if (prefixes) {
220
+ for (let prefix in prefixes) {
221
+ let prefixURL = prefixes[prefix]
222
+ if (prefixURL.match(/^http(s?):\/\/$/i)) {
223
+ prefixURL += url.substring(prefixURL.length)
224
+ } else try {
225
+ prefixURL = new URL(prefixes[prefix], url).href
226
+ } catch(err) {
227
+ console.error('Could not parse prefix', prefixes[prefix], err.message)
228
+ }
229
+
230
+ if (!this.prefixes[prefix]) {
231
+ this.prefixes[prefix] = prefixURL
232
+ this.prefixOrder.push(prefix)
233
+ }
234
+ }
235
+ }
236
+ return this.addGraph(new Graph(quads, url, type, prefixes, this))
237
+ }
238
+
239
+ addGraph(graph)
240
+ {
241
+ if (!graph?.url) {
242
+ throw new Error('Cannot add graph without a url')
243
+ }
244
+
245
+ const existing = this.graphsByUrl[graph.url]
246
+ if (existing) {
247
+ const index = this.graphs.indexOf(existing)
248
+ if (index >= 0) {
249
+ this.graphs[index] = graph
250
+ }
251
+ } else {
252
+ this.graphs.push(graph)
253
+ }
254
+ this.graphsByUrl[graph.url] = graph
255
+ return graph
256
+ }
257
+
258
+ graph(url)
259
+ {
260
+ return this.graphsByUrl[this.fullURI(url)]
261
+ }
262
+
263
+ set(subject, predicate, value, options={})
264
+ {
265
+ return this.resolveGraph(subject, options).set(subject, predicate, value)
266
+ }
267
+
268
+ add(subject, predicate, value, options={})
269
+ {
270
+ return this.resolveGraph(subject, options).add(subject, predicate, value)
271
+ }
272
+
273
+ delete(subject, predicate=null, value=undefined, options={})
274
+ {
275
+ const graph = this.resolveGraph(subject, options)
276
+ if (arguments.length < 3) {
277
+ return graph.delete(subject, predicate)
278
+ }
279
+ return graph.delete(subject, predicate, value)
280
+ }
281
+
282
+ resolveGraph(subject, options={})
283
+ {
284
+ if (options.graph) {
285
+ return this.getGraphOption(options.graph)
286
+ }
287
+
288
+ if (subject instanceof BlankNode && subject.graph instanceof Graph) {
289
+ return subject.graph
290
+ }
291
+
292
+ const id = this.subjectID(subject)
293
+ if (id) {
294
+ const exactGraph = this.graphsByUrl[id]
295
+ if (exactGraph) {
296
+ return exactGraph
297
+ }
298
+
299
+ const documentGraph = this.graphsByUrl[this.documentURL(id)]
300
+ if (documentGraph) {
301
+ return documentGraph
302
+ }
303
+
304
+ const subjectSources = this.graphs.filter(graph => graph.subjects[id])
305
+ if (subjectSources.length == 1) {
306
+ return subjectSources[0]
307
+ }
308
+ if (subjectSources.length > 1) {
309
+ throw new Error(`Cannot choose a source graph for ${id}. Use context.set/add/delete(..., { graph }) or graph.set/add/delete(...) to choose one explicitly.`)
310
+ }
311
+ }
312
+
313
+ if (this.defaultGraph) {
314
+ return this.getGraphOption(this.defaultGraph)
315
+ }
316
+
317
+ if (this.graphs.length == 1) {
318
+ return this.graphs[0]
319
+ }
320
+
321
+ throw new Error('Cannot choose a source graph. Use context.set/add/delete(..., { graph }) or graph.set/add/delete(...) to choose one explicitly.')
322
+ }
323
+
324
+ getGraphOption(graph)
325
+ {
326
+ if (graph instanceof Graph) {
327
+ if (!this.graphs.includes(graph)) {
328
+ throw new Error('The selected graph is not part of this context')
329
+ }
330
+ return graph
331
+ }
332
+
333
+ const resolved = this.graph(graph)
334
+ if (!resolved) {
335
+ throw new Error(`Unknown graph: ${graph}`)
336
+ }
337
+ return resolved
338
+ }
339
+
340
+ documentURL(id)
341
+ {
342
+ try {
343
+ const url = new URL(id)
344
+ url.hash = ''
345
+ return url.href
346
+ } catch(err) {
347
+ return id
348
+ }
349
+ }
350
+
351
+ sources(subject, predicate=null, value=undefined)
352
+ {
353
+ if (!subject) {
354
+ return [...this.graphs]
355
+ }
356
+
357
+ if (subject instanceof BlankNode && !(subject instanceof NamedNode)) {
358
+ return this.sourcesForBlankNode(subject, predicate, value, arguments.length >= 3)
359
+ }
360
+
361
+ const id = this.subjectID(subject)
362
+ if (!id) {
363
+ return []
364
+ }
365
+
366
+ return this.graphs.filter(graph => {
367
+ const graphSubject = graph.subjects[id]
368
+ return graphSubject
369
+ && this.subjectHasSource(graphSubject, predicate, value, arguments.length >= 3)
370
+ })
371
+ }
372
+
373
+ sourcesForBlankNode(subject, predicate, value, hasValue)
374
+ {
375
+ const graph = subject.graph
376
+ if (!(graph instanceof Graph)) {
377
+ return []
378
+ }
379
+ if (this.subjectHasSource(subject, predicate, value, hasValue)) {
380
+ return [graph]
381
+ }
382
+ return []
383
+ }
384
+
385
+ subjectHasSource(subject, predicate, value, hasValue)
386
+ {
387
+ if (!predicate) {
388
+ return true
389
+ }
390
+
391
+ const property = this.propertyName(predicate)
392
+ if (!(property in subject)) {
393
+ return false
394
+ }
395
+ if (!hasValue) {
396
+ return true
397
+ }
398
+
399
+ return values(subject[property]).some(item => sameSourceValue(item, value))
400
+ }
401
+
402
+ subjectID(subject)
403
+ {
404
+ if (subject?.id) {
405
+ return this.fullURI(subject.id)
406
+ }
407
+ if (typeof subject == 'string') {
408
+ return this.fullURI(subject)
409
+ }
410
+ return null
411
+ }
412
+
413
+ propertyName(predicate)
414
+ {
415
+ if (predicate?.id) {
416
+ predicate = predicate.id
417
+ }
418
+ if (predicate == 'a' || predicate == rdfType || this.fullURI(predicate) == rdfType) {
419
+ return 'a'
420
+ }
421
+ return this.shortURI(this.fullURI(predicate))
422
+ }
423
+
424
+ get(shortID)
425
+ {
426
+ return this.subjects[this.fullURI(shortID)]
427
+ }
428
+
429
+ getSubjects()
430
+ {
431
+ const subjects = Object.create(null)
432
+
433
+ this.#buildingSubjects = true
434
+ try {
435
+ for (const graph of this.graphs) {
436
+ for (const id of Object.keys(graph.subjects)) {
437
+ if (!subjects[id]) {
438
+ subjects[id] = this.contextSubject(new NamedNode(id, this))
439
+ }
440
+ }
441
+ }
442
+
443
+ for (const graph of this.graphs) {
444
+ for (const [id, subject] of Object.entries(graph.subjects)) {
445
+ this.mergeSubject(subjects[id], subject, subjects)
446
+ }
447
+ }
448
+ } finally {
449
+ this.#buildingSubjects = false
450
+ }
451
+
452
+ return subjects
453
+ }
454
+
455
+ mergeSubject(target, source, subjects)
456
+ {
457
+ for (const [predicate, value] of Object.entries(source)) {
458
+ if (predicate == 'id') {
459
+ continue
460
+ }
461
+ target[predicate] = mergeValue(
462
+ target[predicate],
463
+ resolveValue(value, subjects, this)
464
+ )
465
+ }
466
+ }
467
+
468
+ contextSubject(subject)
469
+ {
470
+ const context = this
471
+ return new Proxy(subject, {
472
+ set(target, property, value, receiver) {
473
+ if (context.#buildingSubjects || typeof property == 'symbol' || property == 'id' || property == 'graph') {
474
+ return Reflect.set(target, property, value, receiver)
475
+ }
476
+
477
+ context.set(target.id, property, value)
478
+ context.updateContextProperty(target, property)
479
+ return true
480
+ },
481
+
482
+ deleteProperty(target, property) {
483
+ if (context.#buildingSubjects || typeof property == 'symbol' || property == 'id' || property == 'graph') {
484
+ return Reflect.deleteProperty(target, property)
485
+ }
486
+
487
+ context.delete(target.id, property)
488
+ context.updateContextProperty(target, property)
489
+ return true
490
+ }
491
+ })
492
+ }
493
+
494
+ updateContextProperty(target, property)
495
+ {
496
+ const updated = this.get(target.id)
497
+ if (updated && property in updated) {
498
+ target[property] = updated[property]
499
+ } else {
500
+ delete target[property]
501
+ }
502
+ }
503
+
504
+ fullURI(shortURI, separator=null)
505
+ {
506
+ if (!separator) {
507
+ separator = this.separator
508
+ }
509
+ const [prefix, path] = shortURI.split(separator)
510
+ if (path && this.prefixes[prefix]) {
511
+ return this.prefixes[prefix]+path
512
+ }
513
+ return shortURI
514
+ }
515
+
516
+ shortURI(fullURI, separator=null)
517
+ {
518
+ if (!separator) {
519
+ separator = this.separator
520
+ }
521
+ for (const prefix of this.prefixOrder) {
522
+ if (fullURI.startsWith(this.prefixes[prefix])) {
523
+ return prefix + separator + fullURI.substring(this.prefixes[prefix].length)
524
+ }
525
+ }
526
+ return fullURI
527
+ }
528
+
529
+ setType(literal, shortType)
530
+ {
531
+ if (!shortType) {
532
+ return literal
533
+ }
534
+ if (typeof literal == 'string') {
535
+ literal = new String(literal)
536
+ } else if (typeof literal == 'number') {
537
+ literal = new Number(literal)
538
+ }
539
+ if (typeof literal !== 'object') {
540
+ throw new Error('cannot set type on ',literal,shortType)
541
+ }
542
+ literal.type = shortType
543
+ return literal
544
+ }
545
+
546
+ getType(literal)
547
+ {
548
+ if (literal && typeof literal == 'object') {
549
+ return literal.type
550
+ }
551
+ return null
552
+ }
553
+ }
554
+
555
+ export class Graph
556
+ {
557
+ #blankNodes = Object.create(null)
558
+
559
+ constructor(quads, url, mimetype, prefixes, context)
560
+ {
561
+ this.mimetype = mimetype
562
+ this.url = url
563
+ this.prefixes = prefixes
564
+ this.context = context
565
+ this.subjects = Object.create(null)
566
+ for (let quad of quads) {
567
+ let subject
568
+ if (quad.subject.termType=='BlankNode') {
569
+ let shortPred = this.shortURI(quad.predicate.id,':')
570
+ let shortObj
571
+ switch(shortPred) {
572
+ case 'rdf:first':
573
+ subject = this.addCollection(quad.subject.id)
574
+ shortObj = quad.object.id ? this.shortURI(quad.object.id, ':') : null
575
+ if (shortObj!='rdf:nil') {
576
+ const value = this.getValue(quad.object)
577
+ if (value) {
578
+ subject.push(value)
579
+ }
580
+ }
581
+ continue
582
+ case 'rdf:rest':
583
+ this.#blankNodes[quad.object.id] = this.#blankNodes[quad.subject.id]
584
+ continue
585
+ default:
586
+ subject = this.addBlankNode(quad.subject.id)
587
+ break
588
+ }
589
+ } else {
590
+ subject = this.addNamedNode(quad.subject.id)
591
+ }
592
+ subject.addPredicate(quad.predicate.id, quad.object)
593
+ }
594
+ if (this.subjects[url]) {
595
+ this.primary = this.subjects[url]
596
+ } else {
597
+ this.primary = null
598
+ }
599
+ Object.defineProperty(this, 'data', {
600
+ get() {
601
+ return Object.values(this.subjects)
602
+ }
603
+ })
604
+ }
605
+
606
+ addNamedNode(uri)
607
+ {
608
+ // make sure any relative uri subject ids are fully qualified
609
+ let absURI = new URL(uri, this.url).href
610
+ if (!this.subjects[absURI]) {
611
+ this.subjects[absURI] = new NamedNode(absURI, this)
612
+ }
613
+ return this.subjects[absURI]
614
+ }
615
+
616
+ addBlankNode(id)
617
+ {
618
+ if (!this.#blankNodes[id]) {
619
+ this.#blankNodes[id] = new BlankNode(this)
620
+ }
621
+ return this.#blankNodes[id]
622
+ }
623
+
624
+ addCollection(id)
625
+ {
626
+ if (!this.#blankNodes[id]) {
627
+ this.#blankNodes[id] = new Collection(this)
628
+ }
629
+ return this.#blankNodes[id]
630
+ }
631
+
632
+ write()
633
+ {
634
+ return this.context.writer(this)
635
+ }
636
+
637
+ get(shortID)
638
+ {
639
+ return this.subjects[this.fullURI(shortID)]
640
+ }
641
+
642
+ set(subject, predicate, value)
643
+ {
644
+ const node = this.ensureSubject(subject)
645
+ const property = this.context.propertyName(predicate)
646
+
647
+ if (property == 'a') {
648
+ node.a = this.normalizeTypeValues(value)
649
+ } else {
650
+ node[property] = this.normalizeValues(value)
651
+ }
652
+ return node
653
+ }
654
+
655
+ add(subject, predicate, value)
656
+ {
657
+ const node = this.ensureSubject(subject)
658
+ const property = this.context.propertyName(predicate)
659
+ const newValue = property == 'a'
660
+ ? this.normalizeTypeValues(value)
661
+ : this.normalizeValues(value)
662
+
663
+ node[property] = mergeValue(node[property], newValue)
664
+ return node
665
+ }
666
+
667
+ delete(subject, predicate=null, value=undefined)
668
+ {
669
+ const node = this.findSubject(subject)
670
+ if (!node) {
671
+ return false
672
+ }
673
+
674
+ if (!predicate) {
675
+ if (node.id) {
676
+ delete this.subjects[node.id]
677
+ if (this.primary === node) {
678
+ this.primary = null
679
+ }
680
+ }
681
+ return true
682
+ }
683
+
684
+ const property = this.context.propertyName(predicate)
685
+ if (!(property in node)) {
686
+ return false
687
+ }
688
+
689
+ if (arguments.length < 3) {
690
+ delete node[property]
691
+ return true
692
+ }
693
+
694
+ const deleteValues = property == 'a'
695
+ ? values(this.normalizeTypeValues(value))
696
+ : values(this.normalizeValues(value))
697
+ const remaining = values(node[property])
698
+ .filter(item => !deleteValues.some(deleteValue => sameValue(item, deleteValue)))
699
+
700
+ if (remaining.length == values(node[property]).length) {
701
+ return false
702
+ }
703
+ if (remaining.length == 0) {
704
+ delete node[property]
705
+ } else if (remaining.length == 1) {
706
+ node[property] = remaining[0]
707
+ } else {
708
+ node[property] = remaining
709
+ }
710
+ return true
711
+ }
712
+
713
+ ensureSubject(subject)
714
+ {
715
+ if (subject instanceof BlankNode && !(subject instanceof NamedNode)) {
716
+ if (subject.graph !== this) {
717
+ throw new Error('Cannot write a blank node into a different graph')
718
+ }
719
+ return subject
720
+ }
721
+
722
+ if (subject instanceof NamedNode) {
723
+ return this.addNamedNode(subject.id)
724
+ }
725
+
726
+ return this.addNamedNode(this.fullURI(subject))
727
+ }
728
+
729
+ findSubject(subject)
730
+ {
731
+ if (subject instanceof BlankNode && !(subject instanceof NamedNode)) {
732
+ return subject.graph === this ? subject : null
733
+ }
734
+ const id = subject?.id ? subject.id : this.fullURI(subject)
735
+ return this.subjects[id]
736
+ }
737
+
738
+ normalizeValues(value)
739
+ {
740
+ if (Array.isArray(value) && !(value instanceof Collection)) {
741
+ return value.map(item => this.normalizeValue(item))
742
+ }
743
+ return this.normalizeValue(value)
744
+ }
745
+
746
+ normalizeValue(value)
747
+ {
748
+ if (value instanceof Collection) {
749
+ const collection = new Collection(this)
750
+ for (const item of value) {
751
+ collection.push(this.normalizeValue(item))
752
+ }
753
+ return collection
754
+ }
755
+ if (value instanceof NamedNode) {
756
+ return this.addNamedNode(value.id)
757
+ }
758
+ if (value instanceof BlankNode) {
759
+ if (value.graph !== this) {
760
+ throw new Error('Cannot write a blank node into a different graph')
761
+ }
762
+ return value
763
+ }
764
+ if (this.looksLikeURI(value)) {
765
+ return this.addNamedNode(this.fullURI(value))
766
+ }
767
+ return value
768
+ }
769
+
770
+ normalizeTypeValues(value)
771
+ {
772
+ if (Array.isArray(value) && !(value instanceof Collection)) {
773
+ return value.map(item => this.normalizeTypeValue(item))
774
+ }
775
+ return this.normalizeTypeValue(value)
776
+ }
777
+
778
+ normalizeTypeValue(value)
779
+ {
780
+ if (value instanceof NamedNode) {
781
+ return this.shortURI(value.id)
782
+ }
783
+ return this.shortURI(this.fullURI(value))
784
+ }
785
+
786
+ looksLikeURI(value)
787
+ {
788
+ if (typeof value != 'string') {
789
+ return false
790
+ }
791
+ if (/^[a-z][a-z0-9+.-]*:/i.test(value)) {
792
+ return true
793
+ }
794
+ const [prefix, path] = value.split(this.context.separator)
795
+ return Boolean(path && this.context.prefixes[prefix])
796
+ }
797
+
798
+ fullURI(shortURI, separator=null)
799
+ {
800
+ if (!separator) {
801
+ separator = this.context.separator
802
+ }
803
+ const [prefix, path] = shortURI.split(separator)
804
+ if (path) {
805
+ if (this.context.prefixes[prefix]) {
806
+ return this.context.prefixes[prefix]+path
807
+ }
808
+ if (this.prefixes[prefix]) {
809
+ return this.prefixes[prefix]+path
810
+ }
811
+ }
812
+ return shortURI
813
+ }
814
+
815
+ shortURI(fullURI, separator=null)
816
+ {
817
+ if (!separator) {
818
+ separator = this.context.separator
819
+ }
820
+ for (const prefix of this.context.prefixOrder) {
821
+ if (fullURI.startsWith(this.context.prefixes[prefix])) {
822
+ return prefix + separator + fullURI.substring(this.context.prefixes[prefix].length)
823
+ }
824
+ }
825
+ if (this.url && fullURI.startsWith(this.url)) {
826
+ return fullURI.substring(this.url.length)
827
+ }
828
+ return fullURI
829
+ }
830
+
831
+ /**
832
+ * This sets the type of a literal, usually one of the xsd types
833
+ */
834
+ setType(literal, type)
835
+ {
836
+ const shortType = this.shortURI(type)
837
+ return this.context.setType(literal, shortType)
838
+ }
839
+
840
+ /**
841
+ * This returns the type of a literal, or null
842
+ */
843
+ getType(literal)
844
+ {
845
+ return this.context.getType(literal)
846
+ }
847
+
848
+ setLanguage(literal, language)
849
+ {
850
+ if (typeof literal == 'string') {
851
+ literal = new String(literal)
852
+ } else if (typeof literal == 'number') {
853
+ literal = new Number(literal)
854
+ }
855
+ if (typeof literal !== 'object') {
856
+ throw new Error('cannot set language on ',literal)
857
+ }
858
+ literal.language = language
859
+ return literal
860
+ }
861
+
862
+ getValue(object)
863
+ {
864
+ let result
865
+ if (object.termType=='Literal') {
866
+ result = object.value
867
+ let datatype = object.datatype?.id
868
+ if (datatype) {
869
+ result = this.setType(result, datatype)
870
+ }
871
+ let language = object.language
872
+ if (language) {
873
+ result = this.setLanguage(result, language)
874
+ }
875
+ } else if (object.termType=='BlankNode') {
876
+ result = this.addBlankNode(object.id)
877
+ } else {
878
+ result = this.addNamedNode(object.id)
879
+ }
880
+ return result
881
+ }
882
+
883
+
884
+ }
885
+
886
+ export class BlankNode
887
+ {
888
+
889
+ constructor(graph)
890
+ {
891
+ Object.defineProperty(this, 'graph', {
892
+ value: graph,
893
+ writable: false,
894
+ enumerable: false
895
+ })
896
+ }
897
+
898
+ addPredicate(predicate, object)
899
+ {
900
+ if (predicate.id) {
901
+ predicate = predicate.id
902
+ }
903
+ if (predicate==rdfType) {
904
+ let type = this.graph.shortURI(object.id)
905
+ this.addType(type)
906
+ } else {
907
+ const value = this.graph.getValue(object)
908
+ predicate = this.graph.shortURI(predicate)
909
+ if (!this[predicate]) {
910
+ this[predicate] = value
911
+ } else if (Array.isArray(this[predicate])) {
912
+ this[predicate].push(value)
913
+ } else {
914
+ this[predicate] = [ this[predicate], value]
915
+ }
916
+ }
917
+ }
918
+
919
+ /**
920
+ * Adds a rdfType value, stored in this.a
921
+ * Subjects can have more than one type (or class), unlike literals
922
+ * The type value can be any URI, xsdTypes are unexpected here
923
+ */
924
+ addType(type)
925
+ {
926
+ if (!this.a) {
927
+ this.a = type
928
+ } else {
929
+ if (!Array.isArray(this.a)) {
930
+ this.a = [ this.a ]
931
+ }
932
+ this.a.push(type)
933
+ }
934
+ }
935
+ }
936
+
937
+ export class NamedNode extends BlankNode
938
+ {
939
+ constructor(id, graph)
940
+ {
941
+ super(graph)
942
+ Object.defineProperty(this, 'id', {
943
+ value: id,
944
+ writable: false,
945
+ enumerable: true
946
+ })
947
+ }
948
+ }
949
+
950
+ export class Collection extends Array
951
+ {
952
+ constructor(graph)
953
+ {
954
+ super()
955
+ Object.defineProperty(this, 'graph', {
956
+ value: graph,
957
+ writable: false,
958
+ enumerable: false
959
+ })
960
+ }
961
+ }