Skip to content

Commit

Permalink
hybrid api generator. (#1101)
Browse files Browse the repository at this point in the history
* hybrid api generator.
  • Loading branch information
b4rtaz authored Apr 24, 2023
1 parent d6f6476 commit 586a711
Show file tree
Hide file tree
Showing 182 changed files with 1,647 additions and 998 deletions.
6 changes: 6 additions & 0 deletions .changeset/dirty-moles-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@moralisweb3/common-evm-utils': minor
'@moralisweb3/evm-api': minor
---

**Breaking change**: The `utils.endpointWeights()` methods returns the `EvmEndpointWeights` class as the result.
9 changes: 9 additions & 0 deletions .changeset/nice-birds-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@moralisweb3/common-evm-utils': minor
'@moralisweb3/streams': minor
'@moralisweb3/evm-api': minor
'@moralisweb3/auth': minor
'moralis': minor
---

**Breaking change**: The `format()` method has been deleted from the `EvmAddress` class. Please format the value by using one of the following properties: `checksum` or `lowercase`.
9 changes: 9 additions & 0 deletions .changeset/popular-maps-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@moralisweb3/common-evm-utils': minor
'@moralisweb3/evm-api': minor
---

**Breaking change**: The `nft.getNFTTrades()` methods returns the `EvmTrade` class as the result. The `EvmNftTrade` class has been deleted. The SDK uses now the `EvmTrade` instead. The `EvmTrade` has two differences with comparison with the `EvmNftTrade`:

- the `transactionIndex` property is the `string` type now (previously `number`),
- the `blockTimestamp` property is `string` type now (previously `Date`).
6 changes: 6 additions & 0 deletions .changeset/unlucky-paws-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@moralisweb3/common-evm-utils': minor
'@moralisweb3/evm-api': minor
---

**Breaking change**: The `utils.web3ApiVersion()` methods returns the `EvmWeb3version` class as the result.
2 changes: 1 addition & 1 deletion demos/express-proxy/src/api/proxyGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Moralis from 'moralis';
import { operations } from 'moralis/common-sol-utils';
import { operations as evmOperations } from 'moralis/common-evm-utils';
import { operationsV2 as evmOperations } from 'moralis/common-evm-utils';
import express from 'express';
import axios from 'axios';
import { errorHandler } from '../middlewares/errorHandler';
Expand Down
2 changes: 1 addition & 1 deletion demos/parse-server-migration/src/cloud/generated/evmApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { MoralisError, Operation } from '@moralisweb3/common-core';
import { handleRateLimit } from '../../rateLimit';
import { upgradeRequest } from '../upgradeRequest'
import { AxiosError } from 'axios';
import { operations } from '@moralisweb3/common-evm-utils';
import { operationsV2 as operations } from '@moralisweb3/common-evm-utils';
declare const Parse: any;

function getErrorMessage(error: Error, name: string) {
Expand Down
2 changes: 1 addition & 1 deletion demos/parse-server/src/api/proxyGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import express from 'express';
import axios from 'axios';
import { errorHandler } from '../middlewares/errorHandler';
import { operations } from 'moralis/common-sol-utils';
import { operations as evmOperations } from 'moralis/common-evm-utils';
import { operationsV2 as evmOperations } from 'moralis/common-evm-utils';
import { convertOperationToDescriptor } from '@moralisweb3/api-utils';

const proxyRouter = express.Router();
Expand Down
1 change: 1 addition & 0 deletions packages/apiGenerator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"gen:aptos": "ts-node src/index.ts ../../common/aptosUtils",
"gen:evm": "ts-node src/index.ts ../../common/evmUtils",
"gen:playground": "ts-node src/index.ts ../playground",
"gen:all": "yarn gen:aptos && yarn gen:evm",
"test": "jest",
"test:watch": "jest --watch"
},
Expand Down
12 changes: 10 additions & 2 deletions packages/apiGenerator/src/generator/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ export class Generator {
const typeResolver = new TypeResolver(configuration.classNamePrefix, mappingResolver, typeInfoResolver);
const typeDeterminantResolver = new TypeDeterminantResolver(configuration.typeDeterminants);
const generatorWriter = new GeneratorWriter(projectPath, configuration.outputDir);
return new Generator(contract, typeResolver, typeDeterminantResolver, typeInfoResolver, generatorWriter);
return new Generator(
contract,
typeResolver,
typeDeterminantResolver,
typeInfoResolver,
generatorWriter,
configuration,
);
}

private readonly typesIndexGenerator = new IndexFileGenerator();
Expand All @@ -43,6 +50,7 @@ export class Generator {
private readonly typeDeterminantResolver: TypeDeterminantResolver,
private readonly typeInfoResolver: TypeInfoResolver,
private readonly writer: GeneratorWriter,
private readonly configuration: GeneratorConfiguration,
) {}

public generate() {
Expand Down Expand Up @@ -76,7 +84,7 @@ export class Generator {
}

private generateOperation(info: OperationInfo) {
const generator = new OperationFileGenerator(info, this.typeResolver);
const generator = new OperationFileGenerator(info, this.typeResolver, this.configuration);
const result = generator.generate();

this.writer.writeOperation(result.className, result.output);
Expand Down
20 changes: 18 additions & 2 deletions packages/apiGenerator/src/generator/GeneratorConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export interface GeneratorConfiguration {
classNamePrefix: string;
mappings: Mappings;
typeDeterminants: TypeDeterminant[];

/**
* @deprecated Temporary solution. Will be removed in the future.
*/
supportV2?: boolean;
}

export interface Mappings {
Expand All @@ -18,11 +23,22 @@ export interface MappingTarget {
}

export interface TypeMapping extends MappingTarget {
typeName: string;
/**
* Mapping by type name. The type name comes from the OpenAPI reader (not from the swagger file).
*
* @example ["BlockMetadataTransaction_changes_Item"]
* @example ["WriteOrUpdateModuleChange"]
*/
typeNames: string[];
}

export interface RefMapping extends MappingTarget {
$ref: string;
/**
* Mapping by $ref.
*
* @example ["#/components/schemas/endpointWeights/properties/price"]
*/
refs: string[];
}

export interface ComplexTypePropertyMapping extends MappingTarget {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export class AbstractClientFileGenerator {
}
output.commitImports();

// TODO: this interface is duplicated in each module now.
output.write(0, `export interface OperationV3<Request, RequestJSON, Response, ResponseJSON, Body, BodyJSON> {`);
output.write(1, `operationId: string;`);
output.write(1, `groupName: string;`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TypeCodesGenerator } from './codeGenerators/TypeCodesGenerator';
import { TypeResolver } from './resolvers/TypeResolver';
import { TypeScriptOutput } from '../output/TypeScriptOutput';
import { OperationParameterModelBuilder } from './modelBuilders/OperationParameterModelBuilder';
import { GeneratorConfiguration } from '../GeneratorConfiguration';

export interface OperationFileGeneratorResult {
className: string;
Expand All @@ -15,7 +16,11 @@ export interface OperationFileGeneratorResult {
export class OperationFileGenerator {
private readonly operationParameterModelBuilder = new OperationParameterModelBuilder(this.typeResolver);

public constructor(private readonly info: OperationInfo, private readonly typeResolver: TypeResolver) {}
public constructor(
private readonly info: OperationInfo,
private readonly typeResolver: TypeResolver,
private readonly configuration: GeneratorConfiguration,
) {}

public generate(): OperationFileGeneratorResult {
const resolvedResponseType = this.info.response ? this.typeResolver.resolve(this.info.response.descriptor) : null;
Expand All @@ -32,7 +37,7 @@ export class OperationFileGenerator {

const parameters = this.operationParameterModelBuilder.build(this.info.parameters);

const parameterRawNames = parameters.map((p) => p.name.rawName);
const rawParameterNames = parameters.map((p) => p.name.rawName);

// view:

Expand Down Expand Up @@ -93,12 +98,19 @@ export class OperationFileGenerator {
output.write(0, '}');
output.newLine();

if (responseTypeCodes && this.configuration.supportV2) {
// TODO: this part is added to support the V2 approach. It should be removed after the V2 is removed.
output.write(0, `export type ${className}Response = ${responseTypeCodes.valueTypeCode};`);
output.write(0, `export type ${className}ResponseJSON = ${responseTypeCodes.jsonTypeCode};`);
output.newLine();
}

output.write(0, `export const ${className} = {`);
output.write(1, `operationId: ${JSON.stringify(this.info.operationId)},`);
output.write(1, `groupName: ${JSON.stringify(this.info.groupName)},`);
output.write(1, `httpMethod: ${JSON.stringify(this.info.httpMethod)},`);
output.write(1, `routePattern: ${JSON.stringify(this.info.routePattern)},`);
output.write(1, `parameterNames: ${JSON.stringify(parameterRawNames)},`);
output.write(1, `parameterNames: ${JSON.stringify(rawParameterNames)},`);
output.write(1, `hasResponse: ${JSON.stringify(!!this.info.response)},`);
output.write(1, `hasBody: ${JSON.stringify(!!this.info.body)},`);
output.newLine();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export class MappingResolver {
public constructor(private readonly mappings: Mappings) {}

public tryResolveByTypeName(typeName: string): TypeMapping | undefined {
return this.mappings.types.find((m) => m.typeName === typeName);
return this.mappings.types.find((m) => m.typeNames.includes(typeName));
}

public tryResolveBy$ref($ref: string): RefMapping | undefined {
return this.mappings.refs.find((r) => r.$ref === $ref);
return this.mappings.refs.find((r) => r.refs.includes($ref));
}

public tryResolveByOperationParameterName(name: string): OperationParameterMapping | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface OpenApiV3ReaderConfiguration {
operations: {
groupRef: string;
isEnabledRef: string;
filterOperationIds?: string[];
virtualParameters?: VirtualParameter[];
};
}
Expand Down
6 changes: 6 additions & 0 deletions packages/apiGenerator/src/reader/v3/OperationsV3Reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export class OperationsV3Reader {
console.warn(`[no-operation-id] Path ${routePattern} does not have operationId`);
continue;
}
if (
this.configuration.operations.filterOperationIds &&
!this.configuration.operations.filterOperationIds.includes(path.operationId)
) {
continue;
}

operationIdUniquenessChecker.check(path.operationId, () => `Operation id ${path.operationId} is duplicated`);

Expand Down
102 changes: 80 additions & 22 deletions packages/apiUtils/scripts/generate-client.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,39 @@ const sourcePackage = require(sourcePackageName);
const fs = require('fs');
const { determineOperationType } = require('@moralisweb3/common-core');

const uniqueGroupNames = new Set(sourcePackage.operations.map((o) => o.groupName));
const operationsV2 = sourcePackage.operationsV2;
const operationsV3 = sourcePackage.operations || [];

const operations = [
...operationsV2.map((operation) => ({
name: operation.name,
groupName: operation.groupName,
operationVarName: `${operation.name}Operation`,
requestClassName: `${capitalizeFirst(operation.name)}Request`,
responseClassName: `${capitalizeFirst(operation.name)}ResponseAdapter`,
hasRequest: Boolean(operation.urlPathParamNames || operation.urlSearchParamNames || operation.bodyParamNames),
hasBody: false,
type: determineOperationType(operation),
})),

...operationsV3.map((operation) => {
const capitalizedName = capitalizeFirst(operation.operationId);
const isPaginated = operation.parameterNames.includes('cursor');
return {
name: operation.operationId,
groupName: operation.groupName,
operationVarName: `${capitalizedName}Operation`,
requestClassName: `${capitalizedName}OperationRequest`,
responseClassName: `${capitalizedName}OperationResponse`,
responseJSONClassName: `${capitalizedName}OperationResponseJSON`,
hasRequest: operation.parameterNames.length > 0,
hasBody: operation.hasBody,
type: isPaginated ? 'paginatedV3' : 'V3',
};
}),
];

const uniqueGroupNames = new Set(operations.map((o) => o.groupName));
const sourcePackageImports = new Set();
const apiUtilsPackageImports = new Set();
const corePackageImports = new Set();
Expand All @@ -28,43 +60,69 @@ for (const groupName of uniqueGroupNames) {
public readonly ${groupName} = {
`;

for (const operation of sourcePackage.operations.filter((o) => o.groupName === groupName)) {
const operationVarName = `${operation.name}Operation`;
const requestClassName = `${capitalizeFirst(operation.name)}Request`;
const responseClassName = `${capitalizeFirst(operation.name)}ResponseAdapter`;

const omitRequest = !operation?.urlPathParamNames && !operation.urlSearchParamNames && !operation.bodyParamNames;

for (const operation of operations.filter((o) => o.groupName === groupName)) {
let resolverClassName;
let returnType;
switch (determineOperationType(operation)) {
let methodArgs;
let fetchArgs;

switch (operation.type) {
case 'nonNullable':
resolverClassName = 'OperationResolver';
returnType = responseClassName;
returnType = operation.responseClassName;
break;
case 'nullable':
resolverClassName = 'NullableOperationResolver';
returnType = `${responseClassName} | null`;
returnType = `${operation.responseClassName} | null`;
break;
case 'paginated':
resolverClassName = 'PaginatedOperationResolver';
returnType = responseClassName;
returnType = operation.responseClassName;
break;
case 'V3':
resolverClassName = 'OperationV3Resolver';
returnType = `ResponseAdapter<${operation.responseClassName}, ${operation.responseJSONClassName}>`;
sourcePackageImports.add(operation.responseJSONClassName);
corePackageImports.add('ResponseAdapter');
break;
case 'paginatedV3':
resolverClassName = 'PaginatedOperationV3Resolver';
returnType = `PaginatedResponseV3Adapter<${operation.responseClassName}, ${operation.responseJSONClassName}>`;
sourcePackageImports.add(operation.responseJSONClassName);
apiUtilsPackageImports.add('PaginatedResponseV3Adapter');
break;
}

switch (operation.type) {
case 'nonNullable':
case 'nullable':
case 'paginated':
methodArgs = operation.hasRequest ? `request: ${operation.requestClassName}` : '';
fetchArgs = operation.hasRequest ? 'request' : '';
break;
case 'V3':
case 'paginatedV3':
methodArgs = operation.hasRequest ? `request: ${operation.requestClassName}` : '';
fetchArgs = operation.hasRequest ? 'request, ' : '{}, ';
if (operation.hasBody) {
// TODO: add support for body.
methodArgs += ', body: unknown';
fetchArgs += 'body';
} else {
fetchArgs += 'null';
}
break;
}

sourcePackageImports.add(operationVarName);
if (!omitRequest) {
sourcePackageImports.add(requestClassName);
sourcePackageImports.add(operation.operationVarName);
if (operation.hasRequest) {
sourcePackageImports.add(operation.requestClassName);
}
sourcePackageImports.add(responseClassName);
sourcePackageImports.add(operation.responseClassName);
apiUtilsPackageImports.add(resolverClassName);

bodyOutput += ` ${operation.name}: (${
omitRequest ? '' : `request: ${requestClassName}`
}): Promise<${returnType}> => {
return new ${resolverClassName}(${operationVarName}, this.baseUrl, this.core).fetch(${
omitRequest ? '{}' : 'request'
});
bodyOutput += ` ${operation.name}: (${methodArgs}): Promise<${returnType}> => {
return new ${resolverClassName}(${operation.operationVarName}, this.baseUrl, this.core).fetch(${fetchArgs});
},
`;
}
Expand Down
1 change: 1 addition & 0 deletions packages/apiUtils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './config';
export * from './resolvers';
export * from './resolvers2';
export * from './resolvers3';
export * from './ApiUtils';
Loading

1 comment on commit 586a711

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage

Title Lines Statements Branches Functions
api-utils Coverage: 20%
20.6% (61/296) 20.48% (17/83) 19.04% (12/63)
auth Coverage: 89%
92.45% (98/106) 83.33% (20/24) 86.66% (26/30)
evm-api Coverage: 100%
100% (91/91) 66.66% (6/9) 100% (58/58)
common-aptos-utils Coverage: 4%
4.5% (149/3305) 4.64% (25/538) 5.52% (45/814)
common-evm-utils Coverage: 69%
69.85% (1444/2067) 31.06% (306/985) 47.83% (397/830)
sol-api Coverage: 96%
97.5% (39/40) 66.66% (6/9) 93.33% (14/15)
common-sol-utils Coverage: 74%
74.55% (167/224) 66.66% (18/27) 65.38% (51/78)
common-streams-utils Coverage: 91%
91.52% (1231/1345) 76.13% (418/549) 82.25% (445/541)
streams Coverage: 88%
88.2% (576/653) 68.81% (64/93) 88.02% (125/142)

Please sign in to comment.