@oada/list-lib
Advanced tools
Comparing version 1.1.1 to 2.0.0
@@ -0,1 +1,3 @@ | ||
import type { Resource } from '@oada/types/oada/resource'; | ||
import type { Link } from '@oada/types/oada/link/v1'; | ||
import type V2Changes from '@oada/types/oada/change/v2'; | ||
@@ -14,2 +16,17 @@ import { Options, ItemState } from './Options'; | ||
/** | ||
* Type for the lists we can watch | ||
*/ | ||
export declare type List = Resource & { | ||
[key: string]: Link | List; | ||
}; | ||
declare module 'jsonpath-plus' { | ||
interface JSONPathCallable { | ||
(options: JSONPathOptions & { | ||
resultType: 'path' | 'pointer' | 'parentProperty'; | ||
wrap?: true; | ||
}): string[]; | ||
(path: JSONPathOptions['path'], json: JSONPathOptions['json'], callback?: JSONPathOptions['callback'], otherTypeCallback?: JSONPathOptions['otherTypeCallback']): any[]; | ||
} | ||
} | ||
/** | ||
* The main class of this library. | ||
@@ -29,2 +46,6 @@ * Watches an OADA list and calls various callbacks when appropriate. | ||
/** | ||
* The JSON Path for the list items | ||
*/ | ||
readonly itemsPath: string; | ||
/** | ||
* The OADA Tree for the List being watched | ||
@@ -60,3 +81,3 @@ * @see path | ||
}; | ||
constructor({ path, tree, name, resume, conn, persistInterval, assertItem, onAddItem, onChangeItem, onItem, onRemoveItem, onNewList, onDeleteList, getItemState, }: Options<Item>); | ||
constructor({ path, itemsPath, tree, name, resume, conn, persistInterval, assertItem, onAddItem, onChangeItem, onItem, onRemoveItem, onNewList, onDeleteList, getItemState, }: Options<Item>); | ||
/** | ||
@@ -63,0 +84,0 @@ * Force library to recheck all current list items |
124
lib/index.js
@@ -20,5 +20,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ListWatch = exports.ItemState = void 0; | ||
exports.ListWatch = exports.pathFromTree = exports.ItemState = void 0; | ||
const bluebird_1 = __importDefault(require("bluebird")); | ||
const json_pointer_1 = __importDefault(require("json-pointer")); | ||
const jsonpath_plus_1 = require("jsonpath-plus"); | ||
const debug_1 = __importDefault(require("debug")); | ||
@@ -53,3 +54,42 @@ const Options_1 = require("./Options"); | ||
} | ||
function getListItems(list, path) { | ||
const pointers = jsonpath_plus_1.JSONPath({ | ||
resultType: 'pointer', | ||
path, | ||
json: list, | ||
preventEval: true, | ||
}).filter( | ||
// Don't follow underscore keys | ||
(p) => !/\/_/.test(p)); | ||
return pointers; | ||
} | ||
/** | ||
* Generates an equivalent JSON Path from an OADA Tree object | ||
* | ||
* @internal | ||
* @experimental trees with multiple "paths" (excluing *) | ||
*/ | ||
function pathFromTree(tree, root = '') { | ||
let path = '$.*'; | ||
let outPath = '$'; | ||
const json = json_pointer_1.default.get(tree, root); | ||
while (true) { | ||
// Get set of non underscore keys | ||
const keys = [ | ||
...new Set(jsonpath_plus_1.JSONPath({ | ||
resultType: 'parentProperty', | ||
path, | ||
json, | ||
}).filter((k) => !k.startsWith('_'))), | ||
]; | ||
if (keys.length === 0) { | ||
break; | ||
} | ||
outPath += '.' + (keys.length === 1 ? keys[0] : `[${keys.join(',')}]`); | ||
path += '.*'; | ||
} | ||
return outPath; | ||
} | ||
exports.pathFromTree = pathFromTree; | ||
/** | ||
* The main class of this library. | ||
@@ -63,3 +103,3 @@ * Watches an OADA list and calls various callbacks when appropriate. | ||
class ListWatch { | ||
constructor({ path, tree, name, resume = false, conn, persistInterval = 1000, | ||
constructor({ path, itemsPath, tree, name, resume = false, conn, persistInterval = 1000, | ||
// If no assert given, assume all items valid | ||
@@ -99,2 +139,15 @@ assertItem = () => { }, onAddItem, onChangeItem, onItem, onRemoveItem, onNewList, onDeleteList = async () => { | ||
__classPrivateFieldSet(this, _getItemState, getItemState); | ||
if (itemsPath) { | ||
this.itemsPath = itemsPath; | ||
} | ||
else { | ||
if (tree) { | ||
// Asume items are at the leaves of tree | ||
this.itemsPath = pathFromTree(tree, path); | ||
} | ||
else { | ||
// Assume flat list | ||
this.itemsPath = '$.*'; | ||
} | ||
} | ||
if (onNewList) { | ||
@@ -140,3 +193,4 @@ __classPrivateFieldSet(this, _onNewList, onNewList); | ||
const { data: list } = (await conn.get({ path })); | ||
const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
//const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
const items = getListItems(list, this.itemsPath); | ||
//const { rev } = this.#meta; | ||
@@ -252,3 +306,4 @@ await bluebird_1.default.map(items, async (id) => { | ||
// Ignore _ keys of OADA | ||
const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
//const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
const items = getListItems(list, this.itemsPath); | ||
switch (type) { | ||
@@ -258,3 +313,3 @@ case 'merge': | ||
try { | ||
const lchange = list[id]; | ||
const lchange = json_pointer_1.default.get(list, id); | ||
// If there is an _id this is a new link in the list right? | ||
@@ -281,3 +336,3 @@ if (lchange._id) { | ||
try { | ||
const lchange = list[id]; | ||
const lchange = json_pointer_1.default.get(list, id); | ||
if (lchange === null) { | ||
@@ -334,3 +389,3 @@ info(`Detected removal of item ${id} from ${path}, rev ${rev}`); | ||
const change = { | ||
resource_id: list[id]._id, | ||
resource_id: json_pointer_1.default.get(list, id)._id, | ||
path: '', | ||
@@ -380,4 +435,5 @@ // TODO: what is the type the change?? | ||
if (currentItemsNew) { | ||
const { data: list } = (await conn.get({ path })); | ||
const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
const { data: list } = (await conn.get({ path, tree })); | ||
//const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
const items = getListItems(list, this.itemsPath); | ||
// ask for states of pre-existing items | ||
@@ -401,24 +457,42 @@ const states = await __classPrivateFieldGet(this, _onNewList).call(this, items); | ||
const rev = body._rev; | ||
const [id, ...rest] = json_pointer_1.default.parse(changePath); | ||
trace(`Received change to ${path}, rev ${rev}`); | ||
let itemsFound = !!id; | ||
let itemsFound = !!changePath; | ||
let listChange = body; | ||
try { | ||
// The actual change was to an item in the list (or a descendant) | ||
if (id) { | ||
// Make change start at item instead of the list | ||
const change = { | ||
...ctx, | ||
type, | ||
path: json_pointer_1.default.compile(rest), | ||
body: body, | ||
}; | ||
await this.handleItemChange(id, change); | ||
return; | ||
// The actual change was to a descendant of the list | ||
if (changePath) { | ||
// Reconstruct change to list? | ||
const changeObj = {}; | ||
json_pointer_1.default.set(changeObj, changePath, body); | ||
// Find items involved in the change | ||
const itemsChanged = jsonpath_plus_1.JSONPath({ | ||
path: this.itemsPath, | ||
json: changeObj, | ||
resultType: 'pointer', | ||
}); | ||
// The change was to items of the list (or their descendants) | ||
if (itemsChanged.length >= 1) { | ||
return bluebird_1.default.map(itemsChanged, (item) => { | ||
// Make change start at item instead of the list | ||
const path = changePath.slice(item.length); | ||
const change = { | ||
...ctx, | ||
type, | ||
path, | ||
body: json_pointer_1.default.get(changeObj, item), | ||
}; | ||
return this.handleItemChange(item, change); | ||
}); | ||
} | ||
else { | ||
// The change is between the list and items (multiple link levels) | ||
listChange = changeObj; | ||
} | ||
} | ||
// The change was to the list itself | ||
const list = body; | ||
itemsFound = (await this.handleListChange(list, type)) || itemsFound; | ||
itemsFound = | ||
(await this.handleListChange(listChange, type)) || itemsFound; | ||
} | ||
catch (err) { | ||
error(`Error processing change for ${id} at ${path}, rev ${rev}: %O`, err); | ||
error(`Error processing change at ${path}, rev ${rev}: %O`, err); | ||
} | ||
@@ -425,0 +499,0 @@ finally { |
@@ -13,2 +13,72 @@ "use strict"; | ||
const delay = 11; | ||
ava_1.default('it should create JSON Path from simple OADA tree', (t) => { | ||
const tree = { | ||
bookmarks: { | ||
_type: 'application/vnd.oada.bookmarks.1+json', | ||
_rev: 0, | ||
thing: { | ||
_type: 'application/json', | ||
_rev: 0, | ||
abc: { | ||
'*': { | ||
_type: 'application/json', | ||
_rev: 0, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
const path = _1.pathFromTree(tree); | ||
t.is(path, '$.bookmarks.thing.abc.*'); | ||
}); | ||
ava_1.default('it should create JSON Path from OADA tree and root', (t) => { | ||
const tree = { | ||
bookmarks: { | ||
_type: 'application/vnd.oada.bookmarks.1+json', | ||
_rev: 0, | ||
thing: { | ||
_type: 'application/json', | ||
_rev: 0, | ||
abc: { | ||
'*': { | ||
_type: 'application/json', | ||
_rev: 0, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
const path = _1.pathFromTree(tree, '/bookmarks/thing'); | ||
t.is(path, '$.abc.*'); | ||
}); | ||
ava_1.default('it should create JSON Path from two path OADA tree', (t) => { | ||
const tree = { | ||
bookmarks: { | ||
_type: 'application/vnd.oada.bookmarks.1+json', | ||
_rev: 0, | ||
thing1: { | ||
_type: 'application/json', | ||
_rev: 0, | ||
abc: { | ||
'*': { | ||
_type: 'application/json', | ||
_rev: 0, | ||
}, | ||
}, | ||
}, | ||
thing2: { | ||
_type: 'application/json', | ||
_rev: 0, | ||
abc: { | ||
'*': { | ||
_type: 'application/json', | ||
_rev: 0, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
const path = _1.pathFromTree(tree); | ||
t.is(path, '$.bookmarks.[thing1,thing2].abc.*'); | ||
}); | ||
ava_1.default('it should WATCH given path', async (t) => { | ||
@@ -15,0 +85,0 @@ var _a, _b, _c; |
@@ -21,2 +21,11 @@ import type { TypeAssert } from '@oada/types'; | ||
/** | ||
* JSON Path to retrieve items from list. | ||
* | ||
* @default $.* | ||
* @experimental | ||
* currently requires `tree` if there are multiple links to traverse | ||
* @see tree | ||
*/ | ||
itemsPath?: string; | ||
/** | ||
* OADA Tree for the path | ||
@@ -23,0 +32,0 @@ * |
{ | ||
"name": "@oada/list-lib", | ||
"version": "1.1.1", | ||
"version": "2.0.0", | ||
"description": "Library for processing items in an OADA list", | ||
@@ -60,2 +60,3 @@ "main": "lib/index.js", | ||
"json-pointer": "^0.6.0", | ||
"jsonpath-plus": "^4.0.0", | ||
"jsonschema8": "^1.1.0", | ||
@@ -62,0 +63,0 @@ "object-assign-deep": "^0.4.0" |
@@ -11,3 +11,3 @@ import test from 'ava'; | ||
import { ListWatch } from './'; | ||
import { ListWatch, Tree, pathFromTree } from './'; | ||
@@ -18,2 +18,81 @@ const name = 'oada-list-lib-test'; | ||
test('it should create JSON Path from simple OADA tree', (t) => { | ||
const tree: Tree = { | ||
bookmarks: { | ||
_type: 'application/vnd.oada.bookmarks.1+json', | ||
_rev: 0, | ||
thing: { | ||
_type: 'application/json', | ||
_rev: 0, | ||
abc: { | ||
'*': { | ||
_type: 'application/json', | ||
_rev: 0, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
const path = pathFromTree(tree); | ||
t.is(path, '$.bookmarks.thing.abc.*'); | ||
}); | ||
test('it should create JSON Path from OADA tree and root', (t) => { | ||
const tree: Tree = { | ||
bookmarks: { | ||
_type: 'application/vnd.oada.bookmarks.1+json', | ||
_rev: 0, | ||
thing: { | ||
_type: 'application/json', | ||
_rev: 0, | ||
abc: { | ||
'*': { | ||
_type: 'application/json', | ||
_rev: 0, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
const path = pathFromTree(tree, '/bookmarks/thing'); | ||
t.is(path, '$.abc.*'); | ||
}); | ||
test('it should create JSON Path from two path OADA tree', (t) => { | ||
const tree: Tree = { | ||
bookmarks: { | ||
_type: 'application/vnd.oada.bookmarks.1+json', | ||
_rev: 0, | ||
thing1: { | ||
_type: 'application/json', | ||
_rev: 0, | ||
abc: { | ||
'*': { | ||
_type: 'application/json', | ||
_rev: 0, | ||
}, | ||
}, | ||
}, | ||
thing2: { | ||
_type: 'application/json', | ||
_rev: 0, | ||
abc: { | ||
'*': { | ||
_type: 'application/json', | ||
_rev: 0, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
const path = pathFromTree(tree); | ||
t.is(path, '$.bookmarks.[thing1,thing2].abc.*'); | ||
}); | ||
test('it should WATCH given path', async (t) => { | ||
@@ -20,0 +99,0 @@ const conn = createStub(); |
181
src/index.ts
import Bluebird from 'bluebird'; | ||
import pointer from 'json-pointer'; | ||
import { JSONPath } from 'jsonpath-plus'; | ||
import debug from 'debug'; | ||
@@ -7,3 +8,3 @@ | ||
import type { Resource } from '@oada/types/oada/resource'; | ||
import type { List, Link } from '@oada/types/oada/link/v1'; | ||
import type { Link } from '@oada/types/oada/link/v1'; | ||
import type V2Changes from '@oada/types/oada/change/v2'; | ||
@@ -75,2 +76,89 @@ import type { SocketResponse } from '@oada/client/dist/websocket'; | ||
/** | ||
* Type for the lists we can watch | ||
*/ | ||
export type List = Resource & { | ||
[key: string]: Link | List; | ||
}; | ||
declare module 'jsonpath-plus' { | ||
interface JSONPathCallable { | ||
( | ||
options: JSONPathOptions & { | ||
resultType: 'path' | 'pointer' | 'parentProperty'; | ||
wrap?: true; | ||
} | ||
): string[]; | ||
( | ||
path: JSONPathOptions['path'], | ||
json: JSONPathOptions['json'], | ||
callback?: JSONPathOptions['callback'], | ||
otherTypeCallback?: JSONPathOptions['otherTypeCallback'] | ||
): any[]; | ||
} | ||
} | ||
function getListItems(list: List, path: string) { | ||
const pointers = JSONPath({ | ||
resultType: 'pointer', | ||
path, | ||
json: list, | ||
preventEval: true, | ||
}).filter( | ||
// Don't follow underscore keys | ||
(p) => !/\/_/.test(p) | ||
); | ||
return pointers; | ||
} | ||
/** | ||
* OADA Tree | ||
* | ||
* @internal | ||
*/ | ||
export type Tree = { | ||
_type?: string; | ||
_rev?: number; | ||
} & ( | ||
| { | ||
[key: string]: Tree; | ||
} | ||
| {} | ||
); | ||
/** | ||
* Generates an equivalent JSON Path from an OADA Tree object | ||
* | ||
* @internal | ||
* @experimental trees with multiple "paths" (excluing *) | ||
*/ | ||
export function pathFromTree(tree: Tree, root = ''): string { | ||
let path = '$.*'; | ||
let outPath = '$'; | ||
const json = pointer.get(tree, root); | ||
while (true) { | ||
// Get set of non underscore keys | ||
const keys = [ | ||
...new Set( | ||
JSONPath({ | ||
resultType: 'parentProperty', | ||
path, | ||
json, | ||
}).filter((k) => !k.startsWith('_')) | ||
), | ||
]; | ||
if (keys.length === 0) { | ||
break; | ||
} | ||
outPath += '.' + (keys.length === 1 ? keys[0] : `[${keys.join(',')}]`); | ||
path += '.*'; | ||
} | ||
return outPath; | ||
} | ||
/** | ||
* The main class of this library. | ||
@@ -89,2 +177,6 @@ * Watches an OADA list and calls various callbacks when appropriate. | ||
/** | ||
* The JSON Path for the list items | ||
*/ | ||
public readonly itemsPath; | ||
/** | ||
* The OADA Tree for the List being watched | ||
@@ -135,2 +227,3 @@ * @see path | ||
path, | ||
itemsPath, | ||
tree, | ||
@@ -170,2 +263,14 @@ name, | ||
if (itemsPath) { | ||
this.itemsPath = itemsPath; | ||
} else { | ||
if (tree) { | ||
// Asume items are at the leaves of tree | ||
this.itemsPath = pathFromTree(tree, path); | ||
} else { | ||
// Assume flat list | ||
this.itemsPath = '$.*'; | ||
} | ||
} | ||
if (onNewList) { | ||
@@ -213,3 +318,4 @@ this.#onNewList = onNewList; | ||
const { data: list } = (await conn.get({ path })) as GetResponse<List>; | ||
const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
//const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
const items = getListItems(list, this.itemsPath); | ||
@@ -340,3 +446,4 @@ //const { rev } = this.#meta; | ||
// Ignore _ keys of OADA | ||
const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
//const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
const items = getListItems(list as List, this.itemsPath); | ||
@@ -347,3 +454,3 @@ switch (type) { | ||
try { | ||
const lchange = list[id] as Partial<Link>; | ||
const lchange = pointer.get(list, id) as Partial<Link>; | ||
@@ -373,3 +480,3 @@ // If there is an _id this is a new link in the list right? | ||
try { | ||
const lchange = list[id]; | ||
const lchange = pointer.get(list, id); | ||
@@ -434,3 +541,3 @@ if (lchange === null) { | ||
const change: Change = { | ||
resource_id: list[id]._id, | ||
resource_id: pointer.get(list, id)._id, | ||
path: '', | ||
@@ -481,4 +588,7 @@ // TODO: what is the type the change?? | ||
if (currentItemsNew) { | ||
const { data: list } = (await conn.get({ path })) as GetResponse<List>; | ||
const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
const { data: list } = (await conn.get({ path, tree })) as GetResponse< | ||
List | ||
>; | ||
//const items = Object.keys(list).filter((k) => !k.match(/^_/)); | ||
const items = getListItems(list, this.itemsPath); | ||
@@ -506,30 +616,41 @@ // ask for states of pre-existing items | ||
const rev = (body as Change['body'])._rev as string; | ||
const [id, ...rest] = pointer.parse(changePath); | ||
trace(`Received change to ${path}, rev ${rev}`); | ||
let itemsFound = !!id; | ||
let itemsFound = !!changePath; | ||
let listChange = body as DeepPartial<List>; | ||
try { | ||
// The actual change was to an item in the list (or a descendant) | ||
if (id) { | ||
// Make change start at item instead of the list | ||
const change: Change = { | ||
...ctx, | ||
type, | ||
path: pointer.compile(rest), | ||
body: body as {}, | ||
}; | ||
await this.handleItemChange(id, change); | ||
return; | ||
// The actual change was to a descendant of the list | ||
if (changePath) { | ||
// Reconstruct change to list? | ||
const changeObj = {}; | ||
pointer.set(changeObj, changePath, body); | ||
// Find items involved in the change | ||
const itemsChanged = JSONPath({ | ||
path: this.itemsPath, | ||
json: changeObj, | ||
resultType: 'pointer', | ||
}); | ||
// The change was to items of the list (or their descendants) | ||
if (itemsChanged.length >= 1) { | ||
return Bluebird.map(itemsChanged, (item) => { | ||
// Make change start at item instead of the list | ||
const path = changePath.slice(item.length); | ||
const change: Change = { | ||
...ctx, | ||
type, | ||
path, | ||
body: pointer.get(changeObj, item), | ||
}; | ||
return this.handleItemChange(item, change); | ||
}); | ||
} else { | ||
// The change is between the list and items (multiple link levels) | ||
listChange = changeObj; | ||
} | ||
} | ||
// The change was to the list itself | ||
const list = body as DeepPartial<List>; | ||
itemsFound = (await this.handleListChange(list, type)) || itemsFound; | ||
itemsFound = | ||
(await this.handleListChange(listChange, type)) || itemsFound; | ||
} catch (err: unknown) { | ||
error( | ||
`Error processing change for ${id} at ${path}, rev ${rev}: %O`, | ||
err | ||
); | ||
error(`Error processing change at ${path}, rev ${rev}: %O`, err); | ||
} finally { | ||
@@ -536,0 +657,0 @@ // Need this check to prevent infinite loop |
@@ -26,2 +26,11 @@ import type { TypeAssert } from '@oada/types'; | ||
/** | ||
* JSON Path to retrieve items from list. | ||
* | ||
* @default $.* | ||
* @experimental | ||
* currently requires `tree` if there are multiple links to traverse | ||
* @see tree | ||
*/ | ||
itemsPath?: string; | ||
/** | ||
* OADA Tree for the path | ||
@@ -28,0 +37,0 @@ * |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
113333
2530
10
+ Addedjsonpath-plus@^4.0.0
+ Addedjsonpath-plus@4.0.0(transitive)