@graphprotocol/hypergraph 0.1.0 → 0.3.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.
Files changed (40) hide show
  1. package/dist/entity/findMany.js.map +1 -1
  2. package/dist/entity/types.d.ts +4 -4
  3. package/dist/entity/types.d.ts.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/mapping/Mapping.d.ts +418 -0
  9. package/dist/mapping/Mapping.d.ts.map +1 -0
  10. package/dist/mapping/Mapping.js +554 -0
  11. package/dist/mapping/Mapping.js.map +1 -0
  12. package/dist/mapping/Utils.d.ts +74 -0
  13. package/dist/mapping/Utils.d.ts.map +1 -0
  14. package/dist/mapping/Utils.js +144 -0
  15. package/dist/mapping/Utils.js.map +1 -0
  16. package/dist/mapping/index.d.ts +3 -0
  17. package/dist/mapping/index.d.ts.map +1 -0
  18. package/dist/mapping/index.js +3 -0
  19. package/dist/mapping/index.js.map +1 -0
  20. package/dist/store.d.ts +1 -1
  21. package/dist/store.d.ts.map +1 -1
  22. package/dist/store.js.map +1 -1
  23. package/dist/type/type.d.ts +3 -2
  24. package/dist/type/type.d.ts.map +1 -1
  25. package/dist/type/type.js +5 -2
  26. package/dist/type/type.js.map +1 -1
  27. package/dist/type-utils/type-utils.d.ts +7 -0
  28. package/dist/type-utils/type-utils.d.ts.map +1 -0
  29. package/dist/type-utils/type-utils.js +41 -0
  30. package/dist/type-utils/type-utils.js.map +1 -0
  31. package/package.json +3 -5
  32. package/src/entity/findMany.ts +4 -4
  33. package/src/entity/types.ts +4 -4
  34. package/src/index.ts +2 -0
  35. package/src/mapping/Mapping.ts +771 -0
  36. package/src/mapping/Utils.ts +156 -0
  37. package/src/mapping/index.ts +2 -0
  38. package/src/store.ts +1 -1
  39. package/src/type/type.ts +6 -2
  40. package/src/type-utils/type-utils.ts +46 -0
@@ -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('String', 'Number', 'Boolean', '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: 'String' }],
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: 'String' }],
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 === 'Boolean': {
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
+ }