@graphprotocol/hypergraph 0.5.0 → 0.6.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/cli/Cli.js +1 -1
- package/dist/cli/bin.js +0 -0
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/bun.js +0 -0
- package/dist/cli/services/Model.d.ts +99 -0
- package/dist/cli/services/Model.d.ts.map +1 -0
- package/dist/cli/services/Model.js +52 -0
- package/dist/cli/services/Model.js.map +1 -0
- package/dist/cli/services/Typesync.d.ts +7 -4
- package/dist/cli/services/Typesync.d.ts.map +1 -1
- package/dist/cli/services/Typesync.js +109 -4
- package/dist/cli/services/Typesync.js.map +1 -1
- package/dist/cli/services/Utils.d.ts +81 -0
- package/dist/cli/services/Utils.d.ts.map +1 -1
- package/dist/cli/services/Utils.js +198 -8
- package/dist/cli/services/Utils.js.map +1 -1
- package/dist/cli/subcommands/typesync.d.ts +13 -2
- package/dist/cli/subcommands/typesync.d.ts.map +1 -1
- package/dist/cli/subcommands/typesync.js +145 -21
- package/dist/cli/subcommands/typesync.js.map +1 -1
- 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 +24 -12
- package/dist/mapping/Mapping.d.ts.map +1 -1
- package/dist/mapping/Mapping.js +12 -4
- package/dist/mapping/Mapping.js.map +1 -1
- package/dist/typesync-studio/dist/assets/authenticate-callback-DN5-Vy9K.js +1 -0
- package/dist/typesync-studio/dist/assets/ccip-Dy0vD6Om.js +1 -0
- package/dist/typesync-studio/dist/assets/index-C9YLgP33.js +215 -0
- package/dist/typesync-studio/dist/assets/index-CgfQUe9o.js +88 -0
- package/dist/typesync-studio/dist/assets/index-VJZgO5f3.css +1 -0
- package/dist/typesync-studio/dist/index.html +30 -0
- package/dist/typesync-studio/dist/manifest.json +20 -0
- package/dist/typesync-studio/dist/robots.txt +3 -0
- package/package.json +15 -8
- package/src/cli/Cli.ts +1 -1
- package/src/cli/bin.ts +0 -1
- package/src/cli/services/Model.ts +87 -0
- package/src/cli/services/Typesync.ts +142 -9
- package/src/cli/services/Utils.ts +231 -11
- package/src/cli/subcommands/typesync.ts +258 -42
- package/src/index.ts +1 -0
- package/src/mapping/Mapping.ts +21 -6
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { Doc } from '@effect/printer';
|
|
1
2
|
import { Data, Effect, Array as EffectArray } from 'effect';
|
|
2
3
|
import ts from 'typescript';
|
|
3
4
|
|
|
4
5
|
import * as Mapping from '../../mapping/Mapping.js';
|
|
5
6
|
import * as Utils from '../../mapping/Utils.js';
|
|
7
|
+
import type * as Model from './Model.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Takes a parsed schema.ts file and maps it to a the Mapping.Schema type.
|
|
@@ -16,17 +18,17 @@ import * as Utils from '../../mapping/Utils.js';
|
|
|
16
18
|
export const parseSchema = (
|
|
17
19
|
sourceCode: string,
|
|
18
20
|
mapping: Mapping.Mapping,
|
|
19
|
-
): Effect.Effect<
|
|
21
|
+
): Effect.Effect<Model.TypesyncHypergraphSchema, SchemaParserFailure> =>
|
|
20
22
|
Effect.try({
|
|
21
23
|
try() {
|
|
22
24
|
const sourceFile = ts.createSourceFile('schema.ts', sourceCode, ts.ScriptTarget.Latest, true);
|
|
23
25
|
|
|
24
|
-
const entities: Array<
|
|
26
|
+
const entities: Array<Model.TypesyncHypergraphSchemaType> = [];
|
|
25
27
|
|
|
26
28
|
const visit = (node: ts.Node) => {
|
|
27
29
|
if (ts.isClassDeclaration(node) && node.name) {
|
|
28
30
|
const className = node.name.text;
|
|
29
|
-
const properties: Array<
|
|
31
|
+
const properties: Array<Model.TypesyncHypergraphSchemaTypeProperty> = [];
|
|
30
32
|
|
|
31
33
|
// Find the Entity.Class call
|
|
32
34
|
if (node.heritageClauses) {
|
|
@@ -44,14 +46,29 @@ export const parseSchema = (
|
|
|
44
46
|
const propName = prop.name.text;
|
|
45
47
|
let dataType: Mapping.SchemaDataType = 'String';
|
|
46
48
|
let relationType: string | undefined;
|
|
49
|
+
let isOptional = false;
|
|
47
50
|
|
|
48
51
|
const mappingEntry = mapping[className];
|
|
49
52
|
const camelCasePropName = Utils.toCamelCase(propName);
|
|
50
53
|
|
|
54
|
+
// Check if the property is wrapped with Type.optional()
|
|
55
|
+
let typeExpression = prop.initializer;
|
|
56
|
+
if (
|
|
57
|
+
ts.isCallExpression(typeExpression) &&
|
|
58
|
+
ts.isPropertyAccessExpression(typeExpression.expression) &&
|
|
59
|
+
typeExpression.expression.name.text === 'optional'
|
|
60
|
+
) {
|
|
61
|
+
isOptional = true;
|
|
62
|
+
// Unwrap the optional to get the actual type
|
|
63
|
+
if (typeExpression.arguments.length > 0) {
|
|
64
|
+
typeExpression = typeExpression.arguments[0];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
// Extract the type
|
|
52
|
-
if (ts.isPropertyAccessExpression(
|
|
53
|
-
// Simple types like Type.
|
|
54
|
-
dataType = Mapping.getDataType(
|
|
69
|
+
if (ts.isPropertyAccessExpression(typeExpression)) {
|
|
70
|
+
// Simple types like Type.String
|
|
71
|
+
dataType = Mapping.getDataType(typeExpression.name.text);
|
|
55
72
|
|
|
56
73
|
// Look up the property's knowledgeGraphId from the mapping
|
|
57
74
|
const propKnowledgeGraphId = mappingEntry?.properties?.[camelCasePropName] || null;
|
|
@@ -61,10 +78,12 @@ export const parseSchema = (
|
|
|
61
78
|
name: propName,
|
|
62
79
|
dataType: dataType as Mapping.SchemaDataTypePrimitive,
|
|
63
80
|
knowledgeGraphId: propKnowledgeGraphId,
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
optional: isOptional || undefined,
|
|
82
|
+
status: propKnowledgeGraphId != null ? 'published' : 'synced',
|
|
83
|
+
} satisfies Model.TypesyncHypergraphSchemaTypeProperty);
|
|
84
|
+
} else if (ts.isCallExpression(typeExpression)) {
|
|
66
85
|
// Relation types like Type.Relation(User)
|
|
67
|
-
const callNode =
|
|
86
|
+
const callNode = typeExpression;
|
|
68
87
|
if (ts.isPropertyAccessExpression(callNode.expression)) {
|
|
69
88
|
const typeName = callNode.expression.name.text;
|
|
70
89
|
|
|
@@ -83,7 +102,9 @@ export const parseSchema = (
|
|
|
83
102
|
dataType,
|
|
84
103
|
relationType,
|
|
85
104
|
knowledgeGraphId: relKnowledgeGraphId,
|
|
86
|
-
|
|
105
|
+
optional: isOptional || undefined,
|
|
106
|
+
status: relKnowledgeGraphId != null ? 'published' : 'synced',
|
|
107
|
+
} satisfies Model.TypesyncHypergraphSchemaTypeProperty);
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
}
|
|
@@ -101,7 +122,12 @@ export const parseSchema = (
|
|
|
101
122
|
const mappingEntry = mapping[Utils.toPascalCase(className)];
|
|
102
123
|
const typeKnowledgeGraphId = mappingEntry?.typeIds?.[0] ? mappingEntry.typeIds[0] : null;
|
|
103
124
|
|
|
104
|
-
entities.push({
|
|
125
|
+
entities.push({
|
|
126
|
+
name: className,
|
|
127
|
+
knowledgeGraphId: typeKnowledgeGraphId,
|
|
128
|
+
properties,
|
|
129
|
+
status: typeKnowledgeGraphId != null ? 'published' : 'synced',
|
|
130
|
+
});
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
ts.forEachChild(node, visit);
|
|
@@ -185,3 +211,197 @@ export function parseHypergraphMapping(moduleExport: any): Mapping.Mapping {
|
|
|
185
211
|
// If no preferred names found, use the first one
|
|
186
212
|
return mappingCandidates[0][1] as Mapping.Mapping;
|
|
187
213
|
}
|
|
214
|
+
|
|
215
|
+
function fieldToEntityString({ name, dataType, optional = false }: Model.TypesyncHypergraphSchemaTypeProperty): string {
|
|
216
|
+
// Convert type to Entity type
|
|
217
|
+
const entityType = (() => {
|
|
218
|
+
switch (true) {
|
|
219
|
+
case dataType === 'String':
|
|
220
|
+
return 'Type.String';
|
|
221
|
+
case dataType === 'Number':
|
|
222
|
+
return 'Type.Number';
|
|
223
|
+
case dataType === 'Boolean':
|
|
224
|
+
return 'Type.Boolean';
|
|
225
|
+
case dataType === 'Date':
|
|
226
|
+
return 'Type.Date';
|
|
227
|
+
case dataType === 'Point':
|
|
228
|
+
return 'Type.Point';
|
|
229
|
+
case Mapping.isDataTypeRelation(dataType):
|
|
230
|
+
// renders the type as `Type.Relation(Entity)`
|
|
231
|
+
return `Type.${dataType}`;
|
|
232
|
+
default:
|
|
233
|
+
// how to handle complex types
|
|
234
|
+
return 'Type.String';
|
|
235
|
+
}
|
|
236
|
+
})();
|
|
237
|
+
|
|
238
|
+
if (optional === true) {
|
|
239
|
+
return ` ${Utils.toCamelCase(name)}: Type.optional(${entityType})`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// adds a tab before the property
|
|
243
|
+
return ` ${Utils.toCamelCase(name)}: ${entityType}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function typeDefinitionToString(type: Model.TypesyncHypergraphSchemaType): string | null {
|
|
247
|
+
if (!type.name) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const fields = type.properties.filter((_prop) => _prop.name != null && _prop.name.length > 0);
|
|
251
|
+
if (fields.length === 0) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const fieldStrings = fields.map(fieldToEntityString);
|
|
256
|
+
|
|
257
|
+
const name = Utils.toPascalCase(type.name);
|
|
258
|
+
return `export class ${name} extends Entity.Class<${name}>('${name}')({
|
|
259
|
+
${fieldStrings.join(',\n')}
|
|
260
|
+
}) {}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Builds a string of the schema.ts file contents after parsing the schema into the correct format.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
*
|
|
268
|
+
* ```typescript
|
|
269
|
+
* const schema = Model.TypesyncHypergraphSchema.make({
|
|
270
|
+
* types: [
|
|
271
|
+
* {
|
|
272
|
+
* name: "User",
|
|
273
|
+
* knowledgeGraphId: null,
|
|
274
|
+
* status: null,
|
|
275
|
+
* properties: [
|
|
276
|
+
* {
|
|
277
|
+
* name: "name",
|
|
278
|
+
* dataType: "String",
|
|
279
|
+
* knowledgeGraphId: null,
|
|
280
|
+
* optional: null,
|
|
281
|
+
* status: null
|
|
282
|
+
* }
|
|
283
|
+
* ]
|
|
284
|
+
* }
|
|
285
|
+
* ]
|
|
286
|
+
* })
|
|
287
|
+
* const schemaFile = buildSchemaFile(schema)
|
|
288
|
+
*
|
|
289
|
+
* expect(schemaFile).toEqual(`
|
|
290
|
+
* import { Entity, Type } from '@graphprotocol/hypergraph';
|
|
291
|
+
*
|
|
292
|
+
* export class User extends Entity.Class<User>('User')({
|
|
293
|
+
* name: Type.String
|
|
294
|
+
* }) {}
|
|
295
|
+
* `)
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export function buildSchemaFile(schema: Model.TypesyncHypergraphSchema) {
|
|
299
|
+
const importStatement = `import { Entity, Type } from '@graphprotocol/hypergraph';`;
|
|
300
|
+
|
|
301
|
+
const typeDefinitions = schema.types
|
|
302
|
+
.map(typeDefinitionToString)
|
|
303
|
+
.filter((def) => def != null)
|
|
304
|
+
.join('\n\n');
|
|
305
|
+
return [importStatement, typeDefinitions].join('\n\n');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function buildMappingFile(mapping: Mapping.Mapping | Model.TypesyncHypergraphMapping) {
|
|
309
|
+
// Import statements
|
|
310
|
+
const imports = Doc.vsep([
|
|
311
|
+
Doc.text("import type { Mapping } from '@graphprotocol/hypergraph/mapping';"),
|
|
312
|
+
Doc.text("import { Id } from '@graphprotocol/hypergraph';"),
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
// Generate the mapping object - build it line by line for exact formatting
|
|
316
|
+
const mappingLines = [Doc.text('export const mapping: Mapping = {')];
|
|
317
|
+
|
|
318
|
+
for (const [typeName, typeData] of Object.entries(mapping)) {
|
|
319
|
+
mappingLines.push(Doc.text(` ${typeName}: {`));
|
|
320
|
+
|
|
321
|
+
// Type IDs
|
|
322
|
+
const typeIdsList = typeData.typeIds.map((id: string) => `Id("${id}")`).join(', ');
|
|
323
|
+
mappingLines.push(Doc.text(` typeIds: [${typeIdsList}],`));
|
|
324
|
+
|
|
325
|
+
// Properties
|
|
326
|
+
const properties = Object.entries(typeData.properties ?? {});
|
|
327
|
+
if (EffectArray.isNonEmptyArray(properties)) {
|
|
328
|
+
mappingLines.push(Doc.text(' properties: {'));
|
|
329
|
+
properties.forEach(([propName, propId], index, entries) => {
|
|
330
|
+
const isLast = index === entries.length - 1;
|
|
331
|
+
const comma = isLast ? '' : ',';
|
|
332
|
+
mappingLines.push(Doc.text(` ${propName}: Id("${propId}")${comma}`));
|
|
333
|
+
});
|
|
334
|
+
mappingLines.push(Doc.text(' },'));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Relations
|
|
338
|
+
const relations = Object.entries(typeData.relations ?? {});
|
|
339
|
+
if (EffectArray.isNonEmptyArray(relations)) {
|
|
340
|
+
mappingLines.push(Doc.text(' relations: {'));
|
|
341
|
+
relations.forEach(([relationName, relationId], index, entries) => {
|
|
342
|
+
const isLast = index === entries.length - 1;
|
|
343
|
+
const comma = isLast ? '' : ',';
|
|
344
|
+
mappingLines.push(Doc.text(` ${relationName}: Id("${relationId}")${comma}`));
|
|
345
|
+
});
|
|
346
|
+
mappingLines.push(Doc.text(' },'));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
mappingLines.push(Doc.text(' },'));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
mappingLines.push(Doc.rbrace);
|
|
353
|
+
|
|
354
|
+
const compiled = Doc.vcat([imports, Doc.empty, ...mappingLines]);
|
|
355
|
+
|
|
356
|
+
return Doc.render(compiled, {
|
|
357
|
+
style: 'pretty',
|
|
358
|
+
options: { lineWidth: 120 },
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Builds a string of the mapping.ts file contents after parsing the schema into the correct mapping format.
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
*
|
|
367
|
+
* ```typescript
|
|
368
|
+
* const schema = Model.TypesyncHypergraphSchema.make({
|
|
369
|
+
* types: [
|
|
370
|
+
* {
|
|
371
|
+
* name: "User",
|
|
372
|
+
* knowledgeGraphId: "7f9562d4-034d-4385-bf5c-f02cdebba47a",
|
|
373
|
+
* status: null,
|
|
374
|
+
* properties: [
|
|
375
|
+
* {
|
|
376
|
+
* name: "name",
|
|
377
|
+
* dataType: "String",
|
|
378
|
+
* knowledgeGraphId: "a126ca53-0c8e-48d5-b888-82c734c38935",
|
|
379
|
+
* optional: null,
|
|
380
|
+
* status: null
|
|
381
|
+
* }
|
|
382
|
+
* ]
|
|
383
|
+
* }
|
|
384
|
+
* ]
|
|
385
|
+
* })
|
|
386
|
+
* const mappingFile = buildMappingFile(schema)
|
|
387
|
+
*
|
|
388
|
+
* expect(mappingFile).toEqual(`
|
|
389
|
+
* import type { Mapping } from '@graphprotocol/hypergraph/mapping';
|
|
390
|
+
* import { Id } from '@graphprotocol/hypergraph';
|
|
391
|
+
*
|
|
392
|
+
* export const mapping: Mapping = {
|
|
393
|
+
* User: {
|
|
394
|
+
* typeIds: [Id('7f9562d4-034d-4385-bf5c-f02cdebba47a')],
|
|
395
|
+
* properties: {
|
|
396
|
+
* name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'),
|
|
397
|
+
* }
|
|
398
|
+
* }
|
|
399
|
+
* }
|
|
400
|
+
* `)
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
export function buildMappingFileFromSchema(schema: Model.TypesyncHypergraphSchema) {
|
|
404
|
+
const [mapping] = Mapping.generateMapping(schema);
|
|
405
|
+
|
|
406
|
+
return buildMappingFile(mapping);
|
|
407
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
2
3
|
import { Command, Options } from '@effect/cli';
|
|
3
4
|
import {
|
|
5
|
+
FileSystem,
|
|
4
6
|
HttpApi,
|
|
5
7
|
HttpApiBuilder,
|
|
6
8
|
HttpApiEndpoint,
|
|
@@ -8,70 +10,265 @@ import {
|
|
|
8
10
|
HttpApiGroup,
|
|
9
11
|
HttpApiSchema,
|
|
10
12
|
HttpMiddleware,
|
|
13
|
+
HttpRouter,
|
|
11
14
|
HttpServer,
|
|
12
15
|
HttpServerResponse,
|
|
16
|
+
Path,
|
|
13
17
|
} from '@effect/platform';
|
|
14
18
|
import { NodeHttpServer } from '@effect/platform-node';
|
|
15
19
|
import { AnsiDoc } from '@effect/printer-ansi';
|
|
16
|
-
import { Effect, Layer, Schema } from 'effect';
|
|
20
|
+
import { Cause, Data, Effect, Layer, Option, Schema, Struct } from 'effect';
|
|
21
|
+
import open, { type AppName, apps } from 'open';
|
|
22
|
+
import * as Model from '../services/Model.js';
|
|
17
23
|
import * as Typesync from '../services/Typesync.js';
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
class HypergraphTypesyncStudioApiRouter extends HttpApiGroup.make('HypergraphTypesyncStudioApiRouter')
|
|
20
26
|
.add(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
// exposes an api endpoint at /api/vX/schema/events that is a stream of the current Schema parsed from the directory the hypergraph-cli tool is running in
|
|
28
|
+
HttpApiEndpoint.get('HypergraphSchemaEventStream')`/schema/events`
|
|
29
|
+
.addError(HttpApiError.InternalServerError)
|
|
30
|
+
.addSuccess(
|
|
31
|
+
Schema.String.pipe(
|
|
32
|
+
HttpApiSchema.withEncoding({
|
|
33
|
+
kind: 'Json',
|
|
34
|
+
contentType: 'text/event-stream',
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
.add(
|
|
40
|
+
HttpApiEndpoint.post('SyncHypergraphSchema')`/schema/sync`
|
|
41
|
+
.setPayload(Model.TypesyncHypergraphSchema)
|
|
42
|
+
.addSuccess(Model.TypesyncHypergraphSchema)
|
|
43
|
+
.addError(HttpApiError.InternalServerError)
|
|
44
|
+
.addError(HttpApiError.BadRequest),
|
|
45
|
+
)
|
|
46
|
+
.add(
|
|
47
|
+
HttpApiEndpoint.post('SyncHypergraphMapping')`/mapping/sync`
|
|
48
|
+
.setPayload(
|
|
49
|
+
Schema.Struct({
|
|
50
|
+
schema: Model.TypesyncHypergraphSchema,
|
|
51
|
+
mapping: Model.TypesyncHypergraphMapping,
|
|
52
|
+
}),
|
|
34
53
|
)
|
|
35
|
-
.
|
|
54
|
+
.addSuccess(Model.TypesyncHypergraphSchema)
|
|
55
|
+
.addError(HttpApiError.InternalServerError)
|
|
56
|
+
.addError(HttpApiError.BadRequest),
|
|
36
57
|
)
|
|
37
|
-
.prefix('/
|
|
58
|
+
.prefix('/v1') {}
|
|
59
|
+
class HypergraphTypesyncStudioApi extends HttpApi.make('HypergraphTypesyncStudioApi')
|
|
60
|
+
.add(HypergraphTypesyncStudioApiRouter)
|
|
61
|
+
.prefix('/api') {}
|
|
38
62
|
|
|
39
|
-
const hypergraphTypeSyncApiLive = HttpApiBuilder.group(
|
|
40
|
-
|
|
63
|
+
const hypergraphTypeSyncApiLive = HttpApiBuilder.group(
|
|
64
|
+
HypergraphTypesyncStudioApi,
|
|
65
|
+
'HypergraphTypesyncStudioApiRouter',
|
|
66
|
+
(handlers) =>
|
|
41
67
|
Effect.gen(function* () {
|
|
42
68
|
const schemaStream = yield* Typesync.TypesyncSchemaStreamBuilder;
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
70
|
+
return handlers
|
|
71
|
+
.handle('HypergraphSchemaEventStream', () =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const stream = yield* schemaStream.hypergraphSchemaStream().pipe(
|
|
74
|
+
Effect.tapErrorCause((cause) =>
|
|
75
|
+
Effect.logError(
|
|
76
|
+
AnsiDoc.cat(
|
|
77
|
+
AnsiDoc.text('Failure building Hypergraph events stream:'),
|
|
78
|
+
AnsiDoc.text(Cause.pretty(cause)),
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
Effect.catchAll(() => new HttpApiError.InternalServerError()),
|
|
83
|
+
);
|
|
47
84
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
return yield* HttpServerResponse.stream(stream, { contentType: 'text/event-stream' }).pipe(
|
|
86
|
+
HttpServerResponse.setHeaders({
|
|
87
|
+
'Content-Type': 'text/event-stream',
|
|
88
|
+
'Cache-Control': 'no-cache',
|
|
89
|
+
Connection: 'keep-alive',
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
.handle('SyncHypergraphSchema', ({ payload }) =>
|
|
95
|
+
schemaStream.syncSchema(payload).pipe(
|
|
96
|
+
Effect.tapErrorCause((cause) =>
|
|
97
|
+
Effect.logError(
|
|
98
|
+
AnsiDoc.cat(AnsiDoc.text('Failure syncing Hypergraph Schema:'), AnsiDoc.text(Cause.pretty(cause))),
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
Effect.catchAll(() => new HttpApiError.InternalServerError()),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
.handle('SyncHypergraphMapping', ({ payload }) =>
|
|
105
|
+
schemaStream.syncMapping(payload.schema, payload.mapping).pipe(
|
|
106
|
+
Effect.tapErrorCause((cause) =>
|
|
107
|
+
Effect.logError(
|
|
108
|
+
AnsiDoc.cat(AnsiDoc.text('Failure syncing Hypergraph mapping:'), AnsiDoc.text(Cause.pretty(cause))),
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
Effect.catchAll(() => new HttpApiError.InternalServerError()),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
55
114
|
}),
|
|
56
|
-
),
|
|
57
115
|
);
|
|
58
116
|
|
|
59
|
-
const
|
|
60
|
-
|
|
117
|
+
const HypergraphTypeSyncApiLayer = HttpApiBuilder.middlewareCors({
|
|
118
|
+
allowedMethods: ['GET', 'POST', 'OPTIONS'],
|
|
119
|
+
allowedOrigins: ['http://localhost:3000', 'http://localhost:5173'],
|
|
120
|
+
}).pipe(
|
|
121
|
+
Layer.provideMerge(HttpApiBuilder.api(HypergraphTypesyncStudioApi)),
|
|
61
122
|
Layer.provide(Typesync.layer),
|
|
123
|
+
Layer.provide(hypergraphTypeSyncApiLive),
|
|
62
124
|
);
|
|
63
125
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
126
|
+
const HypergraphTypeSyncApiLive = HttpApiBuilder.httpApp.pipe(
|
|
127
|
+
Effect.provide(
|
|
128
|
+
Layer.mergeAll(HypergraphTypeSyncApiLayer, HttpApiBuilder.Router.Live, HttpApiBuilder.Middleware.layer),
|
|
129
|
+
),
|
|
67
130
|
);
|
|
68
131
|
|
|
132
|
+
const TypesyncStudioFileRouter = Effect.gen(function* () {
|
|
133
|
+
const fs = yield* FileSystem.FileSystem;
|
|
134
|
+
const path = yield* Path.Path;
|
|
135
|
+
|
|
136
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
137
|
+
const __dirname = path.dirname(__filename);
|
|
138
|
+
|
|
139
|
+
// Try multiple possible locations for the dist directory
|
|
140
|
+
const possiblePaths = [
|
|
141
|
+
// npm published package (when this file is in node_modules/@graphprotocol/hypergraph/dist/cli/subcommands/)
|
|
142
|
+
path.resolve(__dirname, '..', '..', 'typesync-studio', 'dist'),
|
|
143
|
+
// Development mode (when this file is in packages/hypergraph/src/cli/subcommands/)
|
|
144
|
+
path.resolve(__dirname, '..', '..', '..', 'typesync-studio', 'dist'),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const findTypesyncStudioDist = Effect.fnUntraced(function* () {
|
|
148
|
+
return yield* Effect.findFirst(possiblePaths, (_) => fs.exists(_).pipe(Effect.orElseSucceed(() => false)));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const typesyncStudioClientDist = yield* findTypesyncStudioDist().pipe(
|
|
152
|
+
// default to first path
|
|
153
|
+
Effect.map((maybe) => Option.getOrElse(maybe, () => possiblePaths[0])),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return HttpRouter.empty.pipe(
|
|
157
|
+
HttpRouter.get(
|
|
158
|
+
'/',
|
|
159
|
+
HttpServerResponse.file(path.join(typesyncStudioClientDist, 'index.html')).pipe(
|
|
160
|
+
Effect.orElse(() => HttpServerResponse.empty({ status: 404 })),
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
// specific handler for the /authenticate-callback endpoint for auth
|
|
164
|
+
HttpRouter.get(
|
|
165
|
+
'/authenticate-callback',
|
|
166
|
+
HttpServerResponse.file(path.join(typesyncStudioClientDist, 'index.html')).pipe(
|
|
167
|
+
Effect.orElse(() => HttpServerResponse.empty({ status: 404 })),
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
HttpRouter.get(
|
|
171
|
+
'/assets/:file',
|
|
172
|
+
Effect.gen(function* () {
|
|
173
|
+
const file = yield* HttpRouter.params.pipe(Effect.map(Struct.get('file')), Effect.map(Option.fromNullable));
|
|
174
|
+
|
|
175
|
+
if (Option.isNone(file)) {
|
|
176
|
+
return HttpServerResponse.empty({ status: 404 });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const assets = path.join(typesyncStudioClientDist, 'assets');
|
|
180
|
+
const normalized = path.normalize(path.join(assets, ...file.value.split('/')));
|
|
181
|
+
if (!normalized.startsWith(assets)) {
|
|
182
|
+
return HttpServerResponse.empty({ status: 404 });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return yield* HttpServerResponse.file(normalized);
|
|
186
|
+
}).pipe(Effect.orElse(() => HttpServerResponse.empty({ status: 404 }))),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const Server = Effect.all({
|
|
192
|
+
api: HypergraphTypeSyncApiLive,
|
|
193
|
+
files: TypesyncStudioFileRouter,
|
|
194
|
+
}).pipe(
|
|
195
|
+
Effect.map(({ api, files }) =>
|
|
196
|
+
HttpRouter.empty.pipe(HttpRouter.mount('/', files), HttpRouter.mountApp('/api', api, { includePrefix: true })),
|
|
197
|
+
),
|
|
198
|
+
Effect.map((router) => HttpServer.serve(HttpMiddleware.logger)(router)),
|
|
199
|
+
Layer.unwrapEffect,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const openBrowser = (port: number, browser: AppName | 'arc' | 'safari' | 'browser' | 'browserPrivate') =>
|
|
203
|
+
Effect.async<void, OpenBrowserError>((resume) => {
|
|
204
|
+
const url = `http://localhost:${port}`;
|
|
205
|
+
|
|
206
|
+
const launch = (appOpts?: { name: string | ReadonlyArray<string> }) =>
|
|
207
|
+
open(url, appOpts ? { app: appOpts } : undefined).then((subprocess) => {
|
|
208
|
+
subprocess.on('spawn', () => resume(Effect.void));
|
|
209
|
+
subprocess.on('error', (err) => resume(Effect.fail(new OpenBrowserError({ cause: err }))));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const mapBrowserName = (b: typeof browser): string | ReadonlyArray<string> | undefined => {
|
|
213
|
+
switch (b) {
|
|
214
|
+
case 'chrome':
|
|
215
|
+
return apps.chrome; // cross-platform alias from open
|
|
216
|
+
case 'firefox':
|
|
217
|
+
return apps.firefox;
|
|
218
|
+
case 'edge':
|
|
219
|
+
return apps.edge;
|
|
220
|
+
case 'safari':
|
|
221
|
+
return 'Safari';
|
|
222
|
+
case 'arc':
|
|
223
|
+
return 'Arc';
|
|
224
|
+
default:
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
switch (browser) {
|
|
230
|
+
case 'browser':
|
|
231
|
+
launch();
|
|
232
|
+
break;
|
|
233
|
+
case 'browserPrivate':
|
|
234
|
+
launch({ name: apps.browserPrivate });
|
|
235
|
+
break;
|
|
236
|
+
default: {
|
|
237
|
+
const mapped = mapBrowserName(browser);
|
|
238
|
+
if (mapped) {
|
|
239
|
+
launch({ name: mapped }).catch(() => launch());
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
launch();
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
export class OpenBrowserError extends Data.TaggedError('Nozzle/cli/studio/errors/OpenBrowserError')<{
|
|
249
|
+
readonly cause: unknown;
|
|
250
|
+
}> {}
|
|
251
|
+
|
|
69
252
|
export const typesync = Command.make('typesync', {
|
|
70
253
|
args: {
|
|
71
|
-
|
|
72
|
-
Options.
|
|
73
|
-
Options.withDefault(
|
|
74
|
-
|
|
254
|
+
open: Options.boolean('open').pipe(
|
|
255
|
+
Options.withDescription('If true, opens the nozzle dataset studio in your browser'),
|
|
256
|
+
Options.withDefault(true),
|
|
257
|
+
),
|
|
258
|
+
browser: Options.choice('browser', [
|
|
259
|
+
'chrome',
|
|
260
|
+
'firefox',
|
|
261
|
+
'edge',
|
|
262
|
+
'safari',
|
|
263
|
+
'arc',
|
|
264
|
+
'browser',
|
|
265
|
+
'browserPrivate',
|
|
266
|
+
]).pipe(
|
|
267
|
+
Options.withAlias('b'),
|
|
268
|
+
Options.withDescription(
|
|
269
|
+
'Broweser to open the nozzle dataset studio app in. Default is your default selected browser',
|
|
270
|
+
),
|
|
271
|
+
Options.withDefault('browser'),
|
|
75
272
|
),
|
|
76
273
|
},
|
|
77
274
|
}).pipe(
|
|
@@ -80,14 +277,33 @@ export const typesync = Command.make('typesync', {
|
|
|
80
277
|
),
|
|
81
278
|
Command.withHandler(({ args }) =>
|
|
82
279
|
Effect.gen(function* () {
|
|
83
|
-
yield*
|
|
280
|
+
yield* Server.pipe(
|
|
84
281
|
HttpServer.withLogAddress,
|
|
85
|
-
Layer.provide(NodeHttpServer.layer(createServer, { port:
|
|
282
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
|
|
283
|
+
Layer.tap(() =>
|
|
284
|
+
Effect.gen(function* () {
|
|
285
|
+
if (args.open) {
|
|
286
|
+
return yield* openBrowser(3000, args.browser).pipe(
|
|
287
|
+
Effect.tapErrorCause((cause) =>
|
|
288
|
+
Effect.logWarning(
|
|
289
|
+
AnsiDoc.text(
|
|
290
|
+
'Failure opening nozzle dataset studio in your browser. Open at http://localhost:3000',
|
|
291
|
+
),
|
|
292
|
+
AnsiDoc.text(Cause.pretty(cause)),
|
|
293
|
+
),
|
|
294
|
+
),
|
|
295
|
+
Effect.orElseSucceed(() => Effect.void),
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return Effect.void;
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
86
301
|
Layer.tap(() =>
|
|
87
|
-
Effect.logInfo(AnsiDoc.text(
|
|
302
|
+
Effect.logInfo(AnsiDoc.text('🎉 TypeSync studio started and running at http://localhost:3000')),
|
|
88
303
|
),
|
|
89
304
|
Layer.launch,
|
|
90
305
|
);
|
|
91
306
|
}),
|
|
92
307
|
),
|
|
308
|
+
Command.provide(Typesync.layer),
|
|
93
309
|
);
|
package/src/index.ts
CHANGED