Skip to content

Commit

Permalink
Add belongsTo/hasOne/hasMany to initModels (#34) (#61) (#65) (#82) (#215
Browse files Browse the repository at this point in the history
) (#369)

Add precision to DECIMAL, DOUBLE, and FLOAT types
Add array types to TypeScript definitions
  • Loading branch information
steveschmitt committed Nov 3, 2020
1 parent 55c36b1 commit 1f852bd
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 30 deletions.
2 changes: 1 addition & 1 deletion sample/config.js
@@ -1,6 +1,6 @@
const path = require('path');
const output = path.join(__dirname, "./models");
const options = { directory: output, caseFile: 'l', caseModel: 'p', caseProp: 'c', typescript: true, spaces: true, indentation: 2 };
const options = { directory: output, caseFile: 'l', caseModel: 'p', caseProp: 'c', lang: 'ts', spaces: true, indentation: 2 };

// Edit the configuration below for your database dialect

Expand Down
2 changes: 1 addition & 1 deletion sample/sample-es5.js
Expand Up @@ -13,7 +13,7 @@ var Customer = models.Customer;
var Order = models.Order;

// get a customer using known values in the sample data
return Customer.findOne({ where: { "firstName": "Hanna", "lastName": "Moos" }}).then(cust => {
return Customer.findOne({ where: { "firstName": "Hanna", "lastName": "Moos" }, include: [Order] }).then(cust => {
console.log(cust);
if (cust == null) {
return;
Expand Down
4 changes: 2 additions & 2 deletions sample/sample-ts.ts
Expand Up @@ -18,7 +18,7 @@ class SampleApp {
// });

// get a customer using known values in the sample data
const cust = await Customer.findOne({ where: { "firstName": "Hanna", "lastName": "Moos" } });
const cust = await Customer.findOne({ where: { "firstName": "Hanna", "lastName": "Moos" }, include: [Order as any] });
console.log(cust);
if (cust == null) {
return;
Expand All @@ -36,7 +36,7 @@ class SampleApp {
totalAmount: 223.45
};

Order.create(attr).then(order => {
Order.create(attr).then(() => {
// display list of orders
Order.findAll({ where: { "customerId": cust.id } }).then(rows => {
rows.forEach(r => console.log(r.orderNumber + " " + r.totalAmount));
Expand Down
34 changes: 19 additions & 15 deletions src/auto-generator.ts
Expand Up @@ -401,24 +401,23 @@ export class AutoGenerator {
}
const type: string = attrValue.toLowerCase();
const length = type.match(/\(\d+\)/);
const precision = type.match(/\(\d+,\d+\)/);
let val = null;
let typematch = null;

if (type === "boolean" || type === "bit(1)" || type === "bit") {
val = 'DataTypes.BOOLEAN';
} else if (type.match(/^(smallint|mediumint|tinyint|int)/)) {
val = 'DataTypes.INTEGER' + (!_.isNull(length) ? length : '');
} else if (typematch = type.match(/^(bigint|smallint|mediumint|tinyint|int)/)) {
// integer subtypes
val = 'DataTypes.' + (typematch[0] === 'int' ? 'INTEGER' : typematch[0].toUpperCase());
if (/unsigned/i.test(type)) {
val += '.UNSIGNED';
}
if (/zerofill/i.test(type)) {
val += '.ZEROFILL';
}
} else if (type.match(/^bigint/)) {
val = 'DataTypes.BIGINT';
} else if (type.match(/^n?varchar/)) {
} else if (type.match(/n?varchar|string|varying/)) {
val = 'DataTypes.STRING' + (!_.isNull(length) ? length : '');
} else if (type.match(/^string|varying|nvarchar/)) {
val = 'DataTypes.STRING';
} else if (type.match(/^n?char/)) {
val = 'DataTypes.CHAR' + (!_.isNull(length) ? length : '');
} else if (type.match(/^real/)) {
Expand All @@ -430,17 +429,17 @@ export class AutoGenerator {
} else if (type.match(/^(date|timestamp)/)) {
val = 'DataTypes.DATE' + (!_.isNull(length) ? length : '');
} else if (type.match(/^(time)/)) {
val = 'DataTypes.TIME'; // + (!_.isNull(length) ? length : '');
val = 'DataTypes.TIME';
} else if (type.match(/^(float|float4)/)) {
val = 'DataTypes.FLOAT';
val = 'DataTypes.FLOAT' + (!_.isNull(precision) ? precision : '');
} else if (type.match(/^decimal/)) {
val = 'DataTypes.DECIMAL';
val = 'DataTypes.DECIMAL' + (!_.isNull(precision) ? precision : '');
} else if (type.match(/^money/)) {
val = 'DataTypes.DECIMAL(19,4)';
} else if (type.match(/^smallmoney/)) {
val = 'DataTypes.DECIMAL(10,4)';
} else if (type.match(/^(float8|double|numeric)/)) {
val = 'DataTypes.DOUBLE';
val = 'DataTypes.DOUBLE' + (!_.isNull(precision) ? precision : '');
} else if (type.match(/^uuid|uniqueidentifier/)) {
val = 'DataTypes.UUID';
} else if (type.match(/^jsonb/)) {
Expand All @@ -454,7 +453,7 @@ export class AutoGenerator {
} else if (type.match(/^array/)) {
const eltype = this.getSqType(fieldObj, "special");
val = `DataTypes.ARRAY(${eltype})`;
} else if (type.match(/^(varbinary|image)/)) {
} else if (type.match(/(binary|image|blob)/)) {
val = 'DataTypes.BLOB';
} else if (type.match(/^hstore/)) {
val = 'DataTypes.HSTORE';
Expand All @@ -475,7 +474,11 @@ export class AutoGenerator {

private getTypeScriptType(table: string, field: string) {
const fieldObj = this.tables[table][field];
const fieldType = (fieldObj["type"] || '').toLowerCase();
return this.getTypeScriptFieldType(fieldObj, "type");
}

private getTypeScriptFieldType(fieldObj: any, attr: string) {
const fieldType = (fieldObj[attr] || '').toLowerCase();
let jsType: string;
if (this.isString(fieldType)) {
jsType = 'string';
Expand All @@ -486,9 +489,10 @@ export class AutoGenerator {
} else if (this.isDate(fieldType)) {
jsType = 'Date';
} else if (this.isArray(fieldType)) {
jsType = 'any[]';
const eltype = this.getTypeScriptFieldType(fieldObj, "special");
jsType = eltype + '[]';
} else {
console.log(`Missing type: ${fieldType}`);
console.log(`Missing TypeScript type: ${fieldType}`);
jsType = 'any';
}
return jsType;
Expand Down
53 changes: 47 additions & 6 deletions src/auto-writer.ts
Expand Up @@ -2,20 +2,24 @@ import fs from "fs";
import _ from "lodash";
import path from "path";
import util from "util";
import { FKSpec, TableData } from ".";
import { AutoOptions, CaseOption, LangOption, qNameSplit, recase } from "./types";
const mkdirp = require('mkdirp');

export class AutoWriter {
tableText: { [name: string]: string };
foreignKeys: { [tableName: string]: { [fieldName: string]: FKSpec } };
options: {
caseFile?: CaseOption;
caseModel?: CaseOption;
caseProp?: CaseOption;
directory: string;
lang?: LangOption;
noWrite?: boolean;
};
constructor(tableText: { [name: string]: string }, options: AutoOptions) {
this.tableText = tableText;
constructor(tableData: TableData, options: AutoOptions) {
this.tableText = tableData.text as { [name: string]: string };
this.foreignKeys = tableData.foreignKeys;
this.options = options;
}

Expand All @@ -34,16 +38,18 @@ export class AutoWriter {
return this.createFile(t);
});

const assoc = this.createAssociations();

// get table names without schema
// TODO: add schema to model and file names when schema is non-default for the dialect
const tableNames = tables.map(t => {
const [schemaName, tableName] = qNameSplit(t);
return tableName as string;
});
}).sort();

// write the init-models file
const ists = this.options.lang === 'ts';
const initString = ists ? this.createTsInitString(tableNames) : this.createES5InitString(tableNames);
const initString = ists ? this.createTsInitString(tableNames, assoc) : this.createES5InitString(tableNames, assoc);
const initFilePath = path.join(this.options.directory, "init-models" + (ists ? '.ts' : '.js'));
const writeFile = util.promisify(fs.writeFile);
const initPromise = writeFile(path.resolve(initFilePath), initString);
Expand All @@ -64,8 +70,37 @@ export class AutoWriter {
return writeFile(path.resolve(filePath), this.tableText[table]);
}

/** Create the belongsTo/hasMany/hasOne association strings */
private createAssociations() {
let str = "";
const fkTables = _.keys(this.foreignKeys).sort();
fkTables.forEach(t => {
const [schemaName, tableName] = qNameSplit(t);
const modelName = recase(this.options.caseModel, tableName);
const fkFields = this.foreignKeys[t];
const fkFieldNames = _.keys(fkFields);
fkFieldNames.forEach(fkFieldName => {
const spec = fkFields[fkFieldName];
if (spec.isForeignKey) {
const targetModel = recase(this.options.caseModel, spec.foreignSources.target_table as string);
const targetProp = recase(this.options.caseProp, spec.foreignSources.target_column as string);
const sourceProp = recase(this.options.caseProp, fkFieldName);

str += ` ${modelName}.belongsTo(${targetModel}, { foreignKey: "${targetProp}"});\n`;

// use "hasOne" cardinality if this FK is also a single-column Primary or Unique key; else "hasMany"
const isOne = ((spec.isPrimaryKey && !_.some(fkFields, f => f.isPrimaryKey && f.source_column !== fkFieldName) ||
(spec.isUnique && !_.some(fkFields, f => f.isUnique === spec.isUnique && f.source_column !== fkFieldName))));
const hasRel = isOne ? "hasOne" : "hasMany";
str += ` ${targetModel}.${hasRel}(${modelName}, { foreignKey: "${sourceProp}"});\n`;
}
});
});
return str;
}

// create the TypeScript init-models file to load all the models into Sequelize
private createTsInitString(tables: string[]) {
private createTsInitString(tables: string[], assoc: string) {
let str = 'import { Sequelize } from "sequelize";\n';
const modelNames: string[] = [];
// import statements
Expand All @@ -88,6 +123,9 @@ export class AutoWriter {
str += ` ${m}.initModel(sequelize);\n`;
});

// add the asociations
str += "\n" + assoc;

// return the models
str += "\n return {\n";
modelNames.forEach(m => {
Expand All @@ -100,7 +138,7 @@ export class AutoWriter {
}

// create the ES5 init-models file to load all the models into Sequelize
private createES5InitString(tables: string[]) {
private createES5InitString(tables: string[], assoc: string) {
let str = 'var DataTypes = require("sequelize").DataTypes;\n';
const modelNames: string[] = [];
// import statements
Expand All @@ -117,6 +155,9 @@ export class AutoWriter {
str += ` var ${m} = _${m}(sequelize, DataTypes);\n`;
});

// add the asociations
str += "\n" + assoc;

// return the models
str += "\n return {\n";
modelNames.forEach(m => {
Expand Down
6 changes: 3 additions & 3 deletions src/auto.ts
Expand Up @@ -46,8 +46,8 @@ export class SequelizeAuto {
async run(): Promise<TableData> {
const td = await this.build();
const tt = this.generate(td);
await this.write(tt);
td.text = tt;
await this.write(td);
return td;
}

Expand All @@ -67,8 +67,8 @@ export class SequelizeAuto {
return generator.generateText();
}

write(tableText: { [name: string]: string }) {
const writer = new AutoWriter(tableText, this.options);
write(tableData: TableData) {
const writer = new AutoWriter(tableData, this.options);
return writer.write();
}

Expand Down
9 changes: 8 additions & 1 deletion src/dialects/dialect-options.ts
Expand Up @@ -45,7 +45,14 @@ export interface FKSpec extends FKRelation {
isSerialKey: boolean;
isPrimaryKey: boolean;
isUnique: boolean | string;
foreignSources: { [source: string]: any };
foreignSources: {
source_table?: string;
source_schema?: string;
source_column?: string;
target_table?: string;
target_schema?: string;
target_column?: string;
};
}

export interface ColumnElementType {
Expand Down
2 changes: 1 addition & 1 deletion src/dialects/mysql.ts
Expand Up @@ -59,7 +59,7 @@ export const mysqlOptions: DialectOptions = {
if (!_.isObject(record) || !_.has(record, 'column_key')) {
return false;
}
return records.some(row => row.constraint_name === record.constraint_name && (row.column_key.toUpperCase() === 'UNI' || row.column_key.toUpperCase() === 'MUL'));
return records.some(row => row.constraint_name === record.constraint_name && (row.column_key.toUpperCase() === 'UNI'));
},

/**
Expand Down

0 comments on commit 1f852bd

Please sign in to comment.