@nesso-how/schema 0.1.0-alpha.40 → 0.2.0-beta.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.
- package/README.md +9 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +70 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,15 @@ const doc: GraphDocument = deserialize(json)
|
|
|
27
27
|
|
|
28
28
|
Vocabulary-specific serialization (e.g. Nesso Learning Vocabulary) lives in [`@nesso-how/vocab-learning`](../vocab-learning/README.md).
|
|
29
29
|
|
|
30
|
+
## Compatibility
|
|
31
|
+
|
|
32
|
+
`deserialize` requires an explicit envelope `version`. It replays sequential
|
|
33
|
+
migrations for released older formats and rejects files from newer formats
|
|
34
|
+
with `NewerGraphFormatError`.
|
|
35
|
+
|
|
36
|
+
Envelope format `1` is the first protected beta baseline. Unversioned alpha
|
|
37
|
+
documents are outside the compatibility contract.
|
|
38
|
+
|
|
30
39
|
## License
|
|
31
40
|
|
|
32
41
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** Current on-disk graph document schema version. Bump only when the envelope shape changes. */
|
|
2
2
|
export declare const GRAPH_FORMAT_VERSION: 1;
|
|
3
|
+
export declare class NewerGraphFormatError extends Error {
|
|
4
|
+
readonly version: number;
|
|
5
|
+
constructor(version: number);
|
|
6
|
+
}
|
|
7
|
+
export declare class UnsupportedGraphFormatError extends Error {
|
|
8
|
+
readonly version: number | undefined;
|
|
9
|
+
constructor(version: number | undefined);
|
|
10
|
+
}
|
|
3
11
|
export interface GraphConcept<D extends Record<string, unknown> = Record<string, unknown>> {
|
|
4
12
|
id: string;
|
|
5
13
|
label: string;
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,81 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
/** Current on-disk graph document schema version. Bump only when the envelope shape changes. */
|
|
3
3
|
export const GRAPH_FORMAT_VERSION = 1;
|
|
4
|
+
const ENVELOPE_MIGRATIONS = {};
|
|
5
|
+
export class NewerGraphFormatError extends Error {
|
|
6
|
+
version;
|
|
7
|
+
constructor(version) {
|
|
8
|
+
super(`This graph uses newer Nesso graph format version ${version}`);
|
|
9
|
+
this.name = 'NewerGraphFormatError';
|
|
10
|
+
this.version = version;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class UnsupportedGraphFormatError extends Error {
|
|
14
|
+
version;
|
|
15
|
+
constructor(version) {
|
|
16
|
+
super(version === undefined
|
|
17
|
+
? 'This graph does not declare a supported Nesso graph format version'
|
|
18
|
+
: `Unsupported Nesso graph format version: ${version}`);
|
|
19
|
+
this.name = 'UnsupportedGraphFormatError';
|
|
20
|
+
this.version = version;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readEnvelopeVersion(document) {
|
|
24
|
+
const version = document.version;
|
|
25
|
+
// Stryker disable next-line ConditionalExpression: typeof guard subsumed by !Number.isInteger and version < 0 — equivalent mutant
|
|
26
|
+
if (typeof version !== 'number' || !Number.isInteger(version) || version < 0) {
|
|
27
|
+
throw new UnsupportedGraphFormatError(undefined);
|
|
28
|
+
}
|
|
29
|
+
return version;
|
|
30
|
+
}
|
|
31
|
+
function migrateEnvelope(document) {
|
|
32
|
+
let current = document;
|
|
33
|
+
let version = readEnvelopeVersion(current);
|
|
34
|
+
if (version > GRAPH_FORMAT_VERSION) {
|
|
35
|
+
throw new NewerGraphFormatError(version);
|
|
36
|
+
}
|
|
37
|
+
while (version < GRAPH_FORMAT_VERSION) {
|
|
38
|
+
const migrate = ENVELOPE_MIGRATIONS[version];
|
|
39
|
+
// Stryker disable next-line ConditionalExpression,EqualityOperator: ENVELOPE_MIGRATIONS is empty at baseline; migrate is always undefined — equivalent mutant
|
|
40
|
+
if (migrate === undefined) {
|
|
41
|
+
throw new UnsupportedGraphFormatError(version);
|
|
42
|
+
}
|
|
43
|
+
/* c8 ignore start */ // migration-step-advance is unreachable while ENVELOPE_MIGRATIONS is empty
|
|
44
|
+
// Stryker disable next-line all: migration-step-advance is unreachable while ENVELOPE_MIGRATIONS is empty
|
|
45
|
+
current = migrate(current);
|
|
46
|
+
// Stryker disable next-line all: unreachable
|
|
47
|
+
const nextVersion = readEnvelopeVersion(current);
|
|
48
|
+
// Stryker disable next-line all: unreachable
|
|
49
|
+
if (nextVersion !== version + 1) {
|
|
50
|
+
// Stryker disable next-line all: unreachable
|
|
51
|
+
throw new Error(`Envelope migration ${version} must produce version ${version + 1}`);
|
|
52
|
+
}
|
|
53
|
+
// Stryker disable next-line all: unreachable
|
|
54
|
+
version = nextVersion;
|
|
55
|
+
/* c8 ignore stop */
|
|
56
|
+
}
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
4
59
|
/** Serialize a graph document to JSON with keys in a stable, human-readable order. */
|
|
5
60
|
export function serialize(doc) {
|
|
6
61
|
const { vocabulary, id, updatedAt, name, concepts, relations, meta } = doc;
|
|
7
62
|
return JSON.stringify({
|
|
8
63
|
version: GRAPH_FORMAT_VERSION,
|
|
64
|
+
// Stryker disable next-line ConditionalExpression: JSON.stringify drops undefined values, so always spreading {vocabulary:undefined} is equivalent
|
|
9
65
|
...(vocabulary !== undefined && { vocabulary }),
|
|
66
|
+
// Stryker disable next-line ConditionalExpression: JSON.stringify drops undefined values, so always spreading {id:undefined} is equivalent
|
|
10
67
|
...(id !== undefined && { id }),
|
|
68
|
+
// Stryker disable next-line ConditionalExpression: JSON.stringify drops undefined values, so always spreading {updatedAt:undefined} is equivalent
|
|
11
69
|
...(updatedAt !== undefined && { updatedAt }),
|
|
12
70
|
name,
|
|
13
71
|
concepts,
|
|
14
72
|
relations,
|
|
73
|
+
// Stryker disable next-line ConditionalExpression: JSON.stringify drops undefined values, so always spreading {meta:undefined} is equivalent
|
|
15
74
|
...(meta !== undefined && { meta }),
|
|
16
75
|
}, null, 2);
|
|
17
76
|
}
|
|
18
77
|
function isRecord(value) {
|
|
19
|
-
return typeof value === 'object' && value !== null;
|
|
78
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
79
|
}
|
|
21
80
|
/**
|
|
22
81
|
* Parse and structurally validate a graph document. Files are user-editable (and
|
|
@@ -25,12 +84,13 @@ function isRecord(value) {
|
|
|
25
84
|
* then be re-persisted. Vocabulary-specific normalization is the caller's responsibility.
|
|
26
85
|
*/
|
|
27
86
|
export function deserialize(json) {
|
|
28
|
-
const
|
|
29
|
-
if (!isRecord(
|
|
30
|
-
throw new Error('Invalid Nesso graph document
|
|
87
|
+
const parsed = JSON.parse(json);
|
|
88
|
+
if (!isRecord(parsed)) {
|
|
89
|
+
throw new Error('Invalid Nesso graph document');
|
|
31
90
|
}
|
|
32
|
-
|
|
33
|
-
|
|
91
|
+
const data = migrateEnvelope(parsed);
|
|
92
|
+
if (!Array.isArray(data.concepts) || !Array.isArray(data.relations)) {
|
|
93
|
+
throw new Error('Invalid Nesso graph document: missing concepts or relations array');
|
|
34
94
|
}
|
|
35
95
|
const rawConcepts = data.concepts;
|
|
36
96
|
const concepts = rawConcepts.map((value, i) => {
|
|
@@ -38,8 +98,10 @@ export function deserialize(json) {
|
|
|
38
98
|
typeof value.id !== 'string' ||
|
|
39
99
|
value.id === '' ||
|
|
40
100
|
typeof value.label !== 'string' ||
|
|
101
|
+
// Stryker disable next-line ConditionalExpression: typeof x guard subsumed by !Number.isFinite — equivalent mutant
|
|
41
102
|
typeof value.x !== 'number' ||
|
|
42
103
|
!Number.isFinite(value.x) ||
|
|
104
|
+
// Stryker disable next-line ConditionalExpression: typeof y guard subsumed by !Number.isFinite — equivalent mutant
|
|
43
105
|
typeof value.y !== 'number' ||
|
|
44
106
|
!Number.isFinite(value.y)) {
|
|
45
107
|
throw new Error(`Invalid Nesso graph document: concept ${i} is missing a valid id, label, or position`);
|
|
@@ -82,9 +144,8 @@ export function deserialize(json) {
|
|
|
82
144
|
vocabulary = { id: vocabValue.id, version: vocabValue.version };
|
|
83
145
|
}
|
|
84
146
|
let meta;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
meta = metaValue;
|
|
147
|
+
if (isRecord(data.meta)) {
|
|
148
|
+
meta = data.meta;
|
|
88
149
|
}
|
|
89
150
|
return {
|
|
90
151
|
version: GRAPH_FORMAT_VERSION,
|