@graphprotocol/hypergraph 0.1.0 → 0.2.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/dist/Entity.d.ts +69 -0
- package/dist/Entity.d.ts.map +1 -0
- package/dist/Entity.js +174 -0
- package/dist/Entity.js.map +1 -0
- package/dist/identity/create-identity-keys.d.ts +3 -0
- package/dist/identity/create-identity-keys.d.ts.map +1 -0
- package/dist/identity/create-identity-keys.js +20 -0
- package/dist/identity/create-identity-keys.js.map +1 -0
- package/dist/identity/login.d.ts +38 -0
- package/dist/identity/login.d.ts.map +1 -0
- package/dist/identity/login.js +241 -0
- package/dist/identity/login.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mapping/Mapping.d.ts +418 -0
- package/dist/mapping/Mapping.d.ts.map +1 -0
- package/dist/mapping/Mapping.js +554 -0
- package/dist/mapping/Mapping.js.map +1 -0
- package/dist/mapping/Utils.d.ts +74 -0
- package/dist/mapping/Utils.d.ts.map +1 -0
- package/dist/mapping/Utils.js +144 -0
- package/dist/mapping/Utils.js.map +1 -0
- package/dist/mapping/index.d.ts +3 -0
- package/dist/mapping/index.d.ts.map +1 -0
- package/dist/mapping/index.js +3 -0
- package/dist/mapping/index.js.map +1 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js.map +1 -1
- package/dist/utils/hasArrayField.d.ts +2 -0
- package/dist/utils/hasArrayField.d.ts.map +1 -0
- package/dist/utils/hasArrayField.js +5 -0
- package/dist/utils/hasArrayField.js.map +1 -0
- package/package.json +3 -5
- package/src/index.ts +1 -0
- package/src/mapping/Mapping.ts +771 -0
- package/src/mapping/Utils.ts +156 -0
- package/src/mapping/index.ts +2 -0
- package/src/store.ts +1 -1
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { type CreatePropertyParams, Graph, Id as Grc20Id, type Op } from '@graphprotocol/grc-20';
|
|
2
|
+
import { Data, Array as EffectArray, Schema as EffectSchema, Option, pipe } from 'effect';
|
|
3
|
+
|
|
4
|
+
import { namesAreUnique, toCamelCase, toPascalCase } from './Utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mappings for a schema type and its properties/relations
|
|
8
|
+
*
|
|
9
|
+
* @since 0.2.0
|
|
10
|
+
*/
|
|
11
|
+
export type MappingEntry = {
|
|
12
|
+
/**
|
|
13
|
+
* Array of the `Id.Id` of the type in the Knowledge Graph.
|
|
14
|
+
* Is an array because a type can belong to multiple spaces/extend multiple types.
|
|
15
|
+
*
|
|
16
|
+
* @since 0.2.0
|
|
17
|
+
*/
|
|
18
|
+
typeIds: Array<Grc20Id.Id>;
|
|
19
|
+
/**
|
|
20
|
+
* Record of property names to the `Id.Id` of the type in the Knowledge Graph
|
|
21
|
+
*
|
|
22
|
+
* @since 0.2.0
|
|
23
|
+
*/
|
|
24
|
+
properties?:
|
|
25
|
+
| {
|
|
26
|
+
[key: string]: Grc20Id.Id;
|
|
27
|
+
}
|
|
28
|
+
| undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Record of relation properties to the `Id.Id` of the type in the Knowledge Graph
|
|
31
|
+
*
|
|
32
|
+
* @since 0.2.0
|
|
33
|
+
*/
|
|
34
|
+
relations?:
|
|
35
|
+
| {
|
|
36
|
+
[key: string]: Grc20Id.Id;
|
|
37
|
+
}
|
|
38
|
+
| undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { Id } from '@graphprotocol/grc-20'
|
|
45
|
+
* import type { Mapping } from '@graphprotocol/hypergraph/mapping'
|
|
46
|
+
*
|
|
47
|
+
* const mapping: Mapping = {
|
|
48
|
+
* Account: {
|
|
49
|
+
* typeIds: [Id.Id('a5fd07b1-120f-46c6-b46f-387ef98396a6')],
|
|
50
|
+
* properties: {
|
|
51
|
+
* username: Id.Id('994edcff-6996-4a77-9797-a13e5e3efad8'),
|
|
52
|
+
* createdAt: Id.Id('64bfba51-a69b-4746-be4b-213214a879fe')
|
|
53
|
+
* }
|
|
54
|
+
* },
|
|
55
|
+
* Event: {
|
|
56
|
+
* typeIds: [Id.Id('0349187b-526f-435f-b2bb-9e9caf23127a')],
|
|
57
|
+
* properties: {
|
|
58
|
+
* name: Id.Id('3808e060-fb4a-4d08-8069-35b8c8a1902b'),
|
|
59
|
+
* description: Id.Id('1f0d9007-8da2-4b28-ab9f-3bc0709f4837'),
|
|
60
|
+
* },
|
|
61
|
+
* relations: {
|
|
62
|
+
* speaker: Id.Id('a5fd07b1-120f-46c6-b46f-387ef98396a6')
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @since 0.2.0
|
|
69
|
+
*/
|
|
70
|
+
export type Mapping = {
|
|
71
|
+
[key: string]: MappingEntry;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @since 0.2.0
|
|
76
|
+
*/
|
|
77
|
+
export type DataTypeRelation = `Relation(${string})`;
|
|
78
|
+
/**
|
|
79
|
+
* @since 0.2.0
|
|
80
|
+
*/
|
|
81
|
+
export function isDataTypeRelation(val: string): val is DataTypeRelation {
|
|
82
|
+
return /^Relation\((.+)\)$/.test(val);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* @since 0.2.0
|
|
86
|
+
*/
|
|
87
|
+
export const SchemaDataTypeRelation = EffectSchema.NonEmptyTrimmedString.pipe(
|
|
88
|
+
EffectSchema.filter((val) => isDataTypeRelation(val)),
|
|
89
|
+
);
|
|
90
|
+
/**
|
|
91
|
+
* @since 0.2.0
|
|
92
|
+
*/
|
|
93
|
+
export type SchemaDataTypeRelation = typeof SchemaDataTypeRelation.Type;
|
|
94
|
+
/**
|
|
95
|
+
* @since 0.2.0
|
|
96
|
+
*/
|
|
97
|
+
export const SchemaDataTypePrimitive = EffectSchema.Literal('Text', 'Number', 'Checkbox', 'Date', 'Point');
|
|
98
|
+
/**
|
|
99
|
+
* @since 0.2.0
|
|
100
|
+
*/
|
|
101
|
+
export type SchemaDataTypePrimitive = typeof SchemaDataTypePrimitive.Type;
|
|
102
|
+
/**
|
|
103
|
+
* @since 0.2.0
|
|
104
|
+
*/
|
|
105
|
+
export const SchemaDataType = EffectSchema.Union(SchemaDataTypePrimitive, SchemaDataTypeRelation);
|
|
106
|
+
/**
|
|
107
|
+
* @since 0.2.0
|
|
108
|
+
*/
|
|
109
|
+
export type SchemaDataType = typeof SchemaDataType.Type;
|
|
110
|
+
/**
|
|
111
|
+
* @since 0.2.0
|
|
112
|
+
*/
|
|
113
|
+
export const SchemaTypePropertyRelation = EffectSchema.Struct({
|
|
114
|
+
name: EffectSchema.NonEmptyTrimmedString,
|
|
115
|
+
knowledgeGraphId: EffectSchema.NullOr(EffectSchema.UUID),
|
|
116
|
+
dataType: SchemaDataTypeRelation,
|
|
117
|
+
relationType: EffectSchema.NonEmptyTrimmedString.annotations({
|
|
118
|
+
identifier: 'SchemaTypePropertyRelation.relationType',
|
|
119
|
+
description: 'name of the type within the schema that this property is related to',
|
|
120
|
+
examples: ['Account'],
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* @since 0.2.0
|
|
125
|
+
*/
|
|
126
|
+
export type SchemaTypePropertyRelation = typeof SchemaTypePropertyRelation.Type;
|
|
127
|
+
/**
|
|
128
|
+
* @since 0.2.0
|
|
129
|
+
*/
|
|
130
|
+
export const SchemaTypePropertyPrimitive = EffectSchema.Struct({
|
|
131
|
+
name: EffectSchema.NonEmptyTrimmedString,
|
|
132
|
+
knowledgeGraphId: EffectSchema.NullOr(EffectSchema.UUID),
|
|
133
|
+
dataType: SchemaDataTypePrimitive,
|
|
134
|
+
});
|
|
135
|
+
/**
|
|
136
|
+
* @since 0.2.0
|
|
137
|
+
*/
|
|
138
|
+
export type SchemaTypePropertyPrimitive = typeof SchemaTypePropertyPrimitive.Type;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @since 0.2.0
|
|
142
|
+
*/
|
|
143
|
+
export function propertyIsRelation(
|
|
144
|
+
property: SchemaTypePropertyPrimitive | SchemaTypePropertyRelation,
|
|
145
|
+
): property is SchemaTypePropertyRelation {
|
|
146
|
+
return isDataTypeRelation(property.dataType);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @since 0.2.0
|
|
151
|
+
*/
|
|
152
|
+
export const SchemaType = EffectSchema.Struct({
|
|
153
|
+
name: EffectSchema.NonEmptyTrimmedString,
|
|
154
|
+
knowledgeGraphId: EffectSchema.NullOr(EffectSchema.UUID),
|
|
155
|
+
properties: EffectSchema.Array(EffectSchema.Union(SchemaTypePropertyPrimitive, SchemaTypePropertyRelation)).pipe(
|
|
156
|
+
EffectSchema.minItems(1),
|
|
157
|
+
EffectSchema.filter(namesAreUnique, {
|
|
158
|
+
identifier: 'DuplicatePropertyNames',
|
|
159
|
+
jsonSchema: {},
|
|
160
|
+
description: 'The property.name must be unique across all properties in the type',
|
|
161
|
+
}),
|
|
162
|
+
),
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* @since 0.2.0
|
|
166
|
+
*/
|
|
167
|
+
export type SchemaType = typeof SchemaType.Type;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Represents the user-built schema object to generate a `Mappings` definition for
|
|
171
|
+
*
|
|
172
|
+
* @since 0.2.0
|
|
173
|
+
*/
|
|
174
|
+
export const Schema = EffectSchema.Struct({
|
|
175
|
+
types: EffectSchema.Array(SchemaType).pipe(
|
|
176
|
+
EffectSchema.minItems(1),
|
|
177
|
+
EffectSchema.filter(namesAreUnique, {
|
|
178
|
+
identifier: 'DuplicateTypeNames',
|
|
179
|
+
jsonSchema: {},
|
|
180
|
+
description: 'The type.name must be unique across all types in the schema',
|
|
181
|
+
}),
|
|
182
|
+
EffectSchema.filter(allRelationPropertyTypesExist, {
|
|
183
|
+
identifier: 'AllRelationTypesExist',
|
|
184
|
+
jsonSchema: {},
|
|
185
|
+
description: 'Each type property of dataType RELATION must have a type of the same name in the schema',
|
|
186
|
+
}),
|
|
187
|
+
),
|
|
188
|
+
}).annotations({
|
|
189
|
+
identifier: 'typesync/Schema',
|
|
190
|
+
title: 'TypeSync app Schema',
|
|
191
|
+
description: 'An array of types in the schema defined by the user to generate a Mapping object for',
|
|
192
|
+
examples: [
|
|
193
|
+
{
|
|
194
|
+
types: [
|
|
195
|
+
{
|
|
196
|
+
name: 'Account',
|
|
197
|
+
knowledgeGraphId: null,
|
|
198
|
+
properties: [{ name: 'username', knowledgeGraphId: null, dataType: 'Text' }],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
types: [
|
|
204
|
+
{
|
|
205
|
+
name: 'Account',
|
|
206
|
+
knowledgeGraphId: 'a5fd07b1-120f-46c6-b46f-387ef98396a6',
|
|
207
|
+
properties: [{ name: 'name', knowledgeGraphId: 'a126ca53-0c8e-48d5-b888-82c734c38935', dataType: 'Text' }],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
/**
|
|
214
|
+
* @since 0.2.0
|
|
215
|
+
*/
|
|
216
|
+
export type Schema = typeof Schema.Type;
|
|
217
|
+
/**
|
|
218
|
+
* @since 0.2.0
|
|
219
|
+
*/
|
|
220
|
+
export const SchemaKnownDecoder = EffectSchema.decodeSync(Schema);
|
|
221
|
+
/**
|
|
222
|
+
* @since 0.2.0
|
|
223
|
+
*/
|
|
224
|
+
export const SchemaUnknownDecoder = EffectSchema.decodeUnknownSync(Schema);
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Iterate through all properties in all types in the schema of `dataType` === `Relation(${string})`
|
|
228
|
+
* and validate that the schema.types have a type for the existing relation
|
|
229
|
+
*
|
|
230
|
+
* @example <caption>All types exist</caption>
|
|
231
|
+
* ```ts
|
|
232
|
+
* import { allRelationPropertyTypesExist, type Mapping } from '@graphprotocol/hypergraph/mapping'
|
|
233
|
+
*
|
|
234
|
+
* const types: Mapping['types'] = [
|
|
235
|
+
* {
|
|
236
|
+
* name: "Account",
|
|
237
|
+
* knowledgeGraphId: null,
|
|
238
|
+
* properties: [
|
|
239
|
+
* {
|
|
240
|
+
* name: "username",
|
|
241
|
+
* dataType: "Text",
|
|
242
|
+
* knowledgeGraphId: null
|
|
243
|
+
* }
|
|
244
|
+
* ]
|
|
245
|
+
* },
|
|
246
|
+
* {
|
|
247
|
+
* name: "Event",
|
|
248
|
+
* knowledgeGraphId: null,
|
|
249
|
+
* properties: [
|
|
250
|
+
* {
|
|
251
|
+
* name: "speaker",
|
|
252
|
+
* dataType: "Relation(Account)"
|
|
253
|
+
* relationType: "Account",
|
|
254
|
+
* knowledgeGraphId: null,
|
|
255
|
+
* }
|
|
256
|
+
* ]
|
|
257
|
+
* }
|
|
258
|
+
* ]
|
|
259
|
+
* expect(allRelationPropertyTypesExist(types)).toEqual(true)
|
|
260
|
+
* ```
|
|
261
|
+
*
|
|
262
|
+
* @example <caption>Account type is missing</caption>
|
|
263
|
+
* ```ts
|
|
264
|
+
* import { allRelationPropertyTypesExist, type Mapping } from '@graphprotocol/hypergraph/mapping'
|
|
265
|
+
*
|
|
266
|
+
* const types: Mapping['types'] = [
|
|
267
|
+
* {
|
|
268
|
+
* name: "Event",
|
|
269
|
+
* knowledgeGraphId: null,
|
|
270
|
+
* properties: [
|
|
271
|
+
* {
|
|
272
|
+
* name: "speaker",
|
|
273
|
+
* dataType: "Relation(Account)",
|
|
274
|
+
* relationType: "Account",
|
|
275
|
+
* knowledgeGraphId: null,
|
|
276
|
+
* }
|
|
277
|
+
* ]
|
|
278
|
+
* }
|
|
279
|
+
* ]
|
|
280
|
+
* expect(allRelationPropertyTypesExist(types)).toEqual(false)
|
|
281
|
+
* ```
|
|
282
|
+
*
|
|
283
|
+
* @since 0.2.0
|
|
284
|
+
*
|
|
285
|
+
* @param types the user-submitted schema types
|
|
286
|
+
*/
|
|
287
|
+
export function allRelationPropertyTypesExist(types: ReadonlyArray<SchemaType>): boolean {
|
|
288
|
+
const unqTypeNames = EffectArray.reduce(types, new Set<string>(), (names, curr) => names.add(curr.name));
|
|
289
|
+
return pipe(
|
|
290
|
+
types,
|
|
291
|
+
EffectArray.flatMap((curr) => curr.properties),
|
|
292
|
+
EffectArray.filter((prop) => propertyIsRelation(prop)),
|
|
293
|
+
EffectArray.every((prop) => unqTypeNames.has(prop.relationType)),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export type GenerateMappingResult = [mapping: Mapping, ops: ReadonlyArray<Op>];
|
|
298
|
+
|
|
299
|
+
// Helper types for internal processing
|
|
300
|
+
type PropertyIdMapping = { propName: string; id: Grc20Id.Id };
|
|
301
|
+
type TypeIdMapping = Map<string, Grc20Id.Id | null>;
|
|
302
|
+
type ProcessedProperty =
|
|
303
|
+
| { type: 'resolved'; mapping: PropertyIdMapping; ops: Array<Op> }
|
|
304
|
+
| { type: 'deferred'; property: SchemaTypePropertyRelation };
|
|
305
|
+
|
|
306
|
+
type ProcessedType =
|
|
307
|
+
| { type: 'complete'; entry: MappingEntry & { typeName: string }; ops: Array<Op> }
|
|
308
|
+
| {
|
|
309
|
+
type: 'deferred';
|
|
310
|
+
schemaType: SchemaType;
|
|
311
|
+
properties: Array<PropertyIdMapping>;
|
|
312
|
+
relations: Array<PropertyIdMapping>;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Helper function to build property map from PropertyIdMappings
|
|
316
|
+
function buildPropertyMap(properties: Array<PropertyIdMapping>): MappingEntry['properties'] {
|
|
317
|
+
return pipe(
|
|
318
|
+
properties,
|
|
319
|
+
EffectArray.reduce({} as NonNullable<MappingEntry['properties']>, (props, { propName, id }) => {
|
|
320
|
+
props[toCamelCase(propName)] = id;
|
|
321
|
+
return props;
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Helper function to build relation map from PropertyIdMappings
|
|
327
|
+
function buildRelationMap(relations: Array<PropertyIdMapping>): MappingEntry['relations'] {
|
|
328
|
+
return pipe(
|
|
329
|
+
relations,
|
|
330
|
+
EffectArray.reduce({} as NonNullable<MappingEntry['relations']>, (rels, { propName, id }) => {
|
|
331
|
+
rels[toCamelCase(propName)] = id;
|
|
332
|
+
return rels;
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Helper function to create a property and return the result
|
|
338
|
+
function createPropertyWithOps(
|
|
339
|
+
property: SchemaTypePropertyPrimitive | SchemaTypePropertyRelation,
|
|
340
|
+
typeIdMap: TypeIdMapping,
|
|
341
|
+
): ProcessedProperty {
|
|
342
|
+
if (property.knowledgeGraphId) {
|
|
343
|
+
return {
|
|
344
|
+
type: 'resolved',
|
|
345
|
+
mapping: { propName: property.name, id: Grc20Id.Id(property.knowledgeGraphId) },
|
|
346
|
+
ops: [],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (propertyIsRelation(property)) {
|
|
351
|
+
const relationTypeId = typeIdMap.get(property.relationType);
|
|
352
|
+
if (relationTypeId == null) {
|
|
353
|
+
return { type: 'deferred', property };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const { id, ops } = Graph.createProperty({
|
|
357
|
+
name: property.name,
|
|
358
|
+
dataType: 'RELATION',
|
|
359
|
+
relationValueTypes: [relationTypeId],
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
type: 'resolved',
|
|
363
|
+
mapping: { propName: property.name, id },
|
|
364
|
+
ops,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const { id, ops } = Graph.createProperty({
|
|
369
|
+
name: property.name,
|
|
370
|
+
dataType: mapSchemaDataTypeToGRC20PropDataType(property.dataType),
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
type: 'resolved',
|
|
374
|
+
mapping: { propName: property.name, id },
|
|
375
|
+
ops,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Helper function to process a single type
|
|
380
|
+
function processType(type: SchemaType, typeIdMap: TypeIdMapping): ProcessedType {
|
|
381
|
+
const processedProperties = pipe(
|
|
382
|
+
type.properties,
|
|
383
|
+
EffectArray.map((prop) => createPropertyWithOps(prop, typeIdMap)),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const resolvedProperties = pipe(
|
|
387
|
+
processedProperties,
|
|
388
|
+
EffectArray.filterMap((p) => (p.type === 'resolved' ? Option.some(p) : Option.none())),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const deferredProperties = pipe(
|
|
392
|
+
processedProperties,
|
|
393
|
+
EffectArray.filterMap((p) => (p.type === 'deferred' ? Option.some(p.property) : Option.none())),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// Separate resolved properties into primitive properties and relations
|
|
397
|
+
const primitiveProperties = pipe(
|
|
398
|
+
resolvedProperties,
|
|
399
|
+
EffectArray.filter((p) => {
|
|
400
|
+
const originalProp = type.properties.find((prop) => prop.name === p.mapping.propName);
|
|
401
|
+
return originalProp ? !propertyIsRelation(originalProp) : false;
|
|
402
|
+
}),
|
|
403
|
+
EffectArray.map((p) => p.mapping),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const relationProperties = pipe(
|
|
407
|
+
resolvedProperties,
|
|
408
|
+
EffectArray.filter((p) => {
|
|
409
|
+
const originalProp = type.properties.find((prop) => prop.name === p.mapping.propName);
|
|
410
|
+
return originalProp ? propertyIsRelation(originalProp) : false;
|
|
411
|
+
}),
|
|
412
|
+
EffectArray.map((p) => p.mapping),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
const propertyOps = pipe(
|
|
416
|
+
resolvedProperties,
|
|
417
|
+
EffectArray.flatMap((p) => p.ops),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// If type exists in knowledge graph, return complete entry
|
|
421
|
+
if (type.knowledgeGraphId) {
|
|
422
|
+
const entry: MappingEntry & { typeName: string } = {
|
|
423
|
+
typeName: toPascalCase(type.name),
|
|
424
|
+
typeIds: [Grc20Id.Id(type.knowledgeGraphId)],
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
if (EffectArray.isNonEmptyArray(primitiveProperties)) {
|
|
428
|
+
entry.properties = buildPropertyMap(primitiveProperties);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (EffectArray.isNonEmptyArray(relationProperties)) {
|
|
432
|
+
entry.relations = buildRelationMap(relationProperties);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
type: 'complete',
|
|
437
|
+
entry,
|
|
438
|
+
ops: propertyOps,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// If there are deferred properties, defer type creation
|
|
443
|
+
if (EffectArray.isNonEmptyArray(deferredProperties)) {
|
|
444
|
+
return {
|
|
445
|
+
type: 'deferred',
|
|
446
|
+
schemaType: type,
|
|
447
|
+
properties: primitiveProperties,
|
|
448
|
+
relations: relationProperties,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Create the type with all resolved properties (both primitive and relations)
|
|
453
|
+
const allPropertyIds = [...primitiveProperties, ...relationProperties];
|
|
454
|
+
const { id, ops: typeOps } = Graph.createType({
|
|
455
|
+
name: type.name,
|
|
456
|
+
properties: pipe(
|
|
457
|
+
allPropertyIds,
|
|
458
|
+
EffectArray.map((p) => p.id),
|
|
459
|
+
),
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
typeIdMap.set(type.name, id);
|
|
463
|
+
|
|
464
|
+
const entry: MappingEntry & { typeName: string } = {
|
|
465
|
+
typeName: toPascalCase(type.name),
|
|
466
|
+
typeIds: [id],
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
if (EffectArray.isNonEmptyArray(primitiveProperties)) {
|
|
470
|
+
entry.properties = buildPropertyMap(primitiveProperties);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (EffectArray.isNonEmptyArray(relationProperties)) {
|
|
474
|
+
entry.relations = buildRelationMap(relationProperties);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
type: 'complete',
|
|
479
|
+
entry,
|
|
480
|
+
ops: [...propertyOps, ...typeOps],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Takes the user-submitted schema, validates it, and build the `Mapping` definition for the schema as well as the GRC-20 Ops needed to publish the schema/schema changes to the Knowledge Graph.
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* ```ts
|
|
489
|
+
* import { Id } from "@graphprotocol/grc-20"
|
|
490
|
+
* import { generateMapping } from "@graphprotocol/typesync"
|
|
491
|
+
*
|
|
492
|
+
* const schema: Schema = {
|
|
493
|
+
* types: [
|
|
494
|
+
* {
|
|
495
|
+
* name: "Account",
|
|
496
|
+
* knowledgeGraphId: "a5fd07b1-120f-46c6-b46f-387ef98396a6",
|
|
497
|
+
* properties: [
|
|
498
|
+
* {
|
|
499
|
+
* name: "username",
|
|
500
|
+
* dataType: "Text",
|
|
501
|
+
* knowledgeGraphId: "994edcff-6996-4a77-9797-a13e5e3efad8"
|
|
502
|
+
* },
|
|
503
|
+
* {
|
|
504
|
+
* name: "createdAt",
|
|
505
|
+
* dataType: "Date",
|
|
506
|
+
* knowledgeGraphId: null
|
|
507
|
+
* }
|
|
508
|
+
* ]
|
|
509
|
+
* },
|
|
510
|
+
* {
|
|
511
|
+
* name: "Event",
|
|
512
|
+
* knowledgeGraphId: null,
|
|
513
|
+
* properties: [
|
|
514
|
+
* {
|
|
515
|
+
* name: "name",
|
|
516
|
+
* dataType: "Text",
|
|
517
|
+
* knowledgeGraphId: "3808e060-fb4a-4d08-8069-35b8c8a1902b"
|
|
518
|
+
* },
|
|
519
|
+
* {
|
|
520
|
+
* name: "description",
|
|
521
|
+
* dataType: "Text",
|
|
522
|
+
* knowledgeGraphId: null
|
|
523
|
+
* },
|
|
524
|
+
* {
|
|
525
|
+
* name: "speaker",
|
|
526
|
+
* dataType: "Relation(Account)",
|
|
527
|
+
* relationType: "Account",
|
|
528
|
+
* knowledgeGraphId: null
|
|
529
|
+
* }
|
|
530
|
+
* ]
|
|
531
|
+
* }
|
|
532
|
+
* ],
|
|
533
|
+
* }
|
|
534
|
+
* const [mapping, ops] = generateMapping(schema)
|
|
535
|
+
*
|
|
536
|
+
* expect(mapping).toEqual({
|
|
537
|
+
* Account: {
|
|
538
|
+
* typeIds: [Id.Id("a5fd07b1-120f-46c6-b46f-387ef98396a6")], // comes from input schema
|
|
539
|
+
* properties: {
|
|
540
|
+
* username: Id.Id("994edcff-6996-4a77-9797-a13e5e3efad8"), // comes from input schema
|
|
541
|
+
* createdAt: Id.Id("8cd7d9ac-a878-4287-8000-e71e6f853117"), // generated from Graph.createProperty Op
|
|
542
|
+
* }
|
|
543
|
+
* },
|
|
544
|
+
* Event: {
|
|
545
|
+
* typeIds: [Id.Id("20b3fe39-8e62-41a0-b9cb-92743fd760da")], // generated from Graph.createType Op
|
|
546
|
+
* properties: {
|
|
547
|
+
* name: Id.Id("3808e060-fb4a-4d08-8069-35b8c8a1902b"), // comes from input schema
|
|
548
|
+
* description: Id.Id("8fc4e17c-7581-4d6c-a712-943385afc7b5"), // generated from Graph.createProperty Op
|
|
549
|
+
* },
|
|
550
|
+
* relations: {
|
|
551
|
+
* speaker: Id.Id("651ce59f-643b-4931-bf7a-5dc0ca0f5a47"), // generated from Graph.createProperty Op
|
|
552
|
+
* }
|
|
553
|
+
* }
|
|
554
|
+
* })
|
|
555
|
+
* expect(ops).toEqual([
|
|
556
|
+
* // Graph.createProperty Op for Account.createdAt property
|
|
557
|
+
* {
|
|
558
|
+
* type: "CREATE_PROPERTY",
|
|
559
|
+
* property: {
|
|
560
|
+
* id: Id.Id("8cd7d9ac-a878-4287-8000-e71e6f853117"),
|
|
561
|
+
* dataType: "TEXT"
|
|
562
|
+
* }
|
|
563
|
+
* },
|
|
564
|
+
* // Graph.createProperty Op for Event.description property
|
|
565
|
+
* {
|
|
566
|
+
* type: "CREATE_PROPERTY",
|
|
567
|
+
* property: {
|
|
568
|
+
* id: Id.Id("8fc4e17c-7581-4d6c-a712-943385afc7b5"),
|
|
569
|
+
* dataType: "TEXT"
|
|
570
|
+
* }
|
|
571
|
+
* },
|
|
572
|
+
* // Graph.createProperty Op for Event.speaker property
|
|
573
|
+
* {
|
|
574
|
+
* type: "CREATE_PROPERTY",
|
|
575
|
+
* property: {
|
|
576
|
+
* id: Id.Id("651ce59f-643b-4931-bf7a-5dc0ca0f5a47"),
|
|
577
|
+
* dataType: "RELATION"
|
|
578
|
+
* }
|
|
579
|
+
* },
|
|
580
|
+
* // Graph.createType Op for Event type
|
|
581
|
+
* {
|
|
582
|
+
* type: "CREATE_PROPERTY",
|
|
583
|
+
* property: {
|
|
584
|
+
* id: Id.Id("651ce59f-643b-4931-bf7a-5dc0ca0f5a47"),
|
|
585
|
+
* dataType: "RELATION"
|
|
586
|
+
* }
|
|
587
|
+
* },
|
|
588
|
+
* ])
|
|
589
|
+
* ```
|
|
590
|
+
*
|
|
591
|
+
* @since 0.2.0
|
|
592
|
+
*
|
|
593
|
+
* @param input user-built and submitted schema
|
|
594
|
+
* @returns the generated [Mapping] definition from the submitted schema as well as the GRC-20 Ops required to publish the schema to the Knowledge Graph
|
|
595
|
+
*/
|
|
596
|
+
export function generateMapping(input: Schema): GenerateMappingResult {
|
|
597
|
+
// Validate the schema
|
|
598
|
+
const schema = SchemaKnownDecoder(input);
|
|
599
|
+
|
|
600
|
+
// Build initial type ID map
|
|
601
|
+
const typeIdMap: TypeIdMapping = pipe(
|
|
602
|
+
schema.types,
|
|
603
|
+
EffectArray.reduce(new Map<string, Grc20Id.Id | null>(), (map, type) =>
|
|
604
|
+
map.set(type.name, type.knowledgeGraphId != null ? Grc20Id.Id(type.knowledgeGraphId) : null),
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
// First pass: process all types
|
|
609
|
+
const processedTypes = pipe(
|
|
610
|
+
schema.types,
|
|
611
|
+
EffectArray.map((type) => processType(type, typeIdMap)),
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
// Separate complete and deferred types
|
|
615
|
+
const [deferredTypes, completeTypes] = pipe(
|
|
616
|
+
processedTypes,
|
|
617
|
+
EffectArray.partition(
|
|
618
|
+
(result): result is Extract<ProcessedType, { type: 'complete' }> => result.type === 'complete',
|
|
619
|
+
),
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Collect all operations from first pass
|
|
623
|
+
const firstPassOps = pipe(
|
|
624
|
+
completeTypes,
|
|
625
|
+
EffectArray.flatMap((t) => t.ops),
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Second pass: resolve deferred relation properties and create deferred types
|
|
629
|
+
const { entries: deferredEntries, ops: secondPassOps } = pipe(
|
|
630
|
+
deferredTypes,
|
|
631
|
+
EffectArray.reduce(
|
|
632
|
+
{ entries: [] as Array<MappingEntry & { typeName: string }>, ops: [] as Array<Op> },
|
|
633
|
+
(acc, deferred) => {
|
|
634
|
+
// Resolve all deferred relation properties for this type
|
|
635
|
+
const resolvedRelations = pipe(
|
|
636
|
+
deferred.schemaType.properties,
|
|
637
|
+
EffectArray.filterMap((prop) => {
|
|
638
|
+
if (!propertyIsRelation(prop) || prop.knowledgeGraphId != null) {
|
|
639
|
+
return Option.none();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const relationTypeId = typeIdMap.get(prop.relationType);
|
|
643
|
+
if (relationTypeId == null) {
|
|
644
|
+
throw new RelationValueTypeDoesNotExistError({
|
|
645
|
+
message: `Failed to resolve type ID for relation type: ${prop.relationType}`,
|
|
646
|
+
property: prop.name,
|
|
647
|
+
relatedType: prop.relationType,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const { id, ops } = Graph.createProperty({
|
|
652
|
+
name: prop.name,
|
|
653
|
+
dataType: 'RELATION',
|
|
654
|
+
relationValueTypes: [relationTypeId],
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
return Option.some({ mapping: { propName: prop.name, id }, ops });
|
|
658
|
+
}),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
// Combine resolved relations with existing relations
|
|
662
|
+
const allRelations = [
|
|
663
|
+
...deferred.relations,
|
|
664
|
+
...pipe(
|
|
665
|
+
resolvedRelations,
|
|
666
|
+
EffectArray.map((r) => r.mapping),
|
|
667
|
+
),
|
|
668
|
+
];
|
|
669
|
+
|
|
670
|
+
// Combine all property IDs for type creation
|
|
671
|
+
const allPropertyIds = [...deferred.properties, ...allRelations];
|
|
672
|
+
|
|
673
|
+
// Create the type with all properties
|
|
674
|
+
const { id, ops: typeOps } = Graph.createType({
|
|
675
|
+
name: deferred.schemaType.name,
|
|
676
|
+
properties: pipe(
|
|
677
|
+
allPropertyIds,
|
|
678
|
+
EffectArray.map((p) => p.id),
|
|
679
|
+
),
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
typeIdMap.set(deferred.schemaType.name, id);
|
|
683
|
+
|
|
684
|
+
// Collect all operations
|
|
685
|
+
const allOps = [
|
|
686
|
+
...pipe(
|
|
687
|
+
resolvedRelations,
|
|
688
|
+
EffectArray.flatMap((r) => r.ops),
|
|
689
|
+
),
|
|
690
|
+
...typeOps,
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
// Build the entry with properties and relations separated
|
|
694
|
+
const entry: MappingEntry & { typeName: string } = {
|
|
695
|
+
typeName: toPascalCase(deferred.schemaType.name),
|
|
696
|
+
typeIds: [id],
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
if (EffectArray.isNonEmptyArray(deferred.properties)) {
|
|
700
|
+
entry.properties = buildPropertyMap(deferred.properties);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (EffectArray.isNonEmptyArray(allRelations)) {
|
|
704
|
+
entry.relations = buildRelationMap(allRelations);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
entries: [...acc.entries, entry],
|
|
709
|
+
ops: [...acc.ops, ...allOps],
|
|
710
|
+
};
|
|
711
|
+
},
|
|
712
|
+
),
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
// Combine all entries and build final mapping
|
|
716
|
+
const allEntries = [
|
|
717
|
+
...pipe(
|
|
718
|
+
completeTypes,
|
|
719
|
+
EffectArray.map((t) => t.entry),
|
|
720
|
+
),
|
|
721
|
+
...deferredEntries,
|
|
722
|
+
];
|
|
723
|
+
|
|
724
|
+
const mapping = pipe(
|
|
725
|
+
allEntries,
|
|
726
|
+
EffectArray.reduce({} as Mapping, (mapping, entry) => {
|
|
727
|
+
const { typeName, ...rest } = entry;
|
|
728
|
+
mapping[typeName] = rest;
|
|
729
|
+
return mapping;
|
|
730
|
+
}),
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
return [mapping, [...firstPassOps, ...secondPassOps]] as const;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export class RelationValueTypeDoesNotExistError extends Data.TaggedError(
|
|
737
|
+
'/typesync/errors/RelationValueTypeDoesNotExistError',
|
|
738
|
+
)<{
|
|
739
|
+
readonly message: string;
|
|
740
|
+
readonly property: string;
|
|
741
|
+
readonly relatedType: string;
|
|
742
|
+
}> {}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* @since 0.2.0
|
|
746
|
+
*
|
|
747
|
+
* @param dataType the dataType from the user-submitted schema
|
|
748
|
+
* @returns the mapped to GRC-20 dataType for the GRC-20 ops
|
|
749
|
+
*/
|
|
750
|
+
export function mapSchemaDataTypeToGRC20PropDataType(dataType: SchemaDataType): CreatePropertyParams['dataType'] {
|
|
751
|
+
switch (true) {
|
|
752
|
+
case dataType === 'Checkbox': {
|
|
753
|
+
return 'CHECKBOX';
|
|
754
|
+
}
|
|
755
|
+
case dataType === 'Date': {
|
|
756
|
+
return 'TIME';
|
|
757
|
+
}
|
|
758
|
+
case dataType === 'Number': {
|
|
759
|
+
return 'NUMBER';
|
|
760
|
+
}
|
|
761
|
+
case dataType === 'Point': {
|
|
762
|
+
return 'POINT';
|
|
763
|
+
}
|
|
764
|
+
case isDataTypeRelation(dataType): {
|
|
765
|
+
return 'RELATION';
|
|
766
|
+
}
|
|
767
|
+
default: {
|
|
768
|
+
return 'TEXT';
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|