@muze-nl/oldm-core 0.6.0 → 0.7.1

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 (3) hide show
  1. package/README.md +9 -2
  2. package/package.json +1 -1
  3. package/src/oldm.mjs +133 -56
package/README.md CHANGED
@@ -6,11 +6,12 @@ This package contains the object/graph mapping layer only. It has explicit ESM e
6
6
 
7
7
  ```javascript
8
8
  import oldm, { one, many, first, Collection } from '@muze-nl/oldm-core'
9
- import { n3Parser, n3Writer } from '@muze-nl/oldm-n3'
9
+ import { n3Parser, n3PatchWriter, n3Writer } from '@muze-nl/oldm-n3'
10
10
 
11
11
  const context = oldm({
12
12
  parser: n3Parser,
13
- writer: n3Writer
13
+ writer: n3Writer,
14
+ patchWriter: n3PatchWriter
14
15
  })
15
16
  ```
16
17
 
@@ -97,6 +98,12 @@ delete me.vcard$nickname
97
98
 
98
99
  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
 
101
+ ## PATCH output
102
+
103
+ `Graph#patch()` delegates to the configured `patchWriter`, just like `Graph#write()` delegates to `writer`. Core stays parser/writer agnostic; use `n3PatchWriter` from `@muze-nl/oldm-n3` when you want Solid N3 Patch output.
104
+
105
+ Patch writers can support owned anonymous values conservatively: fresh blank nodes and RDF collections can be inserted, while changed or deleted existing anonymous values are replaced as complete closures. Shared or cyclic anonymous values should fall back to full document replacement.
106
+
100
107
  ## Public exports
101
108
 
102
109
  - default `oldm(options)` context factory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muze-nl/oldm-core",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Core Object - Linked Data Mapper, without parser or writer dependencies",
5
5
  "type": "module",
6
6
  "main": "src/oldm.mjs",
package/src/oldm.mjs CHANGED
@@ -195,6 +195,7 @@ export class Context {
195
195
  }
196
196
  this.parser = options?.parser
197
197
  this.writer = options?.writer
198
+ this.patchWriter = options?.patchWriter
198
199
  this.graphs = []
199
200
  this.graphsByUrl = Object.create(null)
200
201
  this.defaultGraph = options?.defaultGraph ?? null
@@ -233,7 +234,7 @@ export class Context {
233
234
  }
234
235
  }
235
236
  }
236
- return this.addGraph(new Graph(quads, url, type, prefixes, this))
237
+ return this.addGraph(new Graph(quads, url, type, prefixes, this, input))
237
238
  }
238
239
 
239
240
  addGraph(graph)
@@ -262,21 +263,21 @@ export class Context {
262
263
 
263
264
  set(subject, predicate, value, options={})
264
265
  {
265
- return this.resolveGraph(subject, options).set(subject, predicate, value)
266
+ return this.resolveGraph(subject, options).set(subject, predicate, value, {prefixPreference: 'context'})
266
267
  }
267
268
 
268
269
  add(subject, predicate, value, options={})
269
270
  {
270
- return this.resolveGraph(subject, options).add(subject, predicate, value)
271
+ return this.resolveGraph(subject, options).add(subject, predicate, value, {prefixPreference: 'context'})
271
272
  }
272
273
 
273
274
  delete(subject, predicate=null, value=undefined, options={})
274
275
  {
275
276
  const graph = this.resolveGraph(subject, options)
276
277
  if (arguments.length < 3) {
277
- return graph.delete(subject, predicate)
278
+ return graph.delete(subject, predicate, undefined, {prefixPreference: 'context', hasValue: false})
278
279
  }
279
- return graph.delete(subject, predicate, value)
280
+ return graph.delete(subject, predicate, value, {prefixPreference: 'context', hasValue: true})
280
281
  }
281
282
 
282
283
  resolveGraph(subject, options={})
@@ -388,7 +389,9 @@ export class Context {
388
389
  return true
389
390
  }
390
391
 
391
- const property = this.propertyName(predicate)
392
+ const property = subject.graph instanceof Graph
393
+ ? subject.graph.propertyName(this.fullURI(predicate), 'context')
394
+ : this.propertyName(predicate)
392
395
  if (!(property in subject)) {
393
396
  return false
394
397
  }
@@ -458,8 +461,12 @@ export class Context {
458
461
  if (predicate == 'id') {
459
462
  continue
460
463
  }
461
- target[predicate] = mergeValue(
462
- target[predicate],
464
+
465
+ const contextPredicate = predicate == 'a'
466
+ ? 'a'
467
+ : this.propertyName(source.graph.fullURI(predicate, null, 'source'))
468
+ target[contextPredicate] = mergeValue(
469
+ target[contextPredicate],
463
470
  resolveValue(value, subjects, this)
464
471
  )
465
472
  }
@@ -556,12 +563,13 @@ export class Graph
556
563
  {
557
564
  #blankNodes = Object.create(null)
558
565
 
559
- constructor(quads, url, mimetype, prefixes, context)
566
+ constructor(quads, url, mimetype, prefixes, context, originalSource=null)
560
567
  {
561
568
  this.mimetype = mimetype
562
569
  this.url = url
563
570
  this.prefixes = prefixes
564
571
  this.context = context
572
+ this.originalSource = originalSource
565
573
  this.subjects = Object.create(null)
566
574
  for (let quad of quads) {
567
575
  let subject
@@ -634,39 +642,109 @@ export class Graph
634
642
  return this.context.writer(this)
635
643
  }
636
644
 
645
+ patch()
646
+ {
647
+ if (!this.context.patchWriter) {
648
+ throw new Error('Cannot generate a patch without a configured patchWriter')
649
+ }
650
+ return this.context.patchWriter(this)
651
+ }
652
+
637
653
  get(shortID)
638
654
  {
639
655
  return this.subjects[this.fullURI(shortID)]
640
656
  }
641
657
 
642
- set(subject, predicate, value)
658
+ prefixEntries(preference='source')
659
+ {
660
+ const sourcePrefixes = this.prefixes ?? {}
661
+ const sourceOrder = Object.keys(sourcePrefixes)
662
+ const contextPrefixes = this.context.prefixes ?? {}
663
+ const contextOrder = this.context.prefixOrder ?? Object.keys(contextPrefixes)
664
+ const entries = []
665
+ const seen = new Set()
666
+ const seenIRIs = new Set()
667
+ const add = (prefixes, order, skipKnownIRIs=false) => {
668
+ for (const prefix of order) {
669
+ if (!Object.prototype.hasOwnProperty.call(prefixes, prefix)) {
670
+ continue
671
+ }
672
+ const iri = prefixes[prefix]
673
+ if (!seen.has(prefix) && (!skipKnownIRIs || !seenIRIs.has(iri))) {
674
+ entries.push([prefix, iri])
675
+ seen.add(prefix)
676
+ seenIRIs.add(iri)
677
+ }
678
+ }
679
+ for (const prefix of Object.keys(prefixes)) {
680
+ const iri = prefixes[prefix]
681
+ if (!seen.has(prefix) && (!skipKnownIRIs || !seenIRIs.has(iri))) {
682
+ entries.push([prefix, iri])
683
+ seen.add(prefix)
684
+ seenIRIs.add(iri)
685
+ }
686
+ }
687
+ }
688
+
689
+ if (preference == 'context') {
690
+ add(contextPrefixes, contextOrder)
691
+ add(sourcePrefixes, sourceOrder, true)
692
+ } else {
693
+ add(sourcePrefixes, sourceOrder)
694
+ add(contextPrefixes, contextOrder, true)
695
+ }
696
+ return entries
697
+ }
698
+
699
+ prefixDeclarations(preference='source')
700
+ {
701
+ return Object.fromEntries(this.prefixEntries(preference))
702
+ }
703
+
704
+ propertyName(predicate, preference='source')
705
+ {
706
+ if (predicate?.id) {
707
+ predicate = predicate.id
708
+ }
709
+ const fullPredicate = this.fullURI(predicate, null, preference)
710
+ if (predicate == 'a' || fullPredicate == rdfType) {
711
+ return 'a'
712
+ }
713
+ return this.shortURI(fullPredicate, null, 'source')
714
+ }
715
+
716
+ set(subject, predicate, value, options={})
643
717
  {
644
- const node = this.ensureSubject(subject)
645
- const property = this.context.propertyName(predicate)
718
+ const preference = options.prefixPreference ?? 'source'
719
+ const node = this.ensureSubject(subject, preference)
720
+ const property = this.propertyName(predicate, preference)
646
721
 
647
722
  if (property == 'a') {
648
- node.a = this.normalizeTypeValues(value)
723
+ node.a = this.normalizeTypeValues(value, preference)
649
724
  } else {
650
- node[property] = this.normalizeValues(value)
725
+ node[property] = this.normalizeValues(value, preference)
651
726
  }
652
727
  return node
653
728
  }
654
729
 
655
- add(subject, predicate, value)
730
+ add(subject, predicate, value, options={})
656
731
  {
657
- const node = this.ensureSubject(subject)
658
- const property = this.context.propertyName(predicate)
732
+ const preference = options.prefixPreference ?? 'source'
733
+ const node = this.ensureSubject(subject, preference)
734
+ const property = this.propertyName(predicate, preference)
659
735
  const newValue = property == 'a'
660
- ? this.normalizeTypeValues(value)
661
- : this.normalizeValues(value)
736
+ ? this.normalizeTypeValues(value, preference)
737
+ : this.normalizeValues(value, preference)
662
738
 
663
739
  node[property] = mergeValue(node[property], newValue)
664
740
  return node
665
741
  }
666
742
 
667
- delete(subject, predicate=null, value=undefined)
743
+ delete(subject, predicate=null, value=undefined, options={})
668
744
  {
669
- const node = this.findSubject(subject)
745
+ const preference = options.prefixPreference ?? 'source'
746
+ const hasValue = options.hasValue ?? arguments.length >= 3
747
+ const node = this.findSubject(subject, preference)
670
748
  if (!node) {
671
749
  return false
672
750
  }
@@ -681,19 +759,19 @@ export class Graph
681
759
  return true
682
760
  }
683
761
 
684
- const property = this.context.propertyName(predicate)
762
+ const property = this.propertyName(predicate, preference)
685
763
  if (!(property in node)) {
686
764
  return false
687
765
  }
688
766
 
689
- if (arguments.length < 3) {
767
+ if (!hasValue) {
690
768
  delete node[property]
691
769
  return true
692
770
  }
693
771
 
694
772
  const deleteValues = property == 'a'
695
- ? values(this.normalizeTypeValues(value))
696
- : values(this.normalizeValues(value))
773
+ ? values(this.normalizeTypeValues(value, preference))
774
+ : values(this.normalizeValues(value, preference))
697
775
  const remaining = values(node[property])
698
776
  .filter(item => !deleteValues.some(deleteValue => sameValue(item, deleteValue)))
699
777
 
@@ -710,7 +788,7 @@ export class Graph
710
788
  return true
711
789
  }
712
790
 
713
- ensureSubject(subject)
791
+ ensureSubject(subject, preference='source')
714
792
  {
715
793
  if (subject instanceof BlankNode && !(subject instanceof NamedNode)) {
716
794
  if (subject.graph !== this) {
@@ -723,32 +801,32 @@ export class Graph
723
801
  return this.addNamedNode(subject.id)
724
802
  }
725
803
 
726
- return this.addNamedNode(this.fullURI(subject))
804
+ return this.addNamedNode(this.fullURI(subject, null, preference))
727
805
  }
728
806
 
729
- findSubject(subject)
807
+ findSubject(subject, preference='source')
730
808
  {
731
809
  if (subject instanceof BlankNode && !(subject instanceof NamedNode)) {
732
810
  return subject.graph === this ? subject : null
733
811
  }
734
- const id = subject?.id ? subject.id : this.fullURI(subject)
812
+ const id = subject?.id ? subject.id : this.fullURI(subject, null, preference)
735
813
  return this.subjects[id]
736
814
  }
737
815
 
738
- normalizeValues(value)
816
+ normalizeValues(value, preference='source')
739
817
  {
740
818
  if (Array.isArray(value) && !(value instanceof Collection)) {
741
- return value.map(item => this.normalizeValue(item))
819
+ return value.map(item => this.normalizeValue(item, preference))
742
820
  }
743
- return this.normalizeValue(value)
821
+ return this.normalizeValue(value, preference)
744
822
  }
745
823
 
746
- normalizeValue(value)
824
+ normalizeValue(value, preference='source')
747
825
  {
748
826
  if (value instanceof Collection) {
749
827
  const collection = new Collection(this)
750
828
  for (const item of value) {
751
- collection.push(this.normalizeValue(item))
829
+ collection.push(this.normalizeValue(item, preference))
752
830
  }
753
831
  return collection
754
832
  }
@@ -761,29 +839,29 @@ export class Graph
761
839
  }
762
840
  return value
763
841
  }
764
- if (this.looksLikeURI(value)) {
765
- return this.addNamedNode(this.fullURI(value))
842
+ if (this.looksLikeURI(value, preference)) {
843
+ return this.addNamedNode(this.fullURI(value, null, preference))
766
844
  }
767
845
  return value
768
846
  }
769
847
 
770
- normalizeTypeValues(value)
848
+ normalizeTypeValues(value, preference='source')
771
849
  {
772
850
  if (Array.isArray(value) && !(value instanceof Collection)) {
773
- return value.map(item => this.normalizeTypeValue(item))
851
+ return value.map(item => this.normalizeTypeValue(item, preference))
774
852
  }
775
- return this.normalizeTypeValue(value)
853
+ return this.normalizeTypeValue(value, preference)
776
854
  }
777
855
 
778
- normalizeTypeValue(value)
856
+ normalizeTypeValue(value, preference='source')
779
857
  {
780
858
  if (value instanceof NamedNode) {
781
- return this.shortURI(value.id)
859
+ return this.shortURI(value.id, null, 'source')
782
860
  }
783
- return this.shortURI(this.fullURI(value))
861
+ return this.shortURI(this.fullURI(value, null, preference), null, 'source')
784
862
  }
785
863
 
786
- looksLikeURI(value)
864
+ looksLikeURI(value, preference='source')
787
865
  {
788
866
  if (typeof value != 'string') {
789
867
  return false
@@ -792,34 +870,33 @@ export class Graph
792
870
  return true
793
871
  }
794
872
  const [prefix, path] = value.split(this.context.separator)
795
- return Boolean(path && this.context.prefixes[prefix])
873
+ return Boolean(path && this.prefixEntries(preference).some(([candidate]) => candidate == prefix))
796
874
  }
797
875
 
798
- fullURI(shortURI, separator=null)
876
+ fullURI(shortURI, separator=null, preference='source')
799
877
  {
800
878
  if (!separator) {
801
879
  separator = this.context.separator
802
880
  }
803
- const [prefix, path] = shortURI.split(separator)
881
+ const [prefix, path] = String(shortURI).split(separator)
804
882
  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
883
+ for (const [candidate, iri] of this.prefixEntries(preference)) {
884
+ if (candidate == prefix) {
885
+ return iri+path
886
+ }
810
887
  }
811
888
  }
812
889
  return shortURI
813
890
  }
814
891
 
815
- shortURI(fullURI, separator=null)
892
+ shortURI(fullURI, separator=null, preference='source')
816
893
  {
817
894
  if (!separator) {
818
895
  separator = this.context.separator
819
896
  }
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)
897
+ for (const [prefix, iri] of this.prefixEntries(preference)) {
898
+ if (fullURI.startsWith(iri)) {
899
+ return prefix + separator + fullURI.substring(iri.length)
823
900
  }
824
901
  }
825
902
  if (this.url && fullURI.startsWith(this.url)) {
@@ -958,4 +1035,4 @@ export class Collection extends Array
958
1035
  enumerable: false
959
1036
  })
960
1037
  }
961
- }
1038
+ }