Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,16 @@ pnpm up --interactive --latest -r
## Publishing

```sh
# publish hypergraph
pnpm build
# publish hypergraph
cd packages/hypergraph/publish
pnpm publish
# publish hypergraph-react
pnpm build
cd packages/hypergraph-react/publish
pnpm publish
# publish typesync
cd apps/typesync
pnpm publish --tag latest
```

## Deploying your own SyncServer to Fly.io (single instance)
Expand Down
2 changes: 1 addition & 1 deletion apps/typesync/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@graphprotocol/hypergraph-cli",
"version": "0.0.0-alpha.24",
"version": "0.0.0-alpha.26",
"type": "module",
"license": "MIT",
"description": "CLI toolchain to view existing types, select, pick, extend to create schemas and generate a @graphprotocol/hypergraph schema.",
Expand Down
66 changes: 64 additions & 2 deletions apps/typesync/src/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,64 @@ export class SchemaGenerator extends Effect.Service<SchemaGenerator>()('/typesyn
}
});

/**
* Recursively processes all files in the src directory and replaces
* "Address", "address", and "addresses" with the first schema type
*/
const replaceAddressTerms = (
directory: string,
firstTypeName: string,
): Effect.Effect<void, PlatformError.PlatformError> =>
Effect.gen(function* () {
const srcPath = path.join(directory, 'src');
const srcExists = yield* fs.exists(srcPath);

if (!srcExists) {
return; // No src directory to process
}

const processDirectory = (dirPath: string): Effect.Effect<void, PlatformError.PlatformError> =>
Effect.gen(function* () {
const entries = yield* fs.readDirectory(dirPath);

for (const entry of entries) {
const entryPath = path.join(dirPath, entry);
const stat = yield* fs.stat(entryPath);

if (stat.type === 'Directory') {
// Recursively process subdirectories
yield* processDirectory(entryPath);
} else if (stat.type === 'File') {
// Process files that are likely to contain code
const fileExtension = path.extname(entry);
const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.vue', '.svelte'];

if (codeExtensions.includes(fileExtension)) {
yield* Effect.gen(function* () {
// Read file content
const content = yield* fs.readFileString(entryPath);

// Replace address-related terms with the first type name
const pascalCaseName = Utils.toPascalCase(firstTypeName);
const camelCaseName = Utils.toCamelCase(firstTypeName);
const pluralName = `${camelCaseName}s`; // Simple pluralization

const updatedContent = content
.replace(/\bAddress\b/g, pascalCaseName)
.replace(/\baddress\b/g, camelCaseName)
.replace(/\baddresses\b/g, pluralName);

// Write back the updated content
yield* fs.writeFileString(entryPath, updatedContent);
});
}
}
}
});

yield* processDirectory(srcPath);
});

return {
/**
* Generate on the users machine, at the directory they provided,
Expand All @@ -103,7 +161,9 @@ export class SchemaGenerator extends Effect.Service<SchemaGenerator>()('/typesyn
* 2. copy it to the output directory from the app request
* 3. update the package.json
* 4. update the schema.ts
* 5. cleanup the .tmp directory
* 5. update the mapping.ts
* 6. replace the address terms with the first type name
* 7. cleanup the .tmp directory
*/
yield* cloneTemplRepo.pipe(
Effect.tapErrorCause((cause) =>
Expand All @@ -118,6 +178,7 @@ export class SchemaGenerator extends Effect.Service<SchemaGenerator>()('/typesyn
Effect.andThen(() => updatePackageJson(app, directory)),
Effect.andThen(() => fs.writeFileString(path.join(directory, 'src', 'schema.ts'), buildSchemaFile(app))),
Effect.andThen(() => fs.writeFileString(path.join(directory, 'src', 'mapping.ts'), buildMappingFile(app))),
Effect.andThen(() => replaceAddressTerms(directory, app.types[0]?.name || 'Address')),
Effect.andThen(() => cleanup),
);

Expand Down Expand Up @@ -320,7 +381,8 @@ export function buildMappingFile(schema: Domain.InsertAppSchema) {
}
}

const typeMapping = ` ${type.name}: {
const typeName = Utils.toPascalCase(type.name);
const typeMapping = ` ${typeName}: {
typeIds: [Id.Id('${type.knowledgeGraphId}')],
properties: {
${properties.join(',\n')},
Expand Down
8 changes: 4 additions & 4 deletions apps/typesync/src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { Data, String as EffectString } from 'effect';
* @returns camelCased value of the input string
*/
export function toCamelCase(str: string) {
if (EffectString.isEmpty(str)) {
throw new InvalidInputError({ input: str, cause: 'Input is empty' });
if (EffectString.isEmpty(str) || /^\s+$/.test(str)) {
throw new InvalidInputError({ input: str, cause: 'Input is empty or contains only whitespace' });
}

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

let result = '';
Expand Down
152 changes: 152 additions & 0 deletions apps/typesync/test/Utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { describe, expect, it } from '@effect/vitest';

// @ts-ignore - fix the ts setup
import { toCamelCase, toPascalCase } from '../src/Utils.js';

describe('toCamelCase', () => {
it('should convert space-separated words to camelCase', () => {
expect(toCamelCase('Address line 1')).toBe('addressLine1');
expect(toCamelCase('Academic field')).toBe('academicField');
expect(toCamelCase('User profile')).toBe('userProfile');
expect(toCamelCase('Email address')).toBe('emailAddress');
expect(toCamelCase('Phone number')).toBe('phoneNumber');
});

it('should convert underscore-separated words to camelCase', () => {
expect(toCamelCase('address_line_1')).toBe('addressLine1');
expect(toCamelCase('user_profile')).toBe('userProfile');
expect(toCamelCase('email_address')).toBe('emailAddress');
});

it('should convert hyphen-separated words to camelCase', () => {
expect(toCamelCase('address-line-1')).toBe('addressLine1');
expect(toCamelCase('user-profile')).toBe('userProfile');
expect(toCamelCase('email-address')).toBe('emailAddress');
});

it('should convert mixed separators to camelCase', () => {
expect(toCamelCase('address-line_1')).toBe('addressLine1');
expect(toCamelCase('address-line 1')).toBe('addressLine1');
expect(toCamelCase('user_profile-name')).toBe('userProfileName');
});

it('should handle already camelCase strings', () => {
expect(toCamelCase('addressLine1')).toBe('addressLine1');
expect(toCamelCase('userProfile')).toBe('userProfile');
expect(toCamelCase('emailAddress')).toBe('emailAddress');
});

it('should handle PascalCase strings', () => {
expect(toCamelCase('AddressLine1')).toBe('addressLine1');
expect(toCamelCase('UserProfile')).toBe('userProfile');
expect(toCamelCase('EmailAddress')).toBe('emailAddress');
});

it('should handle uppercase strings', () => {
expect(toCamelCase('ADDRESS_LINE_1')).toBe('addressLine1');
expect(toCamelCase('USER_PROFILE')).toBe('userProfile');
expect(toCamelCase('EMAIL_ADDRESS')).toBe('emailAddress');
});

it('should handle single words', () => {
expect(toCamelCase('Address')).toBe('address');
expect(toCamelCase('User')).toBe('user');
expect(toCamelCase('Email')).toBe('email');
});

it('should handle numbers', () => {
expect(toCamelCase('Address1')).toBe('address1');
expect(toCamelCase('User2')).toBe('user2');
expect(toCamelCase('Email3')).toBe('email3');
});

it('should skip leading non-alphanumeric characters', () => {
expect(toCamelCase('!Address')).toBe('address');
expect(toCamelCase('@User')).toBe('user');
expect(toCamelCase('#Email')).toBe('email');
expect(toCamelCase(' Address')).toBe('address');
});

it('should throw error for empty input or whitespace-only input', () => {
expect(() => toCamelCase('')).toThrow();
expect(() => toCamelCase(' ')).toThrow();
expect(() => toCamelCase(' ')).toThrow();
expect(() => toCamelCase('\t')).toThrow();
expect(() => toCamelCase('\n')).toThrow();
expect(() => toCamelCase(' \t \n ')).toThrow();
});
});

describe('toPascalCase', () => {
it('should convert space-separated words to PascalCase', () => {
expect(toPascalCase('Address line 1')).toBe('AddressLine1');
expect(toPascalCase('Academic field')).toBe('AcademicField');
expect(toPascalCase('User profile')).toBe('UserProfile');
expect(toPascalCase('Email address')).toBe('EmailAddress');
expect(toPascalCase('Phone number')).toBe('PhoneNumber');
});

it('should convert underscore-separated words to PascalCase', () => {
expect(toPascalCase('address_line_1')).toBe('AddressLine1');
expect(toPascalCase('user_profile')).toBe('UserProfile');
expect(toPascalCase('email_address')).toBe('EmailAddress');
});

it('should convert hyphen-separated words to PascalCase', () => {
expect(toPascalCase('address-line-1')).toBe('AddressLine1');
expect(toPascalCase('user-profile')).toBe('UserProfile');
expect(toPascalCase('email-address')).toBe('EmailAddress');
});

it('should convert mixed separators to PascalCase', () => {
expect(toPascalCase('address-line_1')).toBe('AddressLine1');
expect(toPascalCase('address-line 1')).toBe('AddressLine1');
expect(toPascalCase('user_profile-name')).toBe('UserProfileName');
});

it('should handle already PascalCase strings', () => {
expect(toPascalCase('AddressLine1')).toBe('AddressLine1');
expect(toPascalCase('UserProfile')).toBe('UserProfile');
expect(toPascalCase('EmailAddress')).toBe('EmailAddress');
});

it('should handle camelCase strings', () => {
expect(toPascalCase('addressLine1')).toBe('AddressLine1');
expect(toPascalCase('userProfile')).toBe('UserProfile');
expect(toPascalCase('emailAddress')).toBe('EmailAddress');
});

it('should handle uppercase strings', () => {
expect(toPascalCase('ADDRESS_LINE_1')).toBe('AddressLine1');
expect(toPascalCase('USER_PROFILE')).toBe('UserProfile');
expect(toPascalCase('EMAIL_ADDRESS')).toBe('EmailAddress');
});

it('should handle single words', () => {
expect(toPascalCase('Address')).toBe('Address');
expect(toPascalCase('User')).toBe('User');
expect(toPascalCase('Email')).toBe('Email');
});

it('should handle numbers', () => {
expect(toPascalCase('Address1')).toBe('Address1');
expect(toPascalCase('User2')).toBe('User2');
expect(toPascalCase('Email3')).toBe('Email3');
});

it('should skip leading non-alphanumeric characters', () => {
expect(toPascalCase('!Address')).toBe('Address');
expect(toPascalCase('@User')).toBe('User');
expect(toPascalCase('#Email')).toBe('Email');
expect(toPascalCase(' Address')).toBe('Address');
});

it('should throw error for empty input or whitespace-only input', () => {
expect(() => toPascalCase('')).toThrow();
expect(() => toPascalCase(' ')).toThrow();
expect(() => toPascalCase(' ')).toThrow();
expect(() => toPascalCase('\t')).toThrow();
expect(() => toPascalCase('\n')).toThrow();
expect(() => toPascalCase(' \t \n ')).toThrow();
});
});
Loading