@kubun/protocol 0.10.0 → 0.11.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/lib/errors.d.ts +2 -0
- package/lib/errors.js +11 -1
- package/lib/executor.js +6 -1
- package/lib/index.js +12 -1
- package/lib/models/catalog.js +6 -1
- package/lib/models/cluster.d.ts +2 -2
- package/lib/models/cluster.js +186 -1
- package/lib/models/document.d.ts +2 -2
- package/lib/models/document.js +550 -1
- package/lib/models/graph.js +55 -1
- package/lib/models/group.js +1 -1
- package/lib/models/json-schema.d.ts +1 -1
- package/lib/models/json-schema.js +320 -1
- package/lib/models/optimal-clusters.js +324 -1
- package/lib/models/value.js +16 -1
- package/lib/semantic.js +23 -1
- package/lib/services/graph.d.ts +1 -1
- package/lib/services/graph.js +367 -1
- package/lib/types.js +1 -1
- package/package.json +8 -8
|
@@ -1 +1,320 @@
|
|
|
1
|
-
export const booleanModel
|
|
1
|
+
export const booleanModel = {
|
|
2
|
+
type: 'object',
|
|
3
|
+
properties: {
|
|
4
|
+
type: {
|
|
5
|
+
type: 'string',
|
|
6
|
+
const: 'boolean'
|
|
7
|
+
},
|
|
8
|
+
default: {
|
|
9
|
+
type: 'boolean'
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
required: [
|
|
13
|
+
'type'
|
|
14
|
+
],
|
|
15
|
+
additionalProperties: false
|
|
16
|
+
};
|
|
17
|
+
export const integerModel = {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
type: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
const: 'integer'
|
|
23
|
+
},
|
|
24
|
+
default: {
|
|
25
|
+
type: 'integer'
|
|
26
|
+
},
|
|
27
|
+
minimum: {
|
|
28
|
+
type: 'integer'
|
|
29
|
+
},
|
|
30
|
+
maximum: {
|
|
31
|
+
type: 'integer'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
required: [
|
|
35
|
+
'type'
|
|
36
|
+
],
|
|
37
|
+
additionalProperties: false
|
|
38
|
+
};
|
|
39
|
+
export const numberModel = {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
type: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
const: 'number'
|
|
45
|
+
},
|
|
46
|
+
default: {
|
|
47
|
+
type: 'number'
|
|
48
|
+
},
|
|
49
|
+
min: {
|
|
50
|
+
type: 'number'
|
|
51
|
+
},
|
|
52
|
+
max: {
|
|
53
|
+
type: 'number'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
required: [
|
|
57
|
+
'type'
|
|
58
|
+
],
|
|
59
|
+
additionalProperties: false
|
|
60
|
+
};
|
|
61
|
+
export const stringConstModel = {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
type: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
const: 'string'
|
|
67
|
+
},
|
|
68
|
+
title: {
|
|
69
|
+
type: 'string'
|
|
70
|
+
},
|
|
71
|
+
const: {
|
|
72
|
+
type: 'string'
|
|
73
|
+
},
|
|
74
|
+
default: {
|
|
75
|
+
type: 'string'
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
required: [
|
|
79
|
+
'type',
|
|
80
|
+
'const'
|
|
81
|
+
],
|
|
82
|
+
additionalProperties: false
|
|
83
|
+
};
|
|
84
|
+
// Uppercase characters and "_" to match GraphQL enum
|
|
85
|
+
const enumString = {
|
|
86
|
+
type: 'string',
|
|
87
|
+
pattern: '^[A-Z]+[A-Z_]+[A-Z]$'
|
|
88
|
+
};
|
|
89
|
+
export const stringEnumModel = {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
type: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
const: 'string'
|
|
95
|
+
},
|
|
96
|
+
title: {
|
|
97
|
+
type: 'string'
|
|
98
|
+
},
|
|
99
|
+
enum: {
|
|
100
|
+
type: 'array',
|
|
101
|
+
items: enumString
|
|
102
|
+
},
|
|
103
|
+
default: enumString
|
|
104
|
+
},
|
|
105
|
+
required: [
|
|
106
|
+
'type',
|
|
107
|
+
'title',
|
|
108
|
+
'enum'
|
|
109
|
+
],
|
|
110
|
+
additionalProperties: false
|
|
111
|
+
};
|
|
112
|
+
export const supportedStringFormat = {
|
|
113
|
+
type: 'string',
|
|
114
|
+
enum: [
|
|
115
|
+
'date',
|
|
116
|
+
'date-time',
|
|
117
|
+
'duration',
|
|
118
|
+
'email',
|
|
119
|
+
'time',
|
|
120
|
+
'uri'
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
export const stringFormatModel = {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
type: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
const: 'string'
|
|
129
|
+
},
|
|
130
|
+
format: supportedStringFormat,
|
|
131
|
+
title: {
|
|
132
|
+
type: 'string'
|
|
133
|
+
},
|
|
134
|
+
default: {
|
|
135
|
+
type: 'string'
|
|
136
|
+
},
|
|
137
|
+
minLength: {
|
|
138
|
+
type: 'integer'
|
|
139
|
+
},
|
|
140
|
+
maxLength: {
|
|
141
|
+
type: 'integer'
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
required: [
|
|
145
|
+
'type',
|
|
146
|
+
'format'
|
|
147
|
+
],
|
|
148
|
+
additionalProperties: false
|
|
149
|
+
};
|
|
150
|
+
export const stringCustomModel = {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
type: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
const: 'string'
|
|
156
|
+
},
|
|
157
|
+
title: {
|
|
158
|
+
type: 'string'
|
|
159
|
+
},
|
|
160
|
+
pattern: {
|
|
161
|
+
type: 'string'
|
|
162
|
+
},
|
|
163
|
+
default: {
|
|
164
|
+
type: 'string'
|
|
165
|
+
},
|
|
166
|
+
minLength: {
|
|
167
|
+
type: 'integer'
|
|
168
|
+
},
|
|
169
|
+
maxLength: {
|
|
170
|
+
type: 'integer'
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
required: [
|
|
174
|
+
'type'
|
|
175
|
+
],
|
|
176
|
+
additionalProperties: false
|
|
177
|
+
};
|
|
178
|
+
export const stringModel = {
|
|
179
|
+
anyOf: [
|
|
180
|
+
stringConstModel,
|
|
181
|
+
stringCustomModel,
|
|
182
|
+
stringEnumModel,
|
|
183
|
+
stringFormatModel
|
|
184
|
+
]
|
|
185
|
+
};
|
|
186
|
+
export const scalarModel = {
|
|
187
|
+
anyOf: [
|
|
188
|
+
booleanModel,
|
|
189
|
+
integerModel,
|
|
190
|
+
numberModel,
|
|
191
|
+
stringModel
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
export const refModel = {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
$ref: {
|
|
198
|
+
type: 'string'
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
required: [
|
|
202
|
+
'$ref'
|
|
203
|
+
],
|
|
204
|
+
additionalProperties: false
|
|
205
|
+
};
|
|
206
|
+
// Valid GraphQL name
|
|
207
|
+
export const nameString = {
|
|
208
|
+
type: 'string',
|
|
209
|
+
pattern: '^[A-Z]+[A-Za-z0-9]+$'
|
|
210
|
+
};
|
|
211
|
+
export const arrayModel = {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
type: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
const: 'array'
|
|
217
|
+
},
|
|
218
|
+
title: nameString,
|
|
219
|
+
items: refModel,
|
|
220
|
+
uniqueItems: {
|
|
221
|
+
type: 'boolean'
|
|
222
|
+
},
|
|
223
|
+
minItems: {
|
|
224
|
+
type: 'integer'
|
|
225
|
+
},
|
|
226
|
+
maxItems: {
|
|
227
|
+
type: 'integer'
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
required: [
|
|
231
|
+
'type',
|
|
232
|
+
'title',
|
|
233
|
+
'items'
|
|
234
|
+
],
|
|
235
|
+
additionalProperties: false
|
|
236
|
+
};
|
|
237
|
+
export const baseObjectModel = {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
type: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
const: 'object'
|
|
243
|
+
},
|
|
244
|
+
properties: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
patternProperties: {
|
|
247
|
+
'^[a-z]+[A-Za-z0-9]+$': refModel
|
|
248
|
+
},
|
|
249
|
+
additionalProperties: false
|
|
250
|
+
},
|
|
251
|
+
required: {
|
|
252
|
+
type: 'array',
|
|
253
|
+
items: {
|
|
254
|
+
type: 'string'
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
additionalProperties: {
|
|
258
|
+
type: 'boolean'
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
required: [
|
|
262
|
+
'type',
|
|
263
|
+
'properties',
|
|
264
|
+
'required',
|
|
265
|
+
'additionalProperties'
|
|
266
|
+
],
|
|
267
|
+
additionalProperties: false
|
|
268
|
+
};
|
|
269
|
+
export const objectModel = {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
...baseObjectModel.properties,
|
|
273
|
+
title: nameString
|
|
274
|
+
},
|
|
275
|
+
required: [
|
|
276
|
+
...baseObjectModel.required,
|
|
277
|
+
'title'
|
|
278
|
+
],
|
|
279
|
+
additionalProperties: false
|
|
280
|
+
};
|
|
281
|
+
export const jsonObjectModel = {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
type: {
|
|
285
|
+
type: 'string',
|
|
286
|
+
const: 'object'
|
|
287
|
+
},
|
|
288
|
+
title: {
|
|
289
|
+
type: 'string',
|
|
290
|
+
const: 'JSONObject'
|
|
291
|
+
},
|
|
292
|
+
additionalProperties: {
|
|
293
|
+
type: 'boolean',
|
|
294
|
+
const: true
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
required: [
|
|
298
|
+
'type',
|
|
299
|
+
'title',
|
|
300
|
+
'additionalProperties'
|
|
301
|
+
],
|
|
302
|
+
additionalProperties: false
|
|
303
|
+
};
|
|
304
|
+
export const shapeModel = {
|
|
305
|
+
anyOf: [
|
|
306
|
+
arrayModel,
|
|
307
|
+
objectModel,
|
|
308
|
+
jsonObjectModel
|
|
309
|
+
]
|
|
310
|
+
};
|
|
311
|
+
export const typeModel = {
|
|
312
|
+
anyOf: [
|
|
313
|
+
scalarModel,
|
|
314
|
+
shapeModel
|
|
315
|
+
]
|
|
316
|
+
};
|
|
317
|
+
export const referencesRecordModel = {
|
|
318
|
+
type: 'object',
|
|
319
|
+
additionalProperties: typeModel
|
|
320
|
+
};
|
|
@@ -1 +1,324 @@
|
|
|
1
|
-
import{DocumentModelID
|
|
1
|
+
import { DocumentModelID, digestJSON } from '@kubun/id';
|
|
2
|
+
import { ClusterBuilder, clusterToRecord } from './cluster.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parse local index from a reference string like '#0', '#1', etc.
|
|
5
|
+
* Returns null if the reference is not a local index.
|
|
6
|
+
*/ function parseLocalIndex(ref) {
|
|
7
|
+
if (!ref.startsWith('#')) return null;
|
|
8
|
+
const index = Number.parseInt(ref.slice(1), 10);
|
|
9
|
+
return Number.isNaN(index) ? null : index;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extract fieldsMeta document reference dependencies for each model.
|
|
13
|
+
* Returns directed edges only (A→B does not imply B→A).
|
|
14
|
+
* Used for SCC grouping — only circular fieldsMeta forces co-location.
|
|
15
|
+
*/ export function extractFieldsMetaDeps(models) {
|
|
16
|
+
return models.map((model)=>{
|
|
17
|
+
const deps = new Set();
|
|
18
|
+
for (const meta of Object.values(model.fieldsMeta ?? {})){
|
|
19
|
+
if (meta.type === 'document' && meta.model != null) {
|
|
20
|
+
const index = parseLocalIndex(meta.model);
|
|
21
|
+
if (index != null) deps.add(index);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return deps;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extract interface implementation dependencies for each model.
|
|
29
|
+
* These form a DAG (no circular interfaces). Used for ordering
|
|
30
|
+
* within a cluster so ClusterBuilder can resolve interfaces eagerly.
|
|
31
|
+
*/ export function extractInterfaceDeps(models) {
|
|
32
|
+
return models.map((model)=>{
|
|
33
|
+
const deps = new Set();
|
|
34
|
+
for (const iface of model.interfaces ?? []){
|
|
35
|
+
const index = parseLocalIndex(iface);
|
|
36
|
+
if (index != null) deps.add(index);
|
|
37
|
+
}
|
|
38
|
+
return deps;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Find strongly connected components using Tarjan's algorithm.
|
|
43
|
+
* Used on fieldsMeta directed graph — only circular references (A→B→A)
|
|
44
|
+
* force models into the same cluster. One-way refs don't group.
|
|
45
|
+
* Returns arrays of model indices, one per SCC.
|
|
46
|
+
*/ export function findStronglyConnectedComponents(deps) {
|
|
47
|
+
const n = deps.length;
|
|
48
|
+
const indices = new Array(n).fill(-1);
|
|
49
|
+
const lowlinks = new Array(n).fill(-1);
|
|
50
|
+
const onStack = new Array(n).fill(false);
|
|
51
|
+
const stack = [];
|
|
52
|
+
const sccs = [];
|
|
53
|
+
let currentIndex = 0;
|
|
54
|
+
function strongConnect(node) {
|
|
55
|
+
indices[node] = currentIndex;
|
|
56
|
+
lowlinks[node] = currentIndex;
|
|
57
|
+
currentIndex++;
|
|
58
|
+
stack.push(node);
|
|
59
|
+
onStack[node] = true;
|
|
60
|
+
for (const dep of deps[node]){
|
|
61
|
+
if (dep >= n) continue;
|
|
62
|
+
if (indices[dep] === -1) {
|
|
63
|
+
strongConnect(dep);
|
|
64
|
+
lowlinks[node] = Math.min(lowlinks[node], lowlinks[dep]);
|
|
65
|
+
} else if (onStack[dep]) {
|
|
66
|
+
lowlinks[node] = Math.min(lowlinks[node], indices[dep]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (lowlinks[node] === indices[node]) {
|
|
70
|
+
const scc = [];
|
|
71
|
+
let member = stack.pop();
|
|
72
|
+
while(member != null){
|
|
73
|
+
onStack[member] = false;
|
|
74
|
+
scc.push(member);
|
|
75
|
+
if (member === node) break;
|
|
76
|
+
member = stack.pop();
|
|
77
|
+
}
|
|
78
|
+
sccs.push(scc);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for(let i = 0; i < n; i++){
|
|
82
|
+
if (indices[i] === -1) {
|
|
83
|
+
strongConnect(i);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Sort each SCC internally and sort SCCs by smallest index
|
|
87
|
+
return sccs.map((scc)=>scc.sort((a, b)=>a - b)).sort((a, b)=>a[0] - b[0]);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Find cluster groups from model definitions.
|
|
91
|
+
* Groups are determined by strongly connected components in the
|
|
92
|
+
* fieldsMeta directed graph. Only circular fieldsMeta references
|
|
93
|
+
* force models into the same cluster.
|
|
94
|
+
*/ export function findClusters(models) {
|
|
95
|
+
const fieldsMetaDeps = extractFieldsMetaDeps(models);
|
|
96
|
+
return findStronglyConnectedComponents(fieldsMetaDeps);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Sort models within a cluster group by interface deps (topological)
|
|
100
|
+
* with name as tiebreaker. Rewrite intra-cluster index refs to new
|
|
101
|
+
* positions. Cross-cluster refs are rewritten to global DocumentModelID
|
|
102
|
+
* strings using the provided mapping.
|
|
103
|
+
*/ function prepareClusterModels(models, group, interfaceDeps, globalIDMap) {
|
|
104
|
+
const groupSet = new Set(group);
|
|
105
|
+
// Topological sort on interface deps within this group, name tiebreaker
|
|
106
|
+
const localInterfaceDeps = new Map();
|
|
107
|
+
for (const origIdx of group){
|
|
108
|
+
const filtered = new Set();
|
|
109
|
+
for (const dep of interfaceDeps[origIdx]){
|
|
110
|
+
if (groupSet.has(dep)) filtered.add(dep);
|
|
111
|
+
}
|
|
112
|
+
localInterfaceDeps.set(origIdx, filtered);
|
|
113
|
+
}
|
|
114
|
+
const remaining = new Map();
|
|
115
|
+
for (const origIdx of group){
|
|
116
|
+
remaining.set(origIdx, localInterfaceDeps.get(origIdx)?.size ?? 0);
|
|
117
|
+
}
|
|
118
|
+
const sorted = [];
|
|
119
|
+
const ready = group.filter((idx)=>(remaining.get(idx) ?? 0) === 0).map((idx)=>({
|
|
120
|
+
originalIndex: idx,
|
|
121
|
+
model: models[idx]
|
|
122
|
+
})).sort((a, b)=>a.model.name.localeCompare(b.model.name));
|
|
123
|
+
while(ready.length > 0){
|
|
124
|
+
const next = ready.shift();
|
|
125
|
+
if (next == null) break;
|
|
126
|
+
sorted.push(next);
|
|
127
|
+
for (const [origIdx, deps] of localInterfaceDeps){
|
|
128
|
+
if (deps.has(next.originalIndex)) {
|
|
129
|
+
const count = (remaining.get(origIdx) ?? 1) - 1;
|
|
130
|
+
remaining.set(origIdx, count);
|
|
131
|
+
if (count === 0) {
|
|
132
|
+
const entry = {
|
|
133
|
+
originalIndex: origIdx,
|
|
134
|
+
model: models[origIdx]
|
|
135
|
+
};
|
|
136
|
+
const insertIdx = ready.findIndex((r)=>r.model.name.localeCompare(entry.model.name) > 0);
|
|
137
|
+
if (insertIdx === -1) {
|
|
138
|
+
ready.push(entry);
|
|
139
|
+
} else {
|
|
140
|
+
ready.splice(insertIdx, 0, entry);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Build mapping: original index → new cluster-internal index
|
|
147
|
+
const internalIndexMap = new Map();
|
|
148
|
+
for(let newIndex = 0; newIndex < sorted.length; newIndex++){
|
|
149
|
+
internalIndexMap.set(sorted[newIndex].originalIndex, newIndex);
|
|
150
|
+
}
|
|
151
|
+
// Rewrite references
|
|
152
|
+
return sorted.map(({ model })=>{
|
|
153
|
+
const rewritten = {
|
|
154
|
+
...model
|
|
155
|
+
};
|
|
156
|
+
// Rewrite interface references
|
|
157
|
+
if (rewritten.interfaces != null) {
|
|
158
|
+
rewritten.interfaces = rewritten.interfaces.map((ref)=>{
|
|
159
|
+
const origIndex = parseLocalIndex(ref);
|
|
160
|
+
if (origIndex == null) return ref;
|
|
161
|
+
// Cross-cluster: use global ID
|
|
162
|
+
const globalID = globalIDMap.get(origIndex);
|
|
163
|
+
if (globalID != null) return globalID;
|
|
164
|
+
// Intra-cluster: use new position
|
|
165
|
+
const newIndex = internalIndexMap.get(origIndex);
|
|
166
|
+
if (newIndex != null) return `#${newIndex}`;
|
|
167
|
+
return ref;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Rewrite fieldsMeta document references
|
|
171
|
+
if (rewritten.fieldsMeta != null) {
|
|
172
|
+
const newFieldsMeta = {
|
|
173
|
+
...rewritten.fieldsMeta
|
|
174
|
+
};
|
|
175
|
+
for (const [key, meta] of Object.entries(newFieldsMeta)){
|
|
176
|
+
if (meta.type === 'document' && meta.model != null) {
|
|
177
|
+
const origIndex = parseLocalIndex(meta.model);
|
|
178
|
+
if (origIndex == null) continue;
|
|
179
|
+
// Cross-cluster: use global ID
|
|
180
|
+
const globalID = globalIDMap.get(origIndex);
|
|
181
|
+
if (globalID != null) {
|
|
182
|
+
newFieldsMeta[key] = {
|
|
183
|
+
...meta,
|
|
184
|
+
model: globalID
|
|
185
|
+
};
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Intra-cluster: use new position
|
|
189
|
+
const newIndex = internalIndexMap.get(origIndex);
|
|
190
|
+
if (newIndex != null) {
|
|
191
|
+
newFieldsMeta[key] = {
|
|
192
|
+
...meta,
|
|
193
|
+
model: `#${newIndex}`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
rewritten.fieldsMeta = newFieldsMeta;
|
|
199
|
+
}
|
|
200
|
+
return rewritten;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Topologically sort cluster groups by inter-group dependencies
|
|
205
|
+
* (interfaces + one-way fieldsMeta). Returns group indices in build order.
|
|
206
|
+
*/ function sortGroupsByDeps(groups, interfaceDeps, fieldsMetaDeps) {
|
|
207
|
+
// Map each model index to its group index
|
|
208
|
+
const modelToGroup = new Map();
|
|
209
|
+
for(let gi = 0; gi < groups.length; gi++){
|
|
210
|
+
for (const modelIdx of groups[gi]){
|
|
211
|
+
modelToGroup.set(modelIdx, gi);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Build inter-group dependency graph
|
|
215
|
+
const groupDeps = Array.from({
|
|
216
|
+
length: groups.length
|
|
217
|
+
}, ()=>new Set());
|
|
218
|
+
for(let gi = 0; gi < groups.length; gi++){
|
|
219
|
+
for (const modelIdx of groups[gi]){
|
|
220
|
+
// Interface deps
|
|
221
|
+
for (const dep of interfaceDeps[modelIdx]){
|
|
222
|
+
const depGroup = modelToGroup.get(dep);
|
|
223
|
+
if (depGroup != null && depGroup !== gi) {
|
|
224
|
+
groupDeps[gi].add(depGroup);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// One-way fieldsMeta deps (to models outside this group)
|
|
228
|
+
for (const dep of fieldsMetaDeps[modelIdx]){
|
|
229
|
+
const depGroup = modelToGroup.get(dep);
|
|
230
|
+
if (depGroup != null && depGroup !== gi) {
|
|
231
|
+
groupDeps[gi].add(depGroup);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Kahn's topological sort
|
|
237
|
+
const remaining = groupDeps.map((deps)=>deps.size);
|
|
238
|
+
const ready = remaining.map((count, idx)=>count === 0 ? idx : -1).filter((idx)=>idx >= 0);
|
|
239
|
+
const sorted = [];
|
|
240
|
+
while(ready.length > 0){
|
|
241
|
+
const next = ready.shift();
|
|
242
|
+
if (next == null) break;
|
|
243
|
+
sorted.push(next);
|
|
244
|
+
for(let gi = 0; gi < groupDeps.length; gi++){
|
|
245
|
+
if (groupDeps[gi].has(next)) {
|
|
246
|
+
remaining[gi]--;
|
|
247
|
+
if (remaining[gi] === 0) {
|
|
248
|
+
ready.push(gi);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return sorted;
|
|
254
|
+
}
|
|
255
|
+
function buildOptimalClusters(models, externalClusters) {
|
|
256
|
+
if (models.length === 0) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
const fieldsMetaDeps = extractFieldsMetaDeps(models);
|
|
260
|
+
const interfaceDeps = extractInterfaceDeps(models);
|
|
261
|
+
const groups = findStronglyConnectedComponents(fieldsMetaDeps);
|
|
262
|
+
const buildOrder = sortGroupsByDeps(groups, interfaceDeps, fieldsMetaDeps);
|
|
263
|
+
// Track global IDs of already-built models: original input index → global DocumentModelID string
|
|
264
|
+
const globalIDMap = new Map();
|
|
265
|
+
// Accumulate built clusters for use as externalClusters by later groups
|
|
266
|
+
const builtClusters = {
|
|
267
|
+
...externalClusters ?? {}
|
|
268
|
+
};
|
|
269
|
+
const result = [];
|
|
270
|
+
for (const groupIdx of buildOrder){
|
|
271
|
+
const group = groups[groupIdx];
|
|
272
|
+
const prepared = prepareClusterModels(models, group, interfaceDeps, globalIDMap);
|
|
273
|
+
const builder = new ClusterBuilder({
|
|
274
|
+
externalClusters: Object.keys(builtClusters).length > 0 ? builtClusters : undefined
|
|
275
|
+
});
|
|
276
|
+
builder.addAll(prepared);
|
|
277
|
+
const cluster = builder.build();
|
|
278
|
+
result.push(cluster);
|
|
279
|
+
// Record global IDs for models in this cluster
|
|
280
|
+
const clusterDigest = digestJSON(cluster.models);
|
|
281
|
+
for(let i = 0; i < group.length; i++){
|
|
282
|
+
// Find which cluster-internal index corresponds to this original model
|
|
283
|
+
// prepareClusterModels sorts by interface deps + name, so we match by name
|
|
284
|
+
const origIdx = group[i];
|
|
285
|
+
const modelName = models[origIdx].name;
|
|
286
|
+
const clusterInternalIdx = cluster.models.findIndex((m)=>m.name === modelName);
|
|
287
|
+
if (clusterInternalIdx !== -1) {
|
|
288
|
+
const globalID = new DocumentModelID(clusterDigest, clusterInternalIdx);
|
|
289
|
+
globalIDMap.set(origIdx, globalID.toString());
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
Object.assign(builtClusters, clusterToRecord(cluster));
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Higher-level builder that produces optimally-split clusters from model
|
|
298
|
+
* definitions. Independent models get their own cluster; cross-referencing
|
|
299
|
+
* models are grouped together. This maximizes content-addressed
|
|
300
|
+
* deduplication while keeping the ClusterBuilder as an internal detail.
|
|
301
|
+
*/ export class GraphBuilder {
|
|
302
|
+
#models = [];
|
|
303
|
+
#externalClusters;
|
|
304
|
+
constructor(params = {}){
|
|
305
|
+
this.#externalClusters = params.externalClusters ?? {};
|
|
306
|
+
}
|
|
307
|
+
add(model) {
|
|
308
|
+
this.#models.push(model);
|
|
309
|
+
return this;
|
|
310
|
+
}
|
|
311
|
+
addAll(models) {
|
|
312
|
+
this.#models.push(...models);
|
|
313
|
+
return this;
|
|
314
|
+
}
|
|
315
|
+
build() {
|
|
316
|
+
const externalClusters = Object.keys(this.#externalClusters).length > 0 ? this.#externalClusters : undefined;
|
|
317
|
+
const clusters = buildOptimalClusters(this.#models, externalClusters);
|
|
318
|
+
const record = {};
|
|
319
|
+
for (const cluster of clusters){
|
|
320
|
+
Object.assign(record, clusterToRecord(cluster));
|
|
321
|
+
}
|
|
322
|
+
return record;
|
|
323
|
+
}
|
|
324
|
+
}
|
package/lib/models/value.js
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
export const binaryStringValue
|
|
1
|
+
export const binaryStringValue = {
|
|
2
|
+
title: 'KubunBinaryValue',
|
|
3
|
+
type: 'string',
|
|
4
|
+
// Base 64 encoding
|
|
5
|
+
pattern: '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$'
|
|
6
|
+
};
|
|
7
|
+
export const didStringValue = {
|
|
8
|
+
title: 'KubunDIDValue',
|
|
9
|
+
type: 'string',
|
|
10
|
+
pattern: '^did:[a-z0-9]+:'
|
|
11
|
+
};
|
|
12
|
+
export const signedJWTStringValue = {
|
|
13
|
+
title: 'KubunSignedJWTValue',
|
|
14
|
+
type: 'string',
|
|
15
|
+
pattern: '^[a-zA-Z0-9_-]+.[a-zA-Z0-9_-]+.[a-zA-Z0-9_-]+$'
|
|
16
|
+
};
|
package/lib/semantic.js
CHANGED
|
@@ -1 +1,23 @@
|
|
|
1
|
-
export const KubunSpanNames=
|
|
1
|
+
export const KubunSpanNames = {
|
|
2
|
+
GRAPH_QUERY: 'kubun.graph.query',
|
|
3
|
+
GRAPH_MUTATE: 'kubun.graph.mutate',
|
|
4
|
+
GRAPH_SUBSCRIBE: 'kubun.graph.subscribe',
|
|
5
|
+
GRAPH_DEPLOY: 'kubun.graph.deploy',
|
|
6
|
+
GRAPHQL_EXECUTE: 'kubun.graphql.execute',
|
|
7
|
+
DATA_QUERY: 'kubun.data.query',
|
|
8
|
+
DATA_MUTATE: 'kubun.data.mutate',
|
|
9
|
+
DATA_TRANSACTION: 'kubun.data.transaction'
|
|
10
|
+
};
|
|
11
|
+
export const KubunAttributeKeys = {
|
|
12
|
+
GRAPH_ID: 'kubun.graph.id',
|
|
13
|
+
GRAPHQL_OPERATION_TYPE: 'graphql.operation.type',
|
|
14
|
+
GRAPHQL_DOCUMENT: 'graphql.document',
|
|
15
|
+
GRAPHQL_VARIABLES: 'graphql.variables',
|
|
16
|
+
MODEL_ID: 'kubun.model.id',
|
|
17
|
+
DOCUMENT_ID: 'kubun.document.id',
|
|
18
|
+
DOCUMENTS_COUNT: 'kubun.documents.count',
|
|
19
|
+
MUTATION_TYPE: 'kubun.mutation.type',
|
|
20
|
+
MUTATION_DATA: 'kubun.mutation.data',
|
|
21
|
+
QUERY_FILTER: 'kubun.query.filter',
|
|
22
|
+
ERROR_CODE: 'kubun.error.code'
|
|
23
|
+
};
|