Skip to content

Commit db012c1

Browse files
committed
feat(typesync pkg): expose more types. add relationType to relation prop. add schema validation
1 parent c05f1af commit db012c1

File tree

2 files changed

+253
-28
lines changed

2 files changed

+253
-28
lines changed

packages/typesync/src/Mapping.ts

Lines changed: 159 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -82,47 +82,96 @@ export type SchemaDataTypeRelation = typeof SchemaDataTypeRelation.Type;
8282
/**
8383
* @since 0.0.1
8484
*/
85-
export const SchemaDataType = EffectSchema.Union(
86-
EffectSchema.Literal('Text', 'Number', 'Boolean', 'Date', 'Point', 'Url'),
87-
SchemaDataTypeRelation,
88-
);
85+
export const SchemaDataTypePrimitive = EffectSchema.Literal('Text', 'Number', 'Boolean', 'Date', 'Point', 'Url');
86+
/**
87+
* @since 0.0.1
88+
*/
89+
export type SchemaDataTypePrimitive = typeof SchemaDataTypePrimitive.Type;
90+
/**
91+
* @since 0.0.1
92+
*/
93+
export const SchemaDataType = EffectSchema.Union(SchemaDataTypePrimitive, SchemaDataTypeRelation);
8994
/**
9095
* @since 0.0.1
9196
*/
9297
export type SchemaDataType = typeof SchemaDataType.Type;
98+
/**
99+
* @since 0.0.1
100+
*/
101+
export const SchemaTypePropertyRelation = EffectSchema.Struct({
102+
name: EffectSchema.NonEmptyTrimmedString,
103+
knowledgeGraphId: EffectSchema.NullOr(EffectSchema.UUID),
104+
dataType: SchemaDataTypeRelation,
105+
relationType: EffectSchema.NonEmptyTrimmedString.annotations({
106+
identifier: 'SchemaTypePropertyRelation.relationType',
107+
description: 'name of the type within the schema that this property is related to',
108+
examples: ['Account'],
109+
}),
110+
});
111+
/**
112+
* @since 0.0.1
113+
*/
114+
export type SchemaTypePropertyRelation = typeof SchemaTypePropertyRelation.Type;
115+
/**
116+
* @since 0.0.1
117+
*/
118+
export const SchemaTypePropertyPrimitive = EffectSchema.Struct({
119+
name: EffectSchema.NonEmptyTrimmedString,
120+
knowledgeGraphId: EffectSchema.NullOr(EffectSchema.UUID),
121+
dataType: SchemaDataTypePrimitive,
122+
});
123+
/**
124+
* @since 0.0.1
125+
*/
126+
export type SchemaTypePropertyPrimitive = typeof SchemaTypePropertyPrimitive.Type;
127+
128+
/**
129+
* @since 0.0.1
130+
*/
131+
export function propertyIsRelation(
132+
property: SchemaTypePropertyPrimitive | SchemaTypePropertyRelation,
133+
): property is SchemaTypePropertyRelation {
134+
return isDataTypeRelation(property.dataType);
135+
}
136+
137+
/**
138+
* @since 0.0.1
139+
*/
140+
export const SchemaType = EffectSchema.Struct({
141+
name: EffectSchema.NonEmptyTrimmedString,
142+
knowledgeGraphId: EffectSchema.NullOr(EffectSchema.UUID),
143+
properties: EffectSchema.Array(EffectSchema.Union(SchemaTypePropertyPrimitive, SchemaTypePropertyRelation)).pipe(
144+
EffectSchema.minItems(1),
145+
EffectSchema.filter(namesAreUnique, {
146+
identifier: 'DuplicatePropertyNames',
147+
jsonSchema: {},
148+
description: 'The property.name must be unique across all properties in the type',
149+
}),
150+
),
151+
});
152+
/**
153+
* @since 0.0.1
154+
*/
155+
export type SchemaType = typeof SchemaType.Type;
93156

94157
/**
95158
* Represents the user-built schema object to generate a `Mappings` definition for
96159
*
97160
* @since 0.0.1
98161
*/
99162
export const Schema = EffectSchema.Struct({
100-
types: EffectSchema.Array(
101-
EffectSchema.Struct({
102-
name: EffectSchema.NonEmptyTrimmedString,
103-
knowledgeGraphId: EffectSchema.NullOr(EffectSchema.UUID),
104-
properties: EffectSchema.Array(
105-
EffectSchema.Struct({
106-
name: EffectSchema.NonEmptyTrimmedString,
107-
knowledgeGraphId: EffectSchema.NullOr(EffectSchema.UUID),
108-
dataType: SchemaDataType,
109-
}),
110-
).pipe(
111-
EffectSchema.minItems(1),
112-
EffectSchema.filter(namesAreUnique, {
113-
identifier: 'DuplicatePropertyNames',
114-
jsonSchema: {},
115-
description: 'The property.name must be unique across all properties in the type',
116-
}),
117-
),
118-
}),
119-
).pipe(
163+
types: EffectSchema.Array(SchemaType).pipe(
120164
EffectSchema.minItems(1),
121165
EffectSchema.filter(namesAreUnique, {
122166
identifier: 'DuplicateTypeNames',
123167
jsonSchema: {},
124168
description: 'The type.name must be unique across all types in the schema',
125169
}),
170+
EffectSchema.filter(allRelationPropertyTypesExist, {
171+
identifier: 'AllRelationTypesExist',
172+
jsonSchema: {},
173+
description: 'Each type property of dataType RELATION must have a type of the same name in the schema',
174+
}),
126175
),
127176
}).annotations({
128177
identifier: 'typesync/Schema',
@@ -163,14 +212,88 @@ export const SchemaKnownDecoder = EffectSchema.decodeSync(Schema);
163212
export const SchemaUnknownDecoder = EffectSchema.decodeUnknownSync(Schema);
164213

165214
/**
215+
* Iterate through all properties in all types in the schema of `dataType` === `Relation(${string})`
216+
* and validate that the schema.types have a type for the existing relation
166217
*
218+
* @example <caption>All types exist</caption>
219+
* ```ts
220+
* import { allRelationPropertyTypesExist, type Mapping } from '@graphprotocol/typesync/Mapping'
221+
*
222+
* const types: Mapping['types'] = [
223+
* {
224+
* name: "Account",
225+
* knowledgeGraphId: null,
226+
* properties: [
227+
* {
228+
* name: "username",
229+
* dataType: "Text",
230+
* knowledgeGraphId: null
231+
* }
232+
* ]
233+
* },
234+
* {
235+
* name: "Event",
236+
* knowledgeGraphId: null,
237+
* properties: [
238+
* {
239+
* name: "speaker",
240+
* dataType: "Relation(Account)"
241+
* relationType: "Account",
242+
* knowledgeGraphId: null,
243+
* }
244+
* ]
245+
* }
246+
* ]
247+
* expect(allRelationPropertyTypesExist(types)).toEqual(true)
248+
* ```
249+
*
250+
* @example <caption>Account type is missing</caption>
251+
* ```ts
252+
* import { allRelationPropertyTypesExist, type Mapping } from '@graphprotocol/typesync/Mapping'
253+
*
254+
* const types: Mapping['types'] = [
255+
* {
256+
* name: "Event",
257+
* knowledgeGraphId: null,
258+
* properties: [
259+
* {
260+
* name: "speaker",
261+
* dataType: "Relation(Account)",
262+
* relationType: "Account",
263+
* knowledgeGraphId: null,
264+
* }
265+
* ]
266+
* }
267+
* ]
268+
* expect(allRelationPropertyTypesExist(types)).toEqual(false)
269+
* ```
270+
*
271+
* @since 0.0.1
272+
*
273+
* @param types the user-submitted schema types
274+
*/
275+
export function allRelationPropertyTypesExist(types: ReadonlyArray<SchemaType>): boolean {
276+
const unqTypeNames = EffectArray.reduce(types, new Set<string>(), (names, curr) => names.add(curr.name));
277+
return pipe(
278+
types,
279+
EffectArray.flatMap((curr) => curr.properties),
280+
EffectArray.filter((prop) => propertyIsRelation(prop)),
281+
EffectArray.every((prop) => unqTypeNames.has(prop.relationType)),
282+
);
283+
}
284+
285+
/**
286+
* Takes the user-submitted schema, validates it, and build the `Mapping` definition for the schema.
167287
*
168288
* @since 0.0.1
169289
*
170-
* @param schema user-built and submitted schema
290+
* @param input user-built and submitted schema
171291
* @returns the generated [Mapping] definition from the submitted schema
172292
*/
173-
export async function generateMapping(schema: Schema): Promise<Mapping> {
293+
export async function generateMapping(input: Schema): Promise<Mapping> {
294+
// validate the schema since the input is the type, but the schema has additional filters against it to validate as well
295+
const schema = SchemaKnownDecoder(input);
296+
174297
const entries: Array<MappingEntry & { typeName: string }> = [];
175298
const ops: Array<Op> = [];
176299

@@ -188,6 +311,9 @@ export async function generateMapping(schema: Schema): Promise<Mapping> {
188311
const { id, ops: createTypePropOp } = Graph.createProperty({
189312
name: property.name,
190313
dataType: 'RELATION',
314+
/**
315+
* @todo fill in the relationValueTypes and properties for creating a relation property
316+
*/
191317
relationValueTypes: [],
192318
properties: [],
193319
});
@@ -198,7 +324,7 @@ export async function generateMapping(schema: Schema): Promise<Mapping> {
198324
}
199325
const { id, ops: createTypePropOp } = Graph.createProperty({
200326
name: property.name,
201-
dataType: mapSchemaDataTypeToGRC20PropDataType(property.dataType),
327+
dataType,
202328
});
203329
typePropertyIds.push({ propName: property.name, id });
204330
ops.push(...createTypePropOp);
@@ -250,6 +376,12 @@ export async function generateMapping(schema: Schema): Promise<Mapping> {
250376
);
251377
}
252378

379+
/**
380+
* @since 0.0.1
381+
*
382+
* @param dataType the dataType from the user-submitted schema
383+
* @returns the mapped to GRC-20 dataType for the GRC-20 ops
384+
*/
253385
export function mapSchemaDataTypeToGRC20PropDataType(dataType: SchemaDataType): CreatePropertyParams['dataType'] {
254386
switch (true) {
255387
case dataType === 'Boolean': {

packages/typesync/test/Mapping.test.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { Id } from '@graphprotocol/grc-20';
22
import { describe, expect, it } from 'vitest';
33

4-
import { type Mapping, generateMapping, mapSchemaDataTypeToGRC20PropDataType } from '../src/Mapping.js';
4+
import {
5+
type Mapping,
6+
type Schema,
7+
allRelationPropertyTypesExist,
8+
generateMapping,
9+
mapSchemaDataTypeToGRC20PropDataType,
10+
} from '../src/Mapping.js';
511

612
describe('Mapping', () => {
713
describe('mapSchemaDataTypeToGRC20PropDataType', () => {
@@ -16,6 +22,40 @@ describe('Mapping', () => {
1622
});
1723
});
1824

25+
describe('allRelationPropertyTypesExist', () => {
26+
it('should return true if the submitted schema contains all required types', () => {
27+
const types: Schema['types'] = [
28+
{
29+
name: 'Account',
30+
knowledgeGraphId: null,
31+
properties: [{ name: 'username', dataType: 'Text', knowledgeGraphId: null }],
32+
},
33+
{
34+
name: 'Event',
35+
knowledgeGraphId: null,
36+
properties: [
37+
{ name: 'speaker', dataType: 'Relation(Account)', relationType: 'Account', knowledgeGraphId: null },
38+
],
39+
},
40+
];
41+
42+
expect(allRelationPropertyTypesExist(types)).toEqual(true);
43+
});
44+
it('should return false if the submitted schema relation properties', () => {
45+
const types: Schema['types'] = [
46+
{
47+
name: 'Event',
48+
knowledgeGraphId: null,
49+
properties: [
50+
{ name: 'speaker', dataType: 'Relation(Account)', relationType: 'Account', knowledgeGraphId: null },
51+
],
52+
},
53+
];
54+
55+
expect(allRelationPropertyTypesExist(types)).toEqual(false);
56+
});
57+
});
58+
1959
describe('generateMapping', () => {
2060
it('should be able to map the input schema to a resulting Mapping definition', async () => {
2161
const actual = await generateMapping({
@@ -53,6 +93,7 @@ describe('Mapping', () => {
5393
{
5494
name: 'speaker',
5595
dataType: 'Relation(Account)',
96+
relationType: 'Account',
5697
knowledgeGraphId: null,
5798
},
5899
],
@@ -115,6 +156,7 @@ describe('Mapping', () => {
115156
{
116157
name: 'speaker',
117158
dataType: 'Relation(Account)',
159+
relationType: 'Account',
118160
knowledgeGraphId: null,
119161
},
120162
],
@@ -141,5 +183,56 @@ describe('Mapping', () => {
141183

142184
expect(actual).toEqual(expected);
143185
});
186+
describe('schema validation failures', () => {
187+
it('should throw an error if the Schema does not pass validation: type names are not unique', async () => {
188+
await expect(() =>
189+
generateMapping({
190+
types: [
191+
{
192+
name: 'Account',
193+
knowledgeGraphId: null,
194+
properties: [{ name: 'username', dataType: 'Text', knowledgeGraphId: null }],
195+
},
196+
{
197+
name: 'Account',
198+
knowledgeGraphId: null,
199+
properties: [{ name: 'image', dataType: 'Text', knowledgeGraphId: null }],
200+
},
201+
],
202+
}),
203+
).rejects.toThrowError();
204+
});
205+
it('should throw an error if the Schema does not pass validation: type property names are not unique', async () => {
206+
await expect(() =>
207+
generateMapping({
208+
types: [
209+
{
210+
name: 'Account',
211+
knowledgeGraphId: null,
212+
properties: [
213+
{ name: 'username', dataType: 'Text', knowledgeGraphId: null },
214+
{ name: 'username', dataType: 'Text', knowledgeGraphId: null },
215+
],
216+
},
217+
],
218+
}),
219+
).rejects.toThrowError();
220+
});
221+
it('should throw an error if the Schema does not pass validation: referenced relation property does not have matching type in schema', async () => {
222+
await expect(() =>
223+
generateMapping({
224+
types: [
225+
{
226+
name: 'Event',
227+
knowledgeGraphId: null,
228+
properties: [
229+
{ name: 'speaker', dataType: 'Relation(Account)', relationType: 'Account', knowledgeGraphId: null },
230+
],
231+
},
232+
],
233+
}),
234+
).rejects.toThrowError();
235+
});
236+
});
144237
});
145238
});

0 commit comments

Comments
 (0)