diff --git a/src/db/schema.ts b/src/db/schema.ts index ad5621a..990d1c5 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -12,8 +12,8 @@ const ignoreTables = ['adonis_schema', 'adonis_schema_versions'] /** * parse schema information from the provided database connection - * @param db - * @returns + * @param db + * @returns */ export async function schema(db: Database) { const knex = db.connection().getWriteClient() diff --git a/src/extractors/import_extractor.ts b/src/extractors/import_extractor.ts index 7a4627c..9c236e9 100644 --- a/src/extractors/import_extractor.ts +++ b/src/extractors/import_extractor.ts @@ -1,4 +1,4 @@ -import Model from "../model/index.js" +import Model from '../model/index.js' import string from '@adonisjs/core/helpers/string' class ModelImport { @@ -8,7 +8,12 @@ class ModelImport { isDefault = false isType = false - constructor(name: string, namespace: string, isDefault: boolean | null = null, isType: boolean | null = null) { + constructor( + name: string, + namespace: string, + isDefault: boolean | null = null, + isType: boolean | null = null + ) { this.name = name this.namespace = namespace this.isDefault = isDefault ?? this.isDefault @@ -18,7 +23,7 @@ class ModelImport { /** * converts model imports into import strings, grouped by import path * @param imports - * @returns + * @returns */ static getStatements(imports: ModelImport[]) { const groups = this.#getNamespaceGroups(imports) @@ -41,7 +46,7 @@ class ModelImport { /** * group imports by their path * @param imports - * @returns + * @returns */ static #getNamespaceGroups(imports: ModelImport[]) { return imports.reduce>((groups, imp) => { @@ -85,8 +90,8 @@ export default class ModelImportManager { /** * extract import statements from provided model - * @param model - * @returns + * @param model + * @returns */ extract(model: Model) { this.add(new ModelImport('BaseModel', '@adonisjs/lucid/orm')) @@ -100,11 +105,24 @@ export default class ModelImportManager { model.relationships.map((definition) => { if (definition.relatedModelName !== model.name) { - this.add(new ModelImport(definition.relatedModelName, `./${string.snakeCase(definition.relatedModelName)}.js`, true)) + this.add( + new ModelImport( + definition.relatedModelName, + `./${string.snakeCase(definition.relatedModelName)}.js`, + true + ) + ) } this.add(new ModelImport(definition.type, '@adonisjs/lucid/orm')) - this.add(new ModelImport(string.pascalCase(definition.type), '@adonisjs/lucid/types/relations', false, true)) + this.add( + new ModelImport( + string.pascalCase(definition.type), + '@adonisjs/lucid/types/relations', + false, + true + ) + ) }) return ModelImport.getStatements([...this.#imports.values()]) diff --git a/src/extractors/relationship_extractor.ts b/src/extractors/relationship_extractor.ts index 3bae479..5babd68 100644 --- a/src/extractors/relationship_extractor.ts +++ b/src/extractors/relationship_extractor.ts @@ -1,7 +1,7 @@ -import RelationshipTypes from "../enums/relationship_types.js"; -import ModelColumn from "../model/column.js"; -import Model from "../model/index.js"; -import string from '@adonisjs/core/helpers/string'; +import RelationshipTypes from '../enums/relationship_types.js' +import ModelColumn from '../model/column.js' +import Model from '../model/index.js' +import string from '@adonisjs/core/helpers/string' type RelationMap = { type: RelationshipTypes @@ -29,7 +29,7 @@ export class ModelRelationship { declare relatedModelName: string declare decoratorOptions: DecoratorOptions | undefined declare map: RelationMap - + declare decorator: string declare property: string @@ -49,8 +49,8 @@ export class ModelRelationship { /** * gets property name for the relationship definition - * @param relationMap - * @returns + * @param relationMap + * @returns */ #getPropertyName(relationMap: RelationMap) { let propertyName = string.camelCase(relationMap.foreignKeyModel.name) @@ -69,13 +69,15 @@ export class ModelRelationship { /** * gets the relationship's decorator options (if needed) - * @param relationMap - * @returns + * @param relationMap + * @returns */ #getDecoratorOptions(relationMap: RelationMap): DecoratorOptions | undefined { switch (relationMap.type) { case RelationshipTypes.MANY_TO_MANY: - const defaultPivotName = string.snakeCase([relationMap.model.name, relationMap.foreignKeyModel.name].sort().join('_')) + const defaultPivotName = string.snakeCase( + [relationMap.model.name, relationMap.foreignKeyModel.name].sort().join('_') + ) if (relationMap.pivotTableName === defaultPivotName) return @@ -88,27 +90,27 @@ export class ModelRelationship { if (relationMap.column.name === defaultBelongsName) return return { - foreignKey: relationMap.column.name + foreignKey: relationMap.column.name, } case RelationshipTypes.HAS_MANY: case RelationshipTypes.HAS_ONE: const defaultHasName = string.camelCase(relationMap.model.name + 'Id') if (relationMap.foreignKeyColumn.name === defaultHasName) return - + return { - foreignKey: relationMap.foreignKeyColumn.name + foreignKey: relationMap.foreignKeyColumn.name, } } } /** * converts the populated decorator options into a string for the stub - * @returns + * @returns */ #getDecoratorString() { const keys = Object.keys(this.decoratorOptions || {}) as Array - + if (!keys.length) return '' const inner = keys.reduce((str, key) => { @@ -122,23 +124,22 @@ export class ModelRelationship { /** * gets definition decorator and property definition lines for the stub - * @returns + * @returns */ #getDefinition() { return { decorator: `@${this.type}(() => ${this.relatedModelName}${this.#getDecoratorString()})`, - property: `declare ${this.propertyName}: ${string.pascalCase(this.type)}` + property: `declare ${this.propertyName}: ${string.pascalCase(this.type)}`, } } } export default class ModelRelationshipManager { - constructor(protected models: Model[]) {} /** * extract relationships from the loaded models - * @returns + * @returns */ extract() { const relationshpMaps = this.#getRelationshipMaps() @@ -146,14 +147,14 @@ export default class ModelRelationshipManager { } /** - * get mappings for the model's relationships with information + * get mappings for the model's relationships with information * needed to populate their definitions - * @returns + * @returns */ #getRelationshipMaps() { const belongsTos = this.#getBelongsTos() const relationships: RelationMap[] = [] - + belongsTos.map((belongsTo) => { const tableNamesSingular = this.models .filter((model) => model.tableName !== belongsTo.column.tableName) @@ -161,8 +162,12 @@ export default class ModelRelationshipManager { // try to build a pivot table by matching table names with the current table name const tableNameSingular = string.singular(belongsTo.column.tableName) - const startsWithTable = tableNamesSingular.find((name) => tableNameSingular.startsWith(name.singular)) - const endsWithTable = tableNamesSingular.find((name) => tableNameSingular.endsWith(name.singular)) + const startsWithTable = tableNamesSingular.find((name) => + tableNameSingular.startsWith(name.singular) + ) + const endsWithTable = tableNamesSingular.find((name) => + tableNameSingular.endsWith(name.singular) + ) const pivotName = `${startsWithTable?.singular}_${endsWithTable?.singular}` // if they match, consider it a pivot and build a many-to-many relationship from the belongsTo info @@ -170,8 +175,12 @@ export default class ModelRelationshipManager { const isStartsWith = startsWithTable?.model.name === belongsTo.foreignKeyModel.name const relatedModel = isStartsWith ? endsWithTable!.model : startsWithTable!.model const relatedColumn = isStartsWith - ? relatedModel.columns.find((column) => column.tableName === endsWithTable?.model.tableName) - : relatedModel.columns.find((column) => column.tableName === startsWithTable?.model.tableName) + ? relatedModel.columns.find( + (column) => column.tableName === endsWithTable?.model.tableName + ) + : relatedModel.columns.find( + (column) => column.tableName === startsWithTable?.model.tableName + ) // mark the model as a pivot, so it can be ignored belongsTo.model.isPivotTable = true @@ -209,7 +218,7 @@ export default class ModelRelationshipManager { /** * get the belongs to relationships from the foreign key definitions on the models * we'll work backwards from here - * @returns + * @returns */ #getBelongsTos() { const belongsTos: RelationMap[] = [] @@ -219,7 +228,9 @@ export default class ModelRelationshipManager { if (!column.foreignKeyTable) return const foreignKeyModel = this.models.find((m) => m.tableName === column.foreignKeyTable) - const foreignKeyColumn = foreignKeyModel?.columns.find((c) => column.foreignKeyColumn === c.columnName) + const foreignKeyColumn = foreignKeyModel?.columns.find( + (c) => column.foreignKeyColumn === c.columnName + ) if (!foreignKeyColumn || !foreignKeyModel) return diff --git a/src/extractors/type_extractor.ts b/src/extractors/type_extractor.ts index e6841e5..c1f84cf 100644 --- a/src/extractors/type_extractor.ts +++ b/src/extractors/type_extractor.ts @@ -1,8 +1,8 @@ /** * Database Type to TypeScript Type Extractor * TODO: Add support for MSSql and other missing Lucid Db Drivers - * - * Based on ehmpathy/sql-code-generator: + * + * Based on ehmpathy/sql-code-generator: * https://github.com/ehmpathy/sql-code-generator/blob/main/src/logic/sqlToTypeDefinitions/resource/common/extractDataTypeFromColumnOrArgumentDefinitionSql.ts */ @@ -10,31 +10,21 @@ // https://dev.mysql.com/doc/refman/8.0/en/string-types.html const mysqlStringTypes = [ - 'CHAR', - 'VARCHAR', - 'BLOB', - 'TEXT', + 'CHAR', + 'VARCHAR', + 'BLOB', + 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', - 'ENUM', - 'SET' + 'ENUM', + 'SET', ] //www.postgresql.org/docs/9.5/datatype-character.html -const pgStringTypes = [ - 'CHARACTER', - 'CHAR', - 'CHARACTER VARYING', - 'VARCHAR', - 'TEXT', - 'UUID', -] +const pgStringTypes = ['CHARACTER', 'CHAR', 'CHARACTER VARYING', 'VARCHAR', 'TEXT', 'UUID'] -const dbStringTypes = new Set([ - ...mysqlStringTypes, - ...pgStringTypes -]) +const dbStringTypes = new Set([...mysqlStringTypes, ...pgStringTypes]) // #endregion // #region Numbers @@ -75,10 +65,7 @@ const pgNumberTypes = [ 'SERIAL8', ] -const dbNumberTypes = new Set([ - ...mysqlNumberTypes, - ...pgNumberTypes -]) +const dbNumberTypes = new Set([...mysqlNumberTypes, ...pgNumberTypes]) // #endregion // #region Dates @@ -97,24 +84,18 @@ const pgDateTypes = [ 'TIME WITH TIMEZONE', ] -const dbDateTypes = new Set([ - ...mysqlDateTypes, - ...pgDateTypes -]) +const dbDateTypes = new Set([...mysqlDateTypes, ...pgDateTypes]) // #endregion // #region Binary // https://dev.mysql.com/doc/refman/8.0/en/binary-varbinary.html -const mysqlBinaryTypes = ['BINARY', 'VARBINARY']; +const mysqlBinaryTypes = ['BINARY', 'VARBINARY'] // https://www.postgresql.org/docs/9.5/datatype-binary.html -const pgBinaryTypes = ['BYTEA']; +const pgBinaryTypes = ['BYTEA'] -const dbBinaryTypes = new Set([ - ...mysqlBinaryTypes, - ...pgBinaryTypes -]) +const dbBinaryTypes = new Set([...mysqlBinaryTypes, ...pgBinaryTypes]) // #endregion // #region Booleans @@ -125,10 +106,7 @@ const mysqlBooleanTypes = ['BOOL', 'BOOLEAN'] // https://www.postgresql.org/docs/9.5/datatype-boolean.html const pgBooleanTypes = ['BOOLEAN'] -const dbBooleanTypes = new Set([ - ...mysqlBooleanTypes, - ...pgBooleanTypes -]) +const dbBooleanTypes = new Set([...mysqlBooleanTypes, ...pgBooleanTypes]) // #endregion // #region JSON @@ -139,34 +117,25 @@ const mysqlJsonTypes = ['JSON'] // https://www.postgresql.org/docs/9.5/datatype-json.html const pgJsonTypes = ['JSON', 'JSONB'] -const dbJsonTypes = new Set([ - ...mysqlJsonTypes, - ...pgJsonTypes -]) +const dbJsonTypes = new Set([...mysqlJsonTypes, ...pgJsonTypes]) // #endregion /** * extract the TypeScript type from the database type - * @param dbDataType - * @returns + * @param dbDataType + * @returns */ export function extractColumnTypeScriptType(dbDataType: string) { const isArray = dbDataType.endsWith('[]') const normalizedDbType = dbDataType.toUpperCase().split('[]')[0].trim() - if (dbStringTypes.has(normalizedDbType)) - return isArray ? 'string[]' : 'string' - if (dbNumberTypes.has(normalizedDbType)) - return isArray ? 'number[]' : 'number' - if (dbDateTypes.has(normalizedDbType)) - return 'DateTime' - if (dbBinaryTypes.has(normalizedDbType)) - return 'Buffer' - if (dbBooleanTypes.has(normalizedDbType)) - return 'boolean' - if (dbJsonTypes.has(normalizedDbType)) - return 'Record' - + if (dbStringTypes.has(normalizedDbType)) return isArray ? 'string[]' : 'string' + if (dbNumberTypes.has(normalizedDbType)) return isArray ? 'number[]' : 'number' + if (dbDateTypes.has(normalizedDbType)) return 'DateTime' + if (dbBinaryTypes.has(normalizedDbType)) return 'Buffer' + if (dbBooleanTypes.has(normalizedDbType)) return 'boolean' + if (dbJsonTypes.has(normalizedDbType)) return 'Record' + return 'unknown' } diff --git a/src/model/column.ts b/src/model/column.ts index a80df67..a43f7a5 100644 --- a/src/model/column.ts +++ b/src/model/column.ts @@ -31,7 +31,7 @@ export default class ModelColumn { /** * get the column decorator for the column's type - * @returns + * @returns */ getDecorator() { if (this.isPrimary) { diff --git a/src/model/index.ts b/src/model/index.ts index fde78bc..742e2de 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -2,7 +2,9 @@ import { generators } from '@adonisjs/core/app' import ModelColumn from './column.js' import { TableSchema } from '../db/schema.js' import ModelImportManager from '../extractors/import_extractor.js' -import ModelRelationshipManager, { ModelRelationship } from '../extractors/relationship_extractor.js'; +import ModelRelationshipManager, { + ModelRelationship, +} from '../extractors/relationship_extractor.js' export default class Model { declare name: string @@ -23,14 +25,14 @@ export default class Model { /** * build the model definitions from the tables - * @param tables - * @returns + * @param tables + * @returns */ static build(tables: TableSchema[]) { const models = this.#getModelsFromTables(tables) const relationshipManager = new ModelRelationshipManager(models) const relationships = relationshipManager.extract() - + for (let model of models) { const importManager = new ModelImportManager() @@ -43,8 +45,8 @@ export default class Model { /** * convert tables into model definitions - * @param tables - * @returns + * @param tables + * @returns */ static #getModelsFromTables(tables: TableSchema[]) { return tables.map((table) => {