Skip to content

Commit

Permalink
wip(stub): working on getting the stubbed relationships added in, nee…
Browse files Browse the repository at this point in the history
…d formatting, cleaning up, and to add imports
  • Loading branch information
tomgobich committed Sep 16, 2024
1 parent 9b3b9a0 commit 49538ec
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 12 deletions.
11 changes: 10 additions & 1 deletion commands/generate_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BaseCommand } from '@adonisjs/core/ace'
import { CommandOptions } from '@adonisjs/core/types/ace'
import Model from '../src/model/index.js'
import { schema } from '../src/db/schema.js'
import { stubsRoot } from '../stubs/main.js'

export default class GenerateModels extends BaseCommand {
static commandName = 'generate:models'
Expand All @@ -12,10 +13,18 @@ export default class GenerateModels extends BaseCommand {
}

async run() {
const codemods = await this.createCodemods()
const db = await this.app.container.make('lucid.db')
const { tables } = await schema(db)
const models = Model.build(tables)

console.log({ models })
for (let model of models) {
await codemods.makeUsingStub(stubsRoot, 'generate/model.stub', {

Check failure on line 22 in commands/generate_models.ts

View workflow job for this annotation

GitHub Actions / windows (20.10.0)

Delete `·`

Check failure on line 22 in commands/generate_models.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 20.10.0)

Delete `·`
model,
relationships: model.relationships.reduce<{ decorator: string; property: string; }[]>((relationships, relationship) => {

Check failure on line 24 in commands/generate_models.ts

View workflow job for this annotation

GitHub Actions / windows (20.10.0)

Replace `;·}[]>(` with `·}[]>(␍⏎··········`

Check failure on line 24 in commands/generate_models.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 20.10.0)

Replace `;·}[]>(` with `·}[]>(⏎··········`
return [...relationships, ...relationship.getDefinitions(model.name)]

Check failure on line 25 in commands/generate_models.ts

View workflow job for this annotation

GitHub Actions / windows (20.10.0)

Insert `··`

Check failure on line 25 in commands/generate_models.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 20.10.0)

Insert `··`
}, [])

Check failure on line 26 in commands/generate_models.ts

View workflow job for this annotation

GitHub Actions / windows (20.10.0)

Replace `},·[])` with `··},␍⏎··········[]␍⏎········),`

Check failure on line 26 in commands/generate_models.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 20.10.0)

Replace `},·[])` with `··},⏎··········[]⏎········),`
})
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"quick:test": "node --import=./tsnode.esm.js --enable-source-maps bin/test.ts",
"pretest": "npm run lint",
"test": "c8 npm run quick:test",
"prebuild": "npm run lint && npm run clean",
"prebuild": "npm run clean",
"build": "tsc",
"postbuild": "npm run copy:templates && npm run index:commands",
"index:commands": "adonis-kit index build/commands",
Expand Down
5 changes: 4 additions & 1 deletion src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ export type TableSchema = {
columns: Column[]
}

const ignoreTables = ['adonis_schema', 'adonis_schema_versions']

export async function schema(db: Database) {
const knex = db.connection().getWriteClient()
const inspector = schemaInspector(knex)
const tableNames = await inspector.tables()
const promises = tableNames.map(async (name) => ({
const targetTableNames = tableNames.filter((name) => !ignoreTables.includes(name))
const promises = targetTableNames.map(async (name) => ({
name,
columns: await inspector.columnInfo(name),
}))
Expand Down
167 changes: 167 additions & 0 deletions src/extractors/type_extractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Database Type to TypeScript Type Extractor
* TODO: Add support for MSSql and other missing Lucid Db Drivers
*
* Based on ehmpathy/sql-code-generator:
* https://github.com/ehmpathy/sql-code-generator/blob/main/src/logic/sqlToTypeDefinitions/resource/common/extractDataTypeFromColumnOrArgumentDefinitionSql.ts
*/

// #region Strings

// https://dev.mysql.com/doc/refman/8.0/en/string-types.html
const mysqlStringTypes = [
'CHAR',
'VARCHAR',
'BLOB',
'TEXT',
'TINYTEXT',
'MEDIUMTEXT',
'LONGTEXT',
'ENUM',
'SET'
]

//www.postgresql.org/docs/9.5/datatype-character.html
const pgStringTypes = [
'CHARACTER',
'CHAR',
'CHARACTER VARYING',
'VARCHAR',
'TEXT',
'UUID',
]

const dbStringTypes = new Set([
...mysqlStringTypes,
...pgStringTypes
])

// #endregion
// #region Numbers

// https://dev.mysql.com/doc/refman/8.0/en/numeric-types.html
const mysqlNumberTypes = [
'INTEGER',
'INT',
'SMALLINT',
'TINYINT',
'MEDIUMINT',
'BIGINT',
'DECIMAL',
'NUMERIC',
'FLOAT',
'DOUBLE',
]

// https://www.postgresql.org/docs/9.5/datatype-numeric.html
const pgNumberTypes = [
'SMALLINT',
'INT2',
'INTEGER',
'INT',
'INT4',
'BIGINT',
'INT8',
'DECIMAL',
'NUMERIC',
'REAL',
'DOUBLE PRECISION',
'FLOAT8',
'SMALLSERIAL',
'SERIAL2',
'SERIAL',
'SERIAL4',
'BIGSERIAL',
'SERIAL8',
]

const dbNumberTypes = new Set([
...mysqlNumberTypes,
...pgNumberTypes
])

// #endregion
// #region Dates

// https://dev.mysql.com/doc/refman/8.0/en/date-and-time-types.html
const mysqlDateTypes = ['DATE', 'TIME', 'DATETIME', 'TIMESTAMP', 'YEAR']

// https://www.postgresql.org/docs/9.5/datatype-datetime.html
const pgDateTypes = [
'TIMESTAMP',
'TIMESTAMPTZ',
'TIMESTAMP WITH TIME ZONE',
'DATE',
'TIME',
'TIMETZ',
'TIME WITH TIMEZONE',
]

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'];

// https://www.postgresql.org/docs/9.5/datatype-binary.html
const pgBinaryTypes = ['BYTEA'];

const dbBinaryTypes = new Set([
...mysqlBinaryTypes,
...pgBinaryTypes
])

// #endregion
// #region Booleans

// https://dev.mysql.com/doc/refman/8.1/en/numeric-type-syntax.html
const mysqlBooleanTypes = ['BOOL', 'BOOLEAN']

// https://www.postgresql.org/docs/9.5/datatype-boolean.html
const pgBooleanTypes = ['BOOLEAN']

const dbBooleanTypes = new Set([
...mysqlBooleanTypes,
...pgBooleanTypes
])

// #endregion
// #region JSON

// https://dev.mysql.com/doc/refman/8.0/en/json.html
const mysqlJsonTypes = ['JSON']

// https://www.postgresql.org/docs/9.5/datatype-json.html
const pgJsonTypes = ['JSON', 'JSONB']

const dbJsonTypes = new Set([
...mysqlJsonTypes,
...pgJsonTypes
])

// #endregion

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<string, any>'

return 'unknown'
}
26 changes: 25 additions & 1 deletion src/model/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { Column } from 'knex-schema-inspector/dist/types/column.js'
import RelationshipTypes from '../enums/relationship_types.js'
import Model from './index.js'
import ModelRelationship from './relationship.js'
import { extractColumnTypeScriptType } from '../extractors/type_extractor.js'

export default class ModelColumn {
declare name: string
declare columnName: string
declare tableName: string
declare type: string // TODO - potential reference: https://github.com/danvk/pg-to-ts/blob/master/src/schemaPostgres.ts
declare type: string
declare typeDb: string
declare isPrimary: boolean
declare isNullable: boolean
declare isDateTime: boolean
Expand All @@ -18,6 +20,8 @@ export default class ModelColumn {

constructor(info: Column) {
this.name = string.camelCase(info.name)
this.type = extractColumnTypeScriptType(info.data_type)
this.typeDb = info.data_type
this.columnName = info.name
this.tableName = info.table
this.isPrimary = info.is_primary_key
Expand All @@ -31,6 +35,26 @@ export default class ModelColumn {
return this.columnName.endsWith('_id')
}

getDecorator() {
if (this.isPrimary) {
return '@column({ isPrimary: true })'
}

if (this.type === 'DateTime' && this.name === 'createdAt') {
return '@column.dateTime({ autoCreate: true })'
}

if (this.type === 'DateTime' && this.name === 'updatedAt') {
return '@column.dateTime({ autoCreate: true, autoUpdate: true })'
}

if (this.type === 'DateTime') {
return '@column.dateTime()'
}

return '@column()'
}

getRelationship(tables: Model[]) {
if (!this.isIdColumn || !this.foreignKeyColumn) return

Expand Down
13 changes: 7 additions & 6 deletions src/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ModelRelationship from './relationship.js'

export default class Model {
declare name: string
declare fileName: string
declare tableName: string
declare columns: ModelColumn[]
declare relationships: ModelRelationship[]
Expand All @@ -13,24 +14,24 @@ export default class Model {

constructor(name: string, columns: ModelColumn[]) {
this.name = generators.modelName(name)
this.fileName = generators.modelFileName(name)
this.tableName = name
this.columns = columns
}

setPivotTable(isPivotTable: boolean) {
this.isPivotTable = isPivotTable
}

static build(tables: TableSchema[]) {
const models = this.#getModelsFromTables(tables)
const relationships = ModelRelationship.parse(models)

for (let model of models) {
const ships = relationships.get(model.name)
model.relationships = [...(ships?.values() || [])]
const values = [...(ships?.values() || [])]

model.relationships = values
model.isPivotTable = values.filter((relation) => relation.isManyToMany)?.length >= 2
}

return models
return models.filter((model) => !model.isPivotTable)
}

static #getModelsFromTables(tables: TableSchema[]) {
Expand Down
23 changes: 21 additions & 2 deletions src/model/relationship.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type ModelRelationshipInfo = {
modelColumn: string
tableName: string
tableColumn: string
relatedModelName: string
}

export default class ModelRelationship {
Expand All @@ -34,6 +35,7 @@ export default class ModelRelationship {
tableColumn: parentColumn!,
modelName: parentModel!.name,
modelColumn: this.#getModelColumn(parentModel!, parentColumn!),
relatedModelName: childModel!.name
}

this.child = {
Expand All @@ -42,11 +44,18 @@ export default class ModelRelationship {
tableColumn: childColumn!,
modelName: childModel!.name,
modelColumn: this.#getModelColumn(childModel!, childColumn!),
relatedModelName: parentModel!.name
}

this.#setTypes(type, column, models)
}

get isManyToMany() {
const isParent = this.parent.type === RelationshipTypes.MANY_TO_MANY
const isChild = this.child.type === RelationshipTypes.MANY_TO_MANY
return isParent && isChild
}

getDefinitions(modelName: string) {
const definitions = []

Expand All @@ -62,9 +71,19 @@ export default class ModelRelationship {
}

#getDefinition(info: ModelRelationshipInfo) {
let propertyName = string.camelCase(info.relatedModelName)

switch (info.type) {
case RelationshipTypes.HAS_MANY:
case RelationshipTypes.MANY_TO_MANY:
propertyName = string.plural(propertyName)
break
}

return {
decorator: `@${info.type}(() => ${info.modelName})`,
property: `declare ${info.modelColumn}: ${string.capitalCase(info.type)}<typeof ${info.modelName}>`,
type: info.type,
decorator: `@${info.type}(() => ${info.relatedModelName})`,
property: `declare ${propertyName}: ${string.pascalCase(info.type)}<typeof ${info.relatedModelName}>`,
}
}

Expand Down
19 changes: 19 additions & 0 deletions stubs/generate/model.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'

export default class {{ model.name }} extends BaseModel {
{{ #each model.columns as column }}
{{ column.getDecorator() }}
declare {{ column.name }}: {{ column.type }}
{{ /each }}
{{ #each relationships as relationship }}
{{ relationship.decorator }}
{{{ relationship.property }}}
{{ /each }}
}

{{{
exports({
to: app.modelsPath(model.fileName)
})
}}}

0 comments on commit 49538ec

Please sign in to comment.