@script-it/json-views-core 0.1.0-beta.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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/THIRD_PARTY_NOTICES.txt +4917 -0
- package/dist/annotation-capabilities.d.ts +17 -0
- package/dist/annotation-capabilities.js +21 -0
- package/dist/atomic-value.d.ts +3 -0
- package/dist/atomic-value.js +6 -0
- package/dist/date-type.d.ts +48 -0
- package/dist/date-type.js +244 -0
- package/dist/diagnostics.d.ts +8 -0
- package/dist/diagnostics.js +252 -0
- package/dist/document-session.d.ts +52 -0
- package/dist/document-session.js +164 -0
- package/dist/formats/conversion.d.ts +17 -0
- package/dist/formats/conversion.js +75 -0
- package/dist/formats/csv-format.d.ts +31 -0
- package/dist/formats/csv-format.js +269 -0
- package/dist/formats/document-format.d.ts +35 -0
- package/dist/formats/document-format.js +1 -0
- package/dist/formats/json-format.d.ts +12 -0
- package/dist/formats/json-format.js +109 -0
- package/dist/html-view-bindings.d.ts +4 -0
- package/dist/html-view-bindings.js +32 -0
- package/dist/html-view-limits.d.ts +1 -0
- package/dist/html-view-limits.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/inference-fields.d.ts +5 -0
- package/dist/inference-fields.js +23 -0
- package/dist/inference.d.ts +26 -0
- package/dist/inference.js +304 -0
- package/dist/json-limits.d.ts +8 -0
- package/dist/json-limits.js +25 -0
- package/dist/json-path.d.ts +14 -0
- package/dist/json-path.js +84 -0
- package/dist/json-source-patcher.d.ts +97 -0
- package/dist/json-source-patcher.js +778 -0
- package/dist/metadata.d.ts +137 -0
- package/dist/metadata.js +952 -0
- package/dist/safe-pattern.d.ts +5 -0
- package/dist/safe-pattern.js +20 -0
- package/dist/schema-validation.d.ts +3 -0
- package/dist/schema-validation.js +4 -0
- package/dist/source-default.d.ts +2 -0
- package/dist/source-default.js +39 -0
- package/dist/table-suitability.d.ts +5 -0
- package/dist/table-suitability.js +55 -0
- package/dist/type-registry.d.ts +57 -0
- package/dist/type-registry.js +238 -0
- package/dist/value-comparison.d.ts +18 -0
- package/dist/value-comparison.js +133 -0
- package/dist/view-model.d.ts +18 -0
- package/dist/view-model.js +134 -0
- package/package.json +52 -0
- package/schema/jsonviews.schema.json +102 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { inferJsonViewRecordTitleKey } from './inference.js';
|
|
2
|
+
import { inferFlatColumnPaths } from './table-suitability.js';
|
|
3
|
+
import { compareJsonValues, jsonValuesEqual, matchesJsonValueFilter } from './value-comparison.js';
|
|
4
|
+
import { JSON_VIEW_PATH_MISSING, getJsonViewViewRows, matchesJsonViewPath, resolveJsonViewRowPath, schemaForJsonViewPath, } from './metadata.js';
|
|
5
|
+
import { defaultTypeRegistry } from './type-registry.js';
|
|
6
|
+
export function matchesJsonViewFilterRule(value, rule, descriptor, typeRegistry = defaultTypeRegistry, context = {}) {
|
|
7
|
+
if (descriptor) {
|
|
8
|
+
const typed = typeRegistry.matchesFilter(value, rule.operator, rule.value, descriptor);
|
|
9
|
+
if (typed !== undefined)
|
|
10
|
+
return typed;
|
|
11
|
+
}
|
|
12
|
+
if (value === JSON_VIEW_PATH_MISSING)
|
|
13
|
+
return rule.operator === 'isEmpty';
|
|
14
|
+
return matchesJsonValueFilter(value, rule.operator, rule.value, context);
|
|
15
|
+
}
|
|
16
|
+
function filteredRows(root, rows, view, schema, typeRegistry, numberAtPath) {
|
|
17
|
+
const filter = view.filter;
|
|
18
|
+
if (!filter || filter.rules.length === 0)
|
|
19
|
+
return rows;
|
|
20
|
+
return rows.filter((row) => {
|
|
21
|
+
const matches = filter.rules.map((rule) => {
|
|
22
|
+
const resolved = resolveJsonViewRowPath(root, row, rule.path);
|
|
23
|
+
const descriptor = schemaForJsonViewPath(schema, resolved.sourcePath)?.descriptor;
|
|
24
|
+
return matchesJsonViewFilterRule(resolved.value, rule, descriptor, typeRegistry, { numberAtPath, leftPath: resolved.sourcePath, rightPath: rule.sourceValuePath });
|
|
25
|
+
});
|
|
26
|
+
return filter.match === 'all' ? matches.every(Boolean) : matches.some(Boolean);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function compare(left, right, direction, descriptor, typeRegistry, context) {
|
|
30
|
+
if (jsonValuesEqual(left, right, context))
|
|
31
|
+
return 0;
|
|
32
|
+
if (left === JSON_VIEW_PATH_MISSING || left == null)
|
|
33
|
+
return 1;
|
|
34
|
+
if (right === JSON_VIEW_PATH_MISSING || right == null)
|
|
35
|
+
return -1;
|
|
36
|
+
const typed = descriptor ? typeRegistry.compare(left, right, descriptor, direction) : undefined;
|
|
37
|
+
if (typed !== undefined)
|
|
38
|
+
return typed;
|
|
39
|
+
const result = compareJsonValues(left, right, context);
|
|
40
|
+
return direction === 'desc' ? -result : result;
|
|
41
|
+
}
|
|
42
|
+
function sortedRows(root, rows, view, schema, typeRegistry, numberAtPath) {
|
|
43
|
+
if (view.sort.length === 0)
|
|
44
|
+
return rows;
|
|
45
|
+
const prepared = rows.map((row, index) => ({
|
|
46
|
+
row, index,
|
|
47
|
+
fields: view.sort.map((sort) => {
|
|
48
|
+
const resolved = resolveJsonViewRowPath(root, row, sort.path);
|
|
49
|
+
return { value: resolved.value, sourcePath: resolved.sourcePath, descriptor: schemaForJsonViewPath(schema, resolved.sourcePath)?.descriptor };
|
|
50
|
+
}),
|
|
51
|
+
}));
|
|
52
|
+
// Each column uses one comparator for every pair, including mixed-type annotations.
|
|
53
|
+
const descriptors = view.sort.map((_, index) => {
|
|
54
|
+
const defined = prepared.map((item) => item.fields[index].descriptor).filter((item) => item !== undefined);
|
|
55
|
+
return new Set(defined.map((item) => item.type)).size === 1 ? defined[0] : undefined;
|
|
56
|
+
});
|
|
57
|
+
return prepared.sort((left, right) => {
|
|
58
|
+
for (let index = 0; index < view.sort.length; index += 1) {
|
|
59
|
+
const result = compare(left.fields[index].value, right.fields[index].value, view.sort[index].direction, descriptors[index], typeRegistry, { numberAtPath, leftPath: left.fields[index].sourcePath, rightPath: right.fields[index].sourcePath });
|
|
60
|
+
if (result !== 0)
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
return left.index - right.index;
|
|
64
|
+
}).map(({ row }) => row);
|
|
65
|
+
}
|
|
66
|
+
export function applyJsonViewViewRows(root, view, schema = [], typeRegistry = defaultTypeRegistry, numberAtPath) {
|
|
67
|
+
return sortedRows(root, filteredRows(root, getJsonViewViewRows(view, root), view, schema, typeRegistry, numberAtPath), view, schema, typeRegistry, numberAtPath);
|
|
68
|
+
}
|
|
69
|
+
function inferredViewProperty(view, key) {
|
|
70
|
+
const escaped = key
|
|
71
|
+
.replace(/\\/g, '\\\\')
|
|
72
|
+
.replace(/'/g, "\\'")
|
|
73
|
+
.replace(/\n/g, '\\n')
|
|
74
|
+
.replace(/\r/g, '\\r')
|
|
75
|
+
.replace(/\t/g, '\\t');
|
|
76
|
+
return {
|
|
77
|
+
source: `${view.path.source}[*]['${escaped}']`,
|
|
78
|
+
root: '$',
|
|
79
|
+
segments: [...view.path.segments, { kind: 'wildcard' }, { kind: 'property', key }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function inferredColumns(view, rows) {
|
|
83
|
+
const titleKey = inferJsonViewRecordTitleKey(rows.map((row) => row.value));
|
|
84
|
+
const flattened = inferFlatColumnPaths(rows.map((row) => row.value));
|
|
85
|
+
if (flattened && titleKey)
|
|
86
|
+
flattened.sort((a, b) => Number(b.length === 1 && b[0] === titleKey) - Number(a.length === 1 && a[0] === titleKey));
|
|
87
|
+
if (flattened)
|
|
88
|
+
return flattened.map((parts, index) => {
|
|
89
|
+
const path = inferredViewProperty(view, parts[0]);
|
|
90
|
+
for (const key of parts.slice(1)) {
|
|
91
|
+
path.segments.push({ kind: 'property', key });
|
|
92
|
+
path.source += `[${JSON.stringify(key)}]`;
|
|
93
|
+
}
|
|
94
|
+
return { label: parts.join(' · '), path, metadataPath: ['__inferred_columns__', index] };
|
|
95
|
+
});
|
|
96
|
+
const keys = new Set();
|
|
97
|
+
rows.forEach((row) => Object.keys(row.value).forEach((key) => keys.add(key)));
|
|
98
|
+
return Array.from(keys).sort((a, b) => Number(b === titleKey) - Number(a === titleKey)).map((key, index) => ({
|
|
99
|
+
label: key,
|
|
100
|
+
path: inferredViewProperty(view, key),
|
|
101
|
+
metadataPath: ['__inferred_columns__', index],
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
export function projectJsonViewCollection(root, view, schema = [], typeRegistry = defaultTypeRegistry, numberAtPath) {
|
|
105
|
+
const rows = applyJsonViewViewRows(root, view, schema, typeRegistry, numberAtPath);
|
|
106
|
+
const declared = view.columns ?? inferredColumns(view, rows);
|
|
107
|
+
const columns = declared.map((column, index) => ({
|
|
108
|
+
id: `column-${index}`,
|
|
109
|
+
label: column.label,
|
|
110
|
+
path: column.path,
|
|
111
|
+
}));
|
|
112
|
+
const records = rows.map((row) => Object.fromEntries(columns.flatMap((column) => {
|
|
113
|
+
const resolved = resolveJsonViewRowPath(root, row, column.path).value;
|
|
114
|
+
return resolved === JSON_VIEW_PATH_MISSING ? [] : [[column.id, resolved]];
|
|
115
|
+
})));
|
|
116
|
+
return { rows, columns, records };
|
|
117
|
+
}
|
|
118
|
+
export function schemaRelativePathForRow(schema, rowPath) {
|
|
119
|
+
if (schema.path.segments.length <= rowPath.length)
|
|
120
|
+
return undefined;
|
|
121
|
+
if (!matchesJsonViewPath({ ...schema.path, segments: schema.path.segments.slice(0, rowPath.length) }, rowPath))
|
|
122
|
+
return undefined;
|
|
123
|
+
const remainder = schema.path.segments.slice(rowPath.length);
|
|
124
|
+
if (remainder.some((segment) => segment.kind === 'wildcard'))
|
|
125
|
+
return undefined;
|
|
126
|
+
const relativePath = [];
|
|
127
|
+
for (const segment of remainder) {
|
|
128
|
+
if (segment.kind === 'property')
|
|
129
|
+
relativePath.push(segment.key);
|
|
130
|
+
else if (segment.kind === 'index')
|
|
131
|
+
relativePath.push(segment.index);
|
|
132
|
+
}
|
|
133
|
+
return relativePath;
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@script-it/json-views-core",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "JSON Views metadata, path, validation, and source-preserving editing core",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./schema.json": "./schema/jsonviews.schema.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"schema",
|
|
21
|
+
"THIRD_PARTY_NOTICES.txt"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "node ../../scripts/clean-dist.mjs && tsc -p tsconfig.json",
|
|
25
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"prepack": "node ../../scripts/prepare-package.mjs"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"ajv": "^8.20.0",
|
|
31
|
+
"typescript": "^5.9.3",
|
|
32
|
+
"vitest": "3.2.7"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"re2js": "^2.8.6"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/script-it/json-views.git",
|
|
40
|
+
"directory": "packages/core"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/script-it/json-views#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/script-it/json-views/issues"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": "^22.13.0 || >=24.0.0"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/script-it/json-views/main/packages/core/schema/jsonviews.schema.json",
|
|
4
|
+
"title": "JSON Views annotation metadata, version 1",
|
|
5
|
+
"description": "Validates the annotation object passed externally or stored under $jsonviews. This is not a JSON Schema for the annotated document. JSONPath resolution, registered types, RE2 patterns, date validity, and view compatibility are checked by the compiler.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": { "const": 1 },
|
|
10
|
+
"schema": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"propertyNames": { "$ref": "#/definitions/path" },
|
|
13
|
+
"additionalProperties": { "$ref": "#/definitions/descriptor" }
|
|
14
|
+
},
|
|
15
|
+
"views": { "type": "array", "items": { "$ref": "#/definitions/view" } }
|
|
16
|
+
},
|
|
17
|
+
"additionalProperties": true,
|
|
18
|
+
"definitions": {
|
|
19
|
+
"path": { "type": "string", "pattern": "^\\$", "description": "Absolute JSON Views path; the compiler checks the supported JSONPath subset and wildcard placement." },
|
|
20
|
+
"label": { "type": "string", "pattern": "\\S" },
|
|
21
|
+
"descriptor": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": ["type"],
|
|
24
|
+
"properties": {
|
|
25
|
+
"type": { "$ref": "#/definitions/label" },
|
|
26
|
+
"title": { "type": "string" },
|
|
27
|
+
"description": { "type": "string" },
|
|
28
|
+
"required": { "type": "boolean" },
|
|
29
|
+
"nullable": false,
|
|
30
|
+
"minimum": { "type": ["number", "string", "null"] },
|
|
31
|
+
"maximum": { "type": ["number", "string", "null"] },
|
|
32
|
+
"defaultIncludeTime": { "type": "boolean" },
|
|
33
|
+
"pattern": { "type": "string", "maxLength": 4096, "description": "RE2 syntax, tested as an unanchored search; unsupported constructs are compiler diagnostics." },
|
|
34
|
+
"options": { "type": "array", "items": { "type": "string" } },
|
|
35
|
+
"optionColors": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": { "enum": ["gray", "blue", "green", "yellow", "orange", "red", "purple", "pink"] },
|
|
38
|
+
"description": "Optional palette color for each select or multi-select option."
|
|
39
|
+
},
|
|
40
|
+
"placeholder": { "type": "string" },
|
|
41
|
+
"multiline": { "type": "boolean" },
|
|
42
|
+
"step": { "type": "number", "exclusiveMinimum": 0 }
|
|
43
|
+
},
|
|
44
|
+
"if": { "properties": { "type": { "const": "date" } }, "required": ["type"] },
|
|
45
|
+
"then": { "properties": { "minimum": { "type": ["string", "null"] }, "maximum": { "type": ["string", "null"] } } },
|
|
46
|
+
"else": { "properties": { "minimum": { "type": "number" }, "maximum": { "type": "number" } } },
|
|
47
|
+
"additionalProperties": true
|
|
48
|
+
},
|
|
49
|
+
"column": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"required": ["label", "path"],
|
|
52
|
+
"properties": { "label": { "$ref": "#/definitions/label" }, "path": { "$ref": "#/definitions/path" } },
|
|
53
|
+
"additionalProperties": true
|
|
54
|
+
},
|
|
55
|
+
"filterRule": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"required": ["path", "operator"],
|
|
58
|
+
"properties": { "path": { "$ref": "#/definitions/path" }, "operator": { "$ref": "#/definitions/label" }, "value": {} },
|
|
59
|
+
"allOf": [
|
|
60
|
+
{
|
|
61
|
+
"if": { "properties": { "operator": { "enum": ["isEmpty", "isNotEmpty"] } }, "required": ["operator"] },
|
|
62
|
+
"else": { "required": ["value"] }
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"if": { "properties": { "operator": { "enum": ["in", "notIn"] } }, "required": ["operator"] },
|
|
66
|
+
"then": { "properties": { "value": { "type": "array" } } }
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"additionalProperties": true
|
|
70
|
+
},
|
|
71
|
+
"filter": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"required": ["rules"],
|
|
74
|
+
"properties": { "match": { "enum": ["all", "any"] }, "rules": { "type": "array", "items": { "$ref": "#/definitions/filterRule" } } },
|
|
75
|
+
"additionalProperties": true
|
|
76
|
+
},
|
|
77
|
+
"sort": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["path", "direction"],
|
|
80
|
+
"properties": { "path": { "$ref": "#/definitions/path" }, "direction": { "enum": ["asc", "desc"] } },
|
|
81
|
+
"additionalProperties": true
|
|
82
|
+
},
|
|
83
|
+
"view": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"required": ["name", "path"],
|
|
86
|
+
"properties": {
|
|
87
|
+
"id": { "type": "string" },
|
|
88
|
+
"name": { "$ref": "#/definitions/label" },
|
|
89
|
+
"path": { "$ref": "#/definitions/path" },
|
|
90
|
+
"display": { "enum": ["kanban", "html"], "description": "Omit for automatic record/collection layout." },
|
|
91
|
+
"title": false,
|
|
92
|
+
"groupBy": { "$ref": "#/definitions/path" },
|
|
93
|
+
"groupOrder": { "type": "array", "items": { "type": ["string", "number", "boolean", "null"] }, "uniqueItems": true },
|
|
94
|
+
"orderPath": { "$ref": "#/definitions/path" },
|
|
95
|
+
"columns": { "type": "array", "items": { "$ref": "#/definitions/column" } },
|
|
96
|
+
"filter": { "$ref": "#/definitions/filter" },
|
|
97
|
+
"sort": { "type": "array", "items": { "$ref": "#/definitions/sort" } }
|
|
98
|
+
},
|
|
99
|
+
"additionalProperties": true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|