Skip to content

Commit 6471996

Browse files
authored
(typesync) fix app template (#299)
1 parent 1cbcb80 commit 6471996

File tree

5 files changed

+225
-9
lines changed

5 files changed

+225
-9
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,16 @@ pnpm up --interactive --latest -r
7979
## Publishing
8080

8181
```sh
82-
# publish hypergraph
8382
pnpm build
83+
# publish hypergraph
8484
cd packages/hypergraph/publish
8585
pnpm publish
8686
# publish hypergraph-react
87-
pnpm build
8887
cd packages/hypergraph-react/publish
8988
pnpm publish
89+
# publish typesync
90+
cd apps/typesync
91+
pnpm publish --tag latest
9092
```
9193

9294
## Deploying your own SyncServer to Fly.io (single instance)

apps/typesync/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@graphprotocol/hypergraph-cli",
3-
"version": "0.0.0-alpha.24",
3+
"version": "0.0.0-alpha.26",
44
"type": "module",
55
"license": "MIT",
66
"description": "CLI toolchain to view existing types, select, pick, extend to create schemas and generate a @graphprotocol/hypergraph schema.",

apps/typesync/src/Generator.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,64 @@ export class SchemaGenerator extends Effect.Service<SchemaGenerator>()('/typesyn
7878
}
7979
});
8080

81+
/**
82+
* Recursively processes all files in the src directory and replaces
83+
* "Address", "address", and "addresses" with the first schema type
84+
*/
85+
const replaceAddressTerms = (
86+
directory: string,
87+
firstTypeName: string,
88+
): Effect.Effect<void, PlatformError.PlatformError> =>
89+
Effect.gen(function* () {
90+
const srcPath = path.join(directory, 'src');
91+
const srcExists = yield* fs.exists(srcPath);
92+
93+
if (!srcExists) {
94+
return; // No src directory to process
95+
}
96+
97+
const processDirectory = (dirPath: string): Effect.Effect<void, PlatformError.PlatformError> =>
98+
Effect.gen(function* () {
99+
const entries = yield* fs.readDirectory(dirPath);
100+
101+
for (const entry of entries) {
102+
const entryPath = path.join(dirPath, entry);
103+
const stat = yield* fs.stat(entryPath);
104+
105+
if (stat.type === 'Directory') {
106+
// Recursively process subdirectories
107+
yield* processDirectory(entryPath);
108+
} else if (stat.type === 'File') {
109+
// Process files that are likely to contain code
110+
const fileExtension = path.extname(entry);
111+
const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.vue', '.svelte'];
112+
113+
if (codeExtensions.includes(fileExtension)) {
114+
yield* Effect.gen(function* () {
115+
// Read file content
116+
const content = yield* fs.readFileString(entryPath);
117+
118+
// Replace address-related terms with the first type name
119+
const pascalCaseName = Utils.toPascalCase(firstTypeName);
120+
const camelCaseName = Utils.toCamelCase(firstTypeName);
121+
const pluralName = `${camelCaseName}s`; // Simple pluralization
122+
123+
const updatedContent = content
124+
.replace(/\bAddress\b/g, pascalCaseName)
125+
.replace(/\baddress\b/g, camelCaseName)
126+
.replace(/\baddresses\b/g, pluralName);
127+
128+
// Write back the updated content
129+
yield* fs.writeFileString(entryPath, updatedContent);
130+
});
131+
}
132+
}
133+
}
134+
});
135+
136+
yield* processDirectory(srcPath);
137+
});
138+
81139
return {
82140
/**
83141
* Generate on the users machine, at the directory they provided,
@@ -103,7 +161,9 @@ export class SchemaGenerator extends Effect.Service<SchemaGenerator>()('/typesyn
103161
* 2. copy it to the output directory from the app request
104162
* 3. update the package.json
105163
* 4. update the schema.ts
106-
* 5. cleanup the .tmp directory
164+
* 5. update the mapping.ts
165+
* 6. replace the address terms with the first type name
166+
* 7. cleanup the .tmp directory
107167
*/
108168
yield* cloneTemplRepo.pipe(
109169
Effect.tapErrorCause((cause) =>
@@ -118,6 +178,7 @@ export class SchemaGenerator extends Effect.Service<SchemaGenerator>()('/typesyn
118178
Effect.andThen(() => updatePackageJson(app, directory)),
119179
Effect.andThen(() => fs.writeFileString(path.join(directory, 'src', 'schema.ts'), buildSchemaFile(app))),
120180
Effect.andThen(() => fs.writeFileString(path.join(directory, 'src', 'mapping.ts'), buildMappingFile(app))),
181+
Effect.andThen(() => replaceAddressTerms(directory, app.types[0]?.name || 'Address')),
121182
Effect.andThen(() => cleanup),
122183
);
123184

@@ -320,7 +381,8 @@ export function buildMappingFile(schema: Domain.InsertAppSchema) {
320381
}
321382
}
322383

323-
const typeMapping = ` ${type.name}: {
384+
const typeName = Utils.toPascalCase(type.name);
385+
const typeMapping = ` ${typeName}: {
324386
typeIds: [Id.Id('${type.knowledgeGraphId}')],
325387
properties: {
326388
${properties.join(',\n')},

apps/typesync/src/Utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import { Data, String as EffectString } from 'effect';
2323
* @returns camelCased value of the input string
2424
*/
2525
export function toCamelCase(str: string) {
26-
if (EffectString.isEmpty(str)) {
27-
throw new InvalidInputError({ input: str, cause: 'Input is empty' });
26+
if (EffectString.isEmpty(str) || /^\s+$/.test(str)) {
27+
throw new InvalidInputError({ input: str, cause: 'Input is empty or contains only whitespace' });
2828
}
2929

3030
let result = '';
@@ -85,8 +85,8 @@ export function toCamelCase(str: string) {
8585
* @returns PascalCased value of the input string
8686
*/
8787
export function toPascalCase(str: string): string {
88-
if (EffectString.isEmpty(str)) {
89-
throw new InvalidInputError({ input: str, cause: 'Input is empty' });
88+
if (EffectString.isEmpty(str) || /^\s+$/.test(str)) {
89+
throw new InvalidInputError({ input: str, cause: 'Input is empty or contains only whitespace' });
9090
}
9191

9292
let result = '';

apps/typesync/test/Utils.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { describe, expect, it } from '@effect/vitest';
2+
3+
// @ts-ignore - fix the ts setup
4+
import { toCamelCase, toPascalCase } from '../src/Utils.js';
5+
6+
describe('toCamelCase', () => {
7+
it('should convert space-separated words to camelCase', () => {
8+
expect(toCamelCase('Address line 1')).toBe('addressLine1');
9+
expect(toCamelCase('Academic field')).toBe('academicField');
10+
expect(toCamelCase('User profile')).toBe('userProfile');
11+
expect(toCamelCase('Email address')).toBe('emailAddress');
12+
expect(toCamelCase('Phone number')).toBe('phoneNumber');
13+
});
14+
15+
it('should convert underscore-separated words to camelCase', () => {
16+
expect(toCamelCase('address_line_1')).toBe('addressLine1');
17+
expect(toCamelCase('user_profile')).toBe('userProfile');
18+
expect(toCamelCase('email_address')).toBe('emailAddress');
19+
});
20+
21+
it('should convert hyphen-separated words to camelCase', () => {
22+
expect(toCamelCase('address-line-1')).toBe('addressLine1');
23+
expect(toCamelCase('user-profile')).toBe('userProfile');
24+
expect(toCamelCase('email-address')).toBe('emailAddress');
25+
});
26+
27+
it('should convert mixed separators to camelCase', () => {
28+
expect(toCamelCase('address-line_1')).toBe('addressLine1');
29+
expect(toCamelCase('address-line 1')).toBe('addressLine1');
30+
expect(toCamelCase('user_profile-name')).toBe('userProfileName');
31+
});
32+
33+
it('should handle already camelCase strings', () => {
34+
expect(toCamelCase('addressLine1')).toBe('addressLine1');
35+
expect(toCamelCase('userProfile')).toBe('userProfile');
36+
expect(toCamelCase('emailAddress')).toBe('emailAddress');
37+
});
38+
39+
it('should handle PascalCase strings', () => {
40+
expect(toCamelCase('AddressLine1')).toBe('addressLine1');
41+
expect(toCamelCase('UserProfile')).toBe('userProfile');
42+
expect(toCamelCase('EmailAddress')).toBe('emailAddress');
43+
});
44+
45+
it('should handle uppercase strings', () => {
46+
expect(toCamelCase('ADDRESS_LINE_1')).toBe('addressLine1');
47+
expect(toCamelCase('USER_PROFILE')).toBe('userProfile');
48+
expect(toCamelCase('EMAIL_ADDRESS')).toBe('emailAddress');
49+
});
50+
51+
it('should handle single words', () => {
52+
expect(toCamelCase('Address')).toBe('address');
53+
expect(toCamelCase('User')).toBe('user');
54+
expect(toCamelCase('Email')).toBe('email');
55+
});
56+
57+
it('should handle numbers', () => {
58+
expect(toCamelCase('Address1')).toBe('address1');
59+
expect(toCamelCase('User2')).toBe('user2');
60+
expect(toCamelCase('Email3')).toBe('email3');
61+
});
62+
63+
it('should skip leading non-alphanumeric characters', () => {
64+
expect(toCamelCase('!Address')).toBe('address');
65+
expect(toCamelCase('@User')).toBe('user');
66+
expect(toCamelCase('#Email')).toBe('email');
67+
expect(toCamelCase(' Address')).toBe('address');
68+
});
69+
70+
it('should throw error for empty input or whitespace-only input', () => {
71+
expect(() => toCamelCase('')).toThrow();
72+
expect(() => toCamelCase(' ')).toThrow();
73+
expect(() => toCamelCase(' ')).toThrow();
74+
expect(() => toCamelCase('\t')).toThrow();
75+
expect(() => toCamelCase('\n')).toThrow();
76+
expect(() => toCamelCase(' \t \n ')).toThrow();
77+
});
78+
});
79+
80+
describe('toPascalCase', () => {
81+
it('should convert space-separated words to PascalCase', () => {
82+
expect(toPascalCase('Address line 1')).toBe('AddressLine1');
83+
expect(toPascalCase('Academic field')).toBe('AcademicField');
84+
expect(toPascalCase('User profile')).toBe('UserProfile');
85+
expect(toPascalCase('Email address')).toBe('EmailAddress');
86+
expect(toPascalCase('Phone number')).toBe('PhoneNumber');
87+
});
88+
89+
it('should convert underscore-separated words to PascalCase', () => {
90+
expect(toPascalCase('address_line_1')).toBe('AddressLine1');
91+
expect(toPascalCase('user_profile')).toBe('UserProfile');
92+
expect(toPascalCase('email_address')).toBe('EmailAddress');
93+
});
94+
95+
it('should convert hyphen-separated words to PascalCase', () => {
96+
expect(toPascalCase('address-line-1')).toBe('AddressLine1');
97+
expect(toPascalCase('user-profile')).toBe('UserProfile');
98+
expect(toPascalCase('email-address')).toBe('EmailAddress');
99+
});
100+
101+
it('should convert mixed separators to PascalCase', () => {
102+
expect(toPascalCase('address-line_1')).toBe('AddressLine1');
103+
expect(toPascalCase('address-line 1')).toBe('AddressLine1');
104+
expect(toPascalCase('user_profile-name')).toBe('UserProfileName');
105+
});
106+
107+
it('should handle already PascalCase strings', () => {
108+
expect(toPascalCase('AddressLine1')).toBe('AddressLine1');
109+
expect(toPascalCase('UserProfile')).toBe('UserProfile');
110+
expect(toPascalCase('EmailAddress')).toBe('EmailAddress');
111+
});
112+
113+
it('should handle camelCase strings', () => {
114+
expect(toPascalCase('addressLine1')).toBe('AddressLine1');
115+
expect(toPascalCase('userProfile')).toBe('UserProfile');
116+
expect(toPascalCase('emailAddress')).toBe('EmailAddress');
117+
});
118+
119+
it('should handle uppercase strings', () => {
120+
expect(toPascalCase('ADDRESS_LINE_1')).toBe('AddressLine1');
121+
expect(toPascalCase('USER_PROFILE')).toBe('UserProfile');
122+
expect(toPascalCase('EMAIL_ADDRESS')).toBe('EmailAddress');
123+
});
124+
125+
it('should handle single words', () => {
126+
expect(toPascalCase('Address')).toBe('Address');
127+
expect(toPascalCase('User')).toBe('User');
128+
expect(toPascalCase('Email')).toBe('Email');
129+
});
130+
131+
it('should handle numbers', () => {
132+
expect(toPascalCase('Address1')).toBe('Address1');
133+
expect(toPascalCase('User2')).toBe('User2');
134+
expect(toPascalCase('Email3')).toBe('Email3');
135+
});
136+
137+
it('should skip leading non-alphanumeric characters', () => {
138+
expect(toPascalCase('!Address')).toBe('Address');
139+
expect(toPascalCase('@User')).toBe('User');
140+
expect(toPascalCase('#Email')).toBe('Email');
141+
expect(toPascalCase(' Address')).toBe('Address');
142+
});
143+
144+
it('should throw error for empty input or whitespace-only input', () => {
145+
expect(() => toPascalCase('')).toThrow();
146+
expect(() => toPascalCase(' ')).toThrow();
147+
expect(() => toPascalCase(' ')).toThrow();
148+
expect(() => toPascalCase('\t')).toThrow();
149+
expect(() => toPascalCase('\n')).toThrow();
150+
expect(() => toPascalCase(' \t \n ')).toThrow();
151+
});
152+
});

0 commit comments

Comments
 (0)