-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #955 from near/main
Prod Release 31/07/2024
- Loading branch information
Showing
13 changed files
with
664 additions
and
383 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import type { Schema } from 'genson-js/dist/types'; | ||
|
||
import type { Event, Method } from '@/pages/api/generateCode'; | ||
|
||
export interface GeneratedCode { | ||
jsCode: string; | ||
sqlCode: string; | ||
} | ||
|
||
interface Column { | ||
name: string; | ||
sql: string; | ||
} | ||
|
||
function sanitizeTableName(tableName: string): string { | ||
// Convert to PascalCase | ||
let pascalCaseTableName = tableName | ||
// Replace special characters with underscores | ||
.replace(/[^a-zA-Z0-9_]/g, '_') | ||
// Makes first letter and any letters following an underscore upper case | ||
.replace(/^([a-zA-Z])|_([a-zA-Z])/g, (match: string) => match.toUpperCase()) | ||
// Removes all underscores | ||
.replace(/_/g, ''); | ||
|
||
// Add underscore if first character is a number | ||
if (/^[0-9]/.test(pascalCaseTableName)) { | ||
pascalCaseTableName = '_' + pascalCaseTableName; | ||
} | ||
|
||
return pascalCaseTableName; | ||
} | ||
|
||
const createColumn = (columnName: string, schema: Schema): Column => { | ||
let type: string; | ||
switch (schema.type) { | ||
case 'string': | ||
type = 'TEXT'; | ||
break; | ||
case 'integer': | ||
type = 'INT'; | ||
break; | ||
case 'number': | ||
type = 'FLOAT'; | ||
break; | ||
case 'boolean': | ||
type = 'BOOLEAN'; | ||
break; | ||
case 'array': | ||
type = 'TEXT[]'; | ||
break; | ||
case 'object': | ||
type = 'JSONB'; | ||
break; | ||
default: | ||
type = 'TEXT'; | ||
} | ||
return { name: columnName, sql: `"${columnName}" ${type}` }; | ||
}; | ||
|
||
export class WizardCodeGenerator { | ||
constructor(private contractFilter: string, private selectedMethods: Method[], private selectedEvents?: Event[]) {} | ||
|
||
private getColumns(method: Method): Column[] { | ||
if (!method.schema.properties) { | ||
return []; | ||
} | ||
return Object.entries(method.schema.properties).map(([k, v]) => createColumn(k, v)); | ||
} | ||
|
||
private getTableName(method: Method): { contextDbName: string; tableName: string } { | ||
const tableName = `calls_to_${method.method_name}`; | ||
return { tableName, contextDbName: sanitizeTableName(tableName) }; | ||
} | ||
|
||
private generateSQLForMethod(method: Method): string { | ||
if (!method.schema.properties) { | ||
return ''; | ||
} | ||
const { tableName } = this.getTableName(method); | ||
const columns = this.getColumns(method); | ||
|
||
// TODO: add NULLABLE for optional fields | ||
return ` | ||
CREATE TABLE ${tableName} | ||
( | ||
"block_height" INT, | ||
"block_timestamp" TIMESTAMP, | ||
"signer_id" TEXT, | ||
"receipt_id" TEXT, | ||
${columns.map((c) => ` ${c.sql},`).join('\n')} | ||
PRIMARY KEY ("receipt_id") | ||
); | ||
-- Consider adding an index (https://www.postgresql.org/docs/14/sql-createindex.html) on a frequently queried column, e.g.: | ||
${columns.map((c) => `-- CREATE INDEX "${tableName}_${c.name}_key" ON "${tableName}" ("${c.name}" ASC);`).join('\n')} | ||
`; | ||
} | ||
|
||
private generateJSForMethod(method: Method): string { | ||
const columnNames = this.getColumns(method).map((c) => c.name); | ||
const primaryKeys = ['receipt_id']; | ||
const { contextDbName } = this.getTableName(method); | ||
const methodName = method.method_name; | ||
return ` | ||
// Extract and upsert ${methodName} function calls | ||
const callsTo${methodName} = extractFunctionCallEntity("${this.contractFilter}", "${methodName}", ${JSON.stringify( | ||
columnNames, | ||
)}); | ||
try { | ||
await context.db.${contextDbName}.upsert(callsTo${methodName}, ${JSON.stringify(primaryKeys)}, ${JSON.stringify( | ||
columnNames, | ||
)}); | ||
} catch(e) { | ||
context.error(\`Unable to upsert ${methodName} function calls: \$\{e.message\}\`); | ||
} | ||
`; | ||
} | ||
|
||
private generateJSCode(): string { | ||
return ` | ||
function extractFunctionCallEntity(contractFilter, methodName, argsToInclude) { | ||
const jsonify = (v) => { | ||
if ((typeof v === "object" && v !== null) || Array.isArray(v)) | ||
return JSON.stringify(v); | ||
return v; | ||
}; | ||
return block | ||
.functionCallsToReceiver(contractFilter, methodName) | ||
.map((fc) => { | ||
let fcArgs = {}; | ||
try { | ||
fcArgs = fc.argsAsJSON(); | ||
} catch (e) { | ||
console.log( | ||
\`Failed to parse args \$\{fc.args\} into JSON for \$\{fc.methodName\}\` | ||
); | ||
} | ||
const extractedArgs = Object.fromEntries( | ||
Object.entries(fcArgs) | ||
.filter(([k]) => argsToInclude.includes(k)) | ||
.map(([k, v]) => [k, jsonify(v)]) | ||
); | ||
return { | ||
block_height: block.blockHeight, | ||
block_timestamp: block.timestamp, | ||
signer_id: fc.signerId, | ||
receipt_id: fc.receiptId, | ||
...extractedArgs, | ||
}; | ||
}); | ||
} | ||
${this.selectedMethods.map((m) => this.generateJSForMethod(m)).join('\n')} | ||
`; | ||
} | ||
|
||
public generateCode(): GeneratedCode { | ||
const jsCode = this.generateJSCode(); | ||
const sqlCode = this.selectedMethods.map((m) => this.generateSQLForMethod(m)).join('\n'); | ||
return { jsCode, sqlCode }; | ||
} | ||
} |
Oops, something went wrong.