Skip to content

Commit 22603e7

Browse files
authored
feat(typesync): initial TypeSync implementation (#175)
1 parent 2ff9b36 commit 22603e7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+11050
-1876
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ typings/
5151
# dotenv environment variables file
5252
.env
5353
.env.test
54+
.env.local
55+
.env.development.local
56+
.env.test.local
57+
.env.production.local
5458

5559
# parcel-bundler cache (https://parceljs.org/)
5660
.cache
5761

5862
# Next.js build output
59-
.next
63+
.next

.vscode/extensions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"recommendations": ["biomejs.biome"]
2+
"recommendations": ["biomejs.biome", "effectful-tech.effect-vscode"]
33
}

apps/server/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import { parse } from 'node:url';
12
import { Identity, Inboxes, Messages, SpaceEvents, Utils } from '@graphprotocol/hypergraph';
23
import cors from 'cors';
34
import { Effect, Exit, Schema } from 'effect';
45
import express, { type Request, type Response } from 'express';
5-
import { parse } from 'node:url';
66
import { SiweMessage } from 'siwe';
77
import type { Hex } from 'viem';
88
import WebSocket, { WebSocketServer } from 'ws';

apps/typesync/.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake

apps/typesync/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024-present <PLACEHOLDER>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

apps/typesync/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# @graphprotocol/typesync
2+
3+
CLI toolchain to view existing types, select, pick, extend to create schemas and generate a @graphprotocol/hypergraph schema.
4+
5+
The `@graphprotocol/typesync` cli works by spinning up a [hono](https://hono.dev/) nodejs server that exposes a built vitejs react app. This app will let users see their created app schemas as well as search existing types to create new app schemas.
6+
Once the user has a schema built in the app, they can then run codegen, which will send a message to the server to codegen the built schema using the `@graphprotocol/hypergraph` framework.
7+
8+
## Running Code
9+
10+
This template leverages [tsx](https://tsx.is) to allow execution of TypeScript files via NodeJS as if they were written in plain JavaScript.
11+
12+
To execute a file with `tsx`:
13+
14+
```sh
15+
pnpm run dev
16+
```
17+
18+
## Operations
19+
20+
**Building**
21+
22+
To build the package:
23+
24+
```sh
25+
pnpm build
26+
```
27+
28+
**Testing**
29+
30+
To test the package:
31+
32+
```sh
33+
pnpm test
34+
```
35+

apps/typesync/client/index.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!DOCTYPE html>
2+
<html
3+
lang="en"
4+
class="h-full min-h-screen w-full p-0 m-0 dark:bg-slate-950 dark:text-white bg-white text-gray-950 font-mono"
5+
>
6+
<head>
7+
<meta charset="UTF-8" />
8+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
9+
<link
10+
rel="icon"
11+
type="image/png"
12+
sizes="64x64"
13+
href="https://storage.thegraph.com/favicons/64x64.png"
14+
/>
15+
<link
16+
rel="icon"
17+
type="image/png"
18+
sizes="256x256"
19+
href="https://storage.thegraph.com/favicons/256x256.png"
20+
/>
21+
<title>Graph Protocol | TypeSync</title>
22+
<link rel="preconnect" href="https://fonts.googleapis.com" />
23+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
24+
</head>
25+
<body class="h-full w-full">
26+
<div id="root"></div>
27+
<script type="module" src="/src/main.tsx"></script>
28+
</body>
29+
</html>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'use client';
2+
3+
import { Disclosure, DisclosureButton, DisclosurePanel, Input } from '@headlessui/react';
4+
import { ChevronDownIcon, PlusIcon } from '@heroicons/react/20/solid';
5+
import { useQuery } from '@tanstack/react-query';
6+
import { Array as EffectArray, Order, pipe } from 'effect';
7+
import { useState } from 'react';
8+
9+
import type { SchemaBrowserTypesQuery } from '../../../generated/graphql';
10+
import { schemaBrowserQueryOptions } from '../../../hooks/useSchemaBrowserQuery';
11+
import { Loading } from '../../Loading';
12+
13+
export type SchemaBrowserType = NonNullable<SchemaBrowserTypesQuery['space']>['types'][number];
14+
type ExtendedSchemaBrowserType = SchemaBrowserType & { slug: string };
15+
16+
const SchemaTypeOrder = Order.mapInput(Order.string, (type: SchemaBrowserType) => type.name || type.id);
17+
18+
export type SchemaBrowserProps = Readonly<{
19+
typeSelected(type: SchemaBrowserType): void;
20+
}>;
21+
export function SchemaBrowser(props: SchemaBrowserProps) {
22+
const [typeSearch, setTypeSearch] = useState('');
23+
24+
const { data: types, isLoading } = useQuery({
25+
...schemaBrowserQueryOptions,
26+
select(data) {
27+
const types = data.space?.types ?? [];
28+
const mappedAndSorted = pipe(
29+
types,
30+
EffectArray.map((type) => {
31+
const slugifiedProps = EffectArray.reduce(type.properties, '', (slug, curr) => `${slug}${curr.name || ''}`);
32+
const slug = `${type.name || ''}${slugifiedProps}`.toLowerCase();
33+
return {
34+
...type,
35+
slug,
36+
} as const satisfies ExtendedSchemaBrowserType;
37+
}),
38+
EffectArray.sort(SchemaTypeOrder),
39+
);
40+
if (!typeSearch) {
41+
return mappedAndSorted;
42+
}
43+
return pipe(
44+
mappedAndSorted,
45+
EffectArray.filter((type) => type.slug.includes(typeSearch.toLowerCase())),
46+
);
47+
},
48+
});
49+
50+
return (
51+
<div className="flex flex-col gap-y-6">
52+
<h3 className="text-sm/6 font-semibold text-gray-900 dark:text-white flex items-center gap-x-3">
53+
Schema Browser
54+
{isLoading ? <Loading /> : null}
55+
</h3>
56+
<div className="bg-gray-100 dark:bg-slate-900 rounded-lg flex flex-col gap-y-3 pt-2">
57+
<div className="px-3 mt-2">
58+
<Input
59+
id="SchemaBrowserSearch"
60+
name="SchemaBrowserSearch"
61+
value={typeSearch}
62+
onChange={(e) => setTypeSearch(e.target.value || '')}
63+
type="search"
64+
placeholder="Search types..."
65+
className="block min-w-0 grow py-1.5 pl-2 pr-3 rounded-md bg-white dark:bg-slate-700 data-[state=invalid]:pr-10 text-base text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline sm:text-sm/6 focus-visible:outline-none w-full"
66+
/>
67+
</div>
68+
<ul className="px-4 divide-y divide-gray-100 dark:divide-gray-700 overflow-y-auto h-fit max-h-[500px] 2xl:max-h-[750px]">
69+
{(types ?? []).map((_type) => (
70+
<Disclosure as="li" key={_type.id} className="py-3 flex flex-col gap-y-2">
71+
<div className="flex items-center justify-between gap-x-6">
72+
<DisclosureButton
73+
as="div"
74+
disabled={_type.properties.length === 0}
75+
data-interactive={_type.properties.length > 0 ? 'clickable' : undefined}
76+
className="min-w-0 data-[interactive=clickable]:cursor-pointer"
77+
>
78+
<div className="flex items-center gap-x-2">
79+
{_type.properties.length > 0 ? <ChevronDownIcon className="size-4" aria-hidden="true" /> : null}
80+
<p className="text-sm/6 font-semibold text-gray-900 dark:text-white">{_type.name || _type.id}</p>
81+
<p className="text-xs font-light text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-slate-600 ring-gray-500/10 rounded-md px-1.5 py-0.5 whitespace-nowrap ring-1 ring-inset">
82+
{_type.id}
83+
</p>
84+
</div>
85+
</DisclosureButton>
86+
<div className="flex flex-none items-center justify-end">
87+
<button
88+
type="button"
89+
className="rounded-md bg-white dark:bg-black p-2 cursor-pointer text-sm font-semibold text-gray-900 dark:text-white shadow-xs ring-1 ring-gray-300 dark:ring-black/15 ring-inset hover:bg-gray-50 dark:hover:bg-slate-950 inline-flex flex-col items-center justify-center"
90+
onClick={() => props.typeSelected(_type)}
91+
>
92+
<PlusIcon className="size-4" aria-hidden="true" />
93+
</button>
94+
</div>
95+
</div>
96+
<DisclosurePanel
97+
as="div"
98+
transition
99+
className="w-full transition duration-200 ease-in-out py-1.5 flex flex-col gap-y-1"
100+
>
101+
<ul className="w-full pl-6 pr-3 divide-y divide-gray-300 dark:divide-gray-800">
102+
{_type.properties.map((prop) => (
103+
<li key={prop.id} className="w-full text-xs py-1.5 flex items-center gap-x-2 list-disc">
104+
{prop.name || prop.id}
105+
{prop.valueType?.name != null ? (
106+
<p className="text-xs font-light text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-slate-700 ring-gray-500/10 dark:ring-gray-700/10 rounded-md px-1.5 py-0.5 whitespace-nowrap ring-1 ring-inset">
107+
{prop.valueType.name}
108+
</p>
109+
) : null}
110+
</li>
111+
))}
112+
</ul>
113+
</DisclosurePanel>
114+
</Disclosure>
115+
))}
116+
</ul>
117+
</div>
118+
</div>
119+
);
120+
}

0 commit comments

Comments
 (0)