Skip to content

Commit

Permalink
Improve safe mode for @graph use cases.
Browse files Browse the repository at this point in the history
- Better handling for `@graph` top-level empty objects, objects with
  only ids, relative references, etc.
  • Loading branch information
davidlehn committed May 19, 2023
1 parent b5df97d commit ca09db4
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 38 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
updated.
- Test on Node.js 20.x.

### Fixed
- Improve safe mode for `@graph` use cases.

## 8.1.1 - 2023-02-25

### Fixed
Expand Down
102 changes: 64 additions & 38 deletions lib/expand.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,48 +372,64 @@ api.expand = async ({
// drop certain top-level objects that do not occur in lists
if(_isObject(rval) &&
!options.keepFreeFloatingNodes && !insideList &&
(activeProperty === null || expandedActiveProperty === '@graph')) {
(activeProperty === null ||
expandedActiveProperty === '@graph' ||
(_getContextValue(activeCtx, activeProperty, '@container') || [])
.includes('@graph')
)) {
// drop empty object, top-level @value/@list, or object with only @id
if(count === 0 || '@value' in rval || '@list' in rval ||
(count === 1 && '@id' in rval)) {
// FIXME
if(options.eventHandler) {
// FIXME: one event or diff event for empty, @v/@l, {@id}?
let code;
let message;
if(count === 0) {
code = 'empty object';
message = 'Dropping empty object.';
} else if('@value' in rval) {
code = 'object with only @value';
message = 'Dropping object with only @value.';
} else if('@list' in rval) {
code = 'object with only @list';
message = 'Dropping object with only @list.';
} else if(count === 1 && '@id' in rval) {
code = 'object with only @id';
message = 'Dropping object with only @id.';
}
_handleEvent({
event: {
type: ['JsonLdEvent'],
code,
level: 'warning',
message,
details: {
value: rval
}
},
options
});
}
rval = null;
}
rval = _dropUnsafeObject({value: rval, count, options});
}

return rval;
};

/**
* Drop empty object, top-level @value/@list, or object with only @id
*/
function _dropUnsafeObject({
value,
count,
options
}) {
if(count === 0 || '@value' in value || '@list' in value ||
(count === 1 && '@id' in value)) {
// FIXME
if(options.eventHandler) {
// FIXME: one event or diff event for empty, @v/@l, {@id}?
let code;
let message;
if(count === 0) {
code = 'empty object';
message = 'Dropping empty object.';
} else if('@value' in value) {
code = 'object with only @value';
message = 'Dropping object with only @value.';
} else if('@list' in value) {
code = 'object with only @list';
message = 'Dropping object with only @list.';
} else if(count === 1 && '@id' in value) {
code = 'object with only @id';
message = 'Dropping object with only @id.';
}
_handleEvent({
event: {
type: ['JsonLdEvent'],
code,
level: 'warning',
message,
details: {
value
}
},
options
});
}
return null;
}
return value;
}

/**
* Expand each key and value of element adding to result
*
Expand Down Expand Up @@ -933,8 +949,18 @@ async function _expandObject({
if(container.includes('@graph') &&
!container.some(key => key === '@id' || key === '@index')) {
// ensure expanded values are arrays
expandedValue = _asArray(expandedValue)
.map(v => ({'@graph': _asArray(v)}));
// ensure an array
expandedValue = _asArray(expandedValue);
// check if needs to be dropped
const count = Object.keys(expandedValue[0]).length;
if(!options.isFrame && _dropUnsafeObject({
value: expandedValue[0], count, options
}) === null) {
// skip adding and continue
continue;
}
// convert to graph
expandedValue = expandedValue.map(v => ({'@graph': _asArray(v)}));
}

// FIXME: can this be merged with code above to simplify?
Expand Down
241 changes: 241 additions & 0 deletions tests/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,247 @@ _:b0 <ex:p> "[null]"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON> .
});
});

it('should emit for @graph with empty object (1)', async () => {
const input =
{
"@context": {
"p": {
"@id": "urn:p",
"@type": "@id",
"@container": "@graph"
}
},
"@id": "urn:id",
"p": {}
}
;
const expected = [];

await _test({
type: 'expand',
input,
expected,
eventCodeLog: [
'empty object',
'object with only @id'
],
testNotSafe: true
});
});

it('should emit for ok @graph with empty object (2)', async () => {
const input =
{
"@context": {
"p": {
"@id": "urn:p",
"@type": "@id",
"@container": "@graph"
},
"urn:t": {
"@type": "@id"
}
},
"@id": "urn:id",
"urn:t": "urn:id",
"p": {}
}
;
const expected =
[
{
"@id": "urn:id",
"urn:t": [
{
"@id": "urn:id"
}
]
}
]
;

await _test({
type: 'expand',
input,
expected,
eventCodeLog: [
'empty object'
],
testNotSafe: true
});
});

it('should emit for @graph with relative @id (1)', async () => {
const input =
{
"@context": {
"p": {
"@id": "urn:p",
"@type": "@id",
"@container": "@graph"
}
},
"@id": "urn:id",
"p": ["rel"]
}
;
const expected = [];

await _test({
type: 'expand',
input,
expected,
eventCodeLog: [
'object with only @id',
'object with only @id'
],
testNotSafe: true
});
});

it('should emit for @graph with relative @id (2)', async () => {
const input =
{
"@context": {
"p": {
"@id": "urn:p",
"@type": "@id",
"@container": "@graph"
},
"urn:t": {
"@type": "@id"
}
},
"@id": "urn:id",
"urn:t": "urn:id",
"p": ["rel"]
}
;
const expected =
[
{
"@id": "urn:id",
"urn:t": [
{
"@id": "urn:id"
}
]
}
]
;

await _test({
type: 'expand',
input,
expected,
eventCodeLog: [
'object with only @id',
],
testNotSafe: true
});
});

it('should emit for @graph with relative @id (3)', async () => {
const input =
{
"@context": {
"p": {
"@id": "urn:p",
"@type": "@id",
"@container": "@graph"
},
"urn:t": {
"@type": "@id"
}
},
"@id": "urn:id",
"urn:t": "urn:id",
"p": "rel"
}
;
const expected =
[
{
"@id": "urn:id",
"urn:t": [
{
"@id": "urn:id"
}
]
}
]
;

await _test({
type: 'expand',
input,
expected,
eventCodeLog: [
'object with only @id',
],
testNotSafe: true
});
});

it('should emit for @graph with relative @id (4)', async () => {
const input =
{
"@context": {
"p": {
"@id": "urn:p",
"@type": "@id",
"@container": "@graph"
},
"urn:t": {
"@type": "@id"
}
},
"@id": "urn:id",
"urn:t": "urn:id",
"p": {
"@id": "rel",
"urn:t": "urn:id2"
}
}
;
const expected =
[
{
"@id": "urn:id",
"urn:t": [
{
"@id": "urn:id"
}
],
"urn:p": [
{
"@graph": [
{
"@id": "rel",
"urn:t": [
{
"@id": "urn:id2"
}
]
}
]
}
]
}
]
;

await _test({
type: 'expand',
input,
expected,
eventCodeLog: [
'relative @id reference',
],
testNotSafe: true
});
});

it('should emit for null @value', async () => {
const input =
{
Expand Down

0 comments on commit ca09db4

Please sign in to comment.