Skip to content

Commit

Permalink
feat: add mysql parser
Browse files Browse the repository at this point in the history
  • Loading branch information
dziraf committed Jun 5, 2023
1 parent 7ff6193 commit 1c4c4be
Show file tree
Hide file tree
Showing 9 changed files with 335 additions and 19 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"dependencies": {
"knex": "^2.4.2",
"mysql2": "^3.3.3",
"pg": "^8.10.0"
},
"peerDependencies": {
Expand Down
8 changes: 7 additions & 1 deletion src/Property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type ColumnInfo = {
isEditable: boolean;
type: PropertyType;
referencedTable: string | null;
availableValues?: string[] | null;
}

export class Property extends BaseProperty {
Expand All @@ -22,6 +23,8 @@ export class Property extends BaseProperty {

private readonly _name: string;

private readonly _availableValues?: string[] | null;

constructor(column: ColumnInfo) {
const {
name,
Expand All @@ -31,6 +34,7 @@ export class Property extends BaseProperty {
isEditable,
type,
referencedTable,
availableValues,
} = column;

super({
Expand All @@ -45,6 +49,7 @@ export class Property extends BaseProperty {
this._isNullable = isNullable;
this._isEditable = isEditable;
this._referencedTable = referencedTable;
this._availableValues = availableValues;
}

override isId(): boolean {
Expand All @@ -69,7 +74,8 @@ export class Property extends BaseProperty {

// eslint-disable-next-line class-methods-use-this
override availableValues(): Array<string> | null {
// Currently "availableValues" have to be set explicitly via resource options
if (this._availableValues) return this._availableValues;

return null;
}

Expand Down
25 changes: 17 additions & 8 deletions src/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class Resource extends BaseResource {

private tableName: string;

private schemaName = 'public';
private schemaName: string | null;

private _database: string;

Expand Down Expand Up @@ -100,12 +100,14 @@ export class Resource extends BaseResource {
}

override async findOne(id: string): Promise<BaseRecord | null> {
const res = await this.knex(this.tableName).withSchema(this.schemaName).where(this.idColumn, id);
const knex = this.schemaName ? this.knex(this.tableName).withSchema(this.schemaName) : this.knex(this.tableName);
const res = await knex.where(this.idColumn, id);
return res[0] ? this.build(res[0]) : null;
}

override async findMany(ids: (string | number)[]): Promise<BaseRecord[]> {
const res = await this.knex(this.tableName).withSchema(this.schemaName).whereIn(this.idColumn, ids);
const knex = this.schemaName ? this.knex(this.tableName).withSchema(this.schemaName) : this.knex(this.tableName);
const res = await knex.whereIn(this.idColumn, ids);
return res.map((r) => this.build(r));
}

Expand All @@ -114,7 +116,8 @@ export class Resource extends BaseResource {
}

override async create(params: Record<string, any>): Promise<ParamsType> {
await this.knex(this.tableName).withSchema(this.schemaName).insert(params);
const knex = this.schemaName ? this.knex(this.tableName).withSchema(this.schemaName) : this.knex(this.tableName);
await knex.insert(params);

return params;
}
Expand All @@ -123,20 +126,26 @@ export class Resource extends BaseResource {
id: string,
params: Record<string, any>,
): Promise<ParamsType> {
await this.knex
const knex = this.schemaName ? this.knex.withSchema(this.schemaName) : this.knex;

await knex
.from(this.tableName)
.update(params)
.where(this.idColumn, id);
const [row] = await this.knex(this.tableName).withSchema(this.schemaName).where(this.idColumn, id);

const knexQb = this.schemaName ? this.knex(this.tableName).withSchema(this.schemaName) : this.knex(this.tableName);
const [row] = await knexQb.where(this.idColumn, id);
return row;
}

override async delete(id: string): Promise<void> {
await this.knex.withSchema(this.schemaName).from(this.tableName).delete().where(this.idColumn, id);
const knex = this.schemaName ? this.knex.withSchema(this.schemaName) : this.knex;
await knex.from(this.tableName).delete().where(this.idColumn, id);
}

private filterQuery(filter: Filter | undefined): Knex.QueryBuilder {
const q = this.knex(this.tableName).withSchema(this.schemaName);
const knex = this.schemaName ? this.knex(this.tableName).withSchema(this.schemaName) : this.knex(this.tableName);
const q = knex;

if (!filter) {
return q;
Expand Down
4 changes: 1 addition & 3 deletions src/dialects/base-database.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import KnexModule from 'knex';

import { DatabaseMetadata, ResourceMetadata } from '../metadata/index.js';

import { ConnectionOptions } from './types/index.js';

import { DatabaseDialect } from './index.js';
import { ConnectionOptions, DatabaseDialect } from './types/index.js';

const KnexConnection = KnexModule.knex;

Expand Down
7 changes: 3 additions & 4 deletions src/dialects/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { BaseDatabaseParser } from './base-database.parser.js';
import { MysqlParser } from './mysql.parser.js';
import { PostgresParser } from './postgres.parser.js';
import { ConnectionOptions } from './types/index.js';
import { ConnectionOptions, DatabaseDialect } from './types/index.js';

export * from './types/index.js';

export type DatabaseDialect = 'postgresql'

const parsers: (typeof BaseDatabaseParser)[] = [PostgresParser];
const parsers: (typeof BaseDatabaseParser)[] = [PostgresParser, MysqlParser];

export function parse(dialect: DatabaseDialect, connection: ConnectionOptions) {
const Parser = parsers.find((p) => p.dialects.includes(dialect));
Expand Down
201 changes: 201 additions & 0 deletions src/dialects/mysql.parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* MySQL parser originally authored by https://github.com/wirekang
* Source: https://github.com/wirekang/adminjs-sql/blob/main/src/parser/mysql.ts
*/
import { PropertyType } from 'adminjs';

import { DatabaseMetadata, ResourceMetadata } from '../metadata/index.js';
import { ColumnInfo, Property } from '../Property.js';

import { BaseDatabaseParser } from './base-database.parser.js';

const getColumnInfo = (column: Record<string, any>): ColumnInfo => {
const type = column.DATA_TYPE.toLowerCase();
const columnType = column.COLUMN_TYPE.toLowerCase();

let availableValues: string[] | null = null;
if (type === 'set' || type === 'enum') {
if (!columnType.startsWith(type)) {
throw new Error(`Unknown column type: ${type}`);
}
availableValues = columnType
.split(type)[1]
.replace(/^\('/, '')
.replace(/'\)$/, '')
.split('\',\'');
}
const reference = column.REFERENCED_TABLE_NAME;
const isId = column.COLUMN_KEY.toLowerCase() === 'pri';
const isNullable = column.IS_NULLABLE.toLowerCase() !== 'no';

return {
name: column.COLUMN_NAME,
isId,
position: column.ORDINAL_POSITION,
defaultValue: column.COLUMN_DEFAULT,
isNullable,
isEditable: !isId,
type: reference ? 'reference' : ensureType(type, columnType),
referencedTable: reference ?? null,
availableValues,
};
};

const ensureType = (dataType: string, columnType: string): PropertyType => {
switch (dataType) {
case 'char':
case 'varchar':
case 'binary':
case 'varbinary':
case 'tinyblob':
case 'blob':
case 'mediumblob':
case 'longblob':
case 'enum':
case 'set':
case 'time':
case 'year':
return 'string';

case 'tinytext':
case 'text':
case 'mediumtext':
case 'longtext':
return 'textarea';

case 'bit':
case 'smallint':
case 'mediumint':
case 'int':
case 'integer':
case 'bigint':
return 'number';

case 'float':
case 'double':
case 'decimal':
case 'dec':
return 'float';

case 'tinyint':
if (columnType === 'tinyint(1)') {
return 'boolean';
}
return 'number';

case 'bool':
case 'boolean':
return 'boolean';

case 'date':
return 'date';

case 'datetime':
case 'timestamp':
return 'datetime';

default:
// eslint-disable-next-line no-console
console.warn(
`Unexpected type: ${dataType} ${columnType} fallback to string`,
);
return 'string';
}
};

export class MysqlParser extends BaseDatabaseParser {
public static dialects = ['mysql' as const, 'mysql2' as const];

public async parse() {
const tableNames = await this.getTables();
const resources = await this.getResources(tableNames);
const resourceMap = new Map<string, ResourceMetadata>();
resources.forEach((r) => {
resourceMap.set(r.tableName, r);
});

return new DatabaseMetadata(this.connectionOptions.database, resourceMap);
}

public async getTables() {
const query = await this.knex.raw(`
SHOW FULL TABLES FROM \`${this.connectionOptions.database}\` WHERE Table_type = 'BASE TABLE'
`);

const result = await query;
const tables = result?.[0];

if (!tables?.length) {
// eslint-disable-next-line no-console
console.warn(`No tables in database ${this.connectionOptions.database}`);

return [];
}

return tables.reduce((memo, info) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars, camelcase
const { Table_type, ...nameInfo } = info;

const tableName = Object.values(nameInfo ?? {})[0];

memo.push(tableName);

return memo;
}, []);
}

public async getResources(tables: string[]) {
const resources = await Promise.all(
tables.map(async (tableName) => {
try {
const resourceMetadata = new ResourceMetadata(
this.dialect,
this.knex,
this.connectionOptions.database,
null,
tableName,
await this.getProperties(tableName),
);

return resourceMetadata;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);

return false;
}
}),
);

return resources.filter(Boolean) as ResourceMetadata[];
}

public async getProperties(table: string) {
const query = this.knex
.from('information_schema.columns as col')
.select(
'col.column_name',
'col.ordinal_position',
'col.column_default',
'col.is_nullable',
'col.data_type',
'col.column_type',
'col.column_key',
'col.extra',
'col.column_comment',
'key.referenced_table_name',
'key.referenced_column_name',
)
.leftJoin('information_schema.key_column_usage as key', (c) => c
.on('key.table_schema', 'col.table_schema')
.on('key.table_name', 'col.table_name')
.on('key.column_name', 'col.column_name')
.on('key.referenced_table_schema', 'col.table_schema'))
.where('col.table_schema', this.connectionOptions.database)
.where('col.table_name', table);

const columns = await query;

return columns.map((col) => new Property(getColumnInfo(col)));
}
}
Loading

0 comments on commit 1c4c4be

Please sign in to comment.