Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sequelize-typescript-generator",
"version": "11.0.8",
"name": "@jsindos/sequelize-typescript-generator",
"version": "11.1.1",
"description": "Automatically generates typescript models compatible with sequelize-typescript library (https://www.npmjs.com/package/sequelize-typescript) directly from your source database.",
"main": "build/index.js",
"types": "build/index.d.ts",
Expand Down
196 changes: 171 additions & 25 deletions src/builders/ModelBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { pascalCase } from 'change-case';
import { promises as fs } from 'fs';
import path from 'path';
import * as ts from 'typescript';
Expand Down Expand Up @@ -81,7 +82,7 @@ export class ModelBuilder extends Builder {
)
],
col.name,
(col.autoIncrement || col.allowNull || col.defaultValue !== undefined) ?
(col.autoIncrement || col.allowNull) ?
ts.factory.createToken(ts.SyntaxKind.QuestionToken) : ts.factory.createToken(ts.SyntaxKind.ExclamationToken),
ts.factory.createTypeReferenceNode(dialect.mapDbTypeToJs(col.type) ?? 'any', undefined),
undefined,
Expand All @@ -92,35 +93,140 @@ export class ModelBuilder extends Builder {
* Build association class member
* @param {IAssociationMetadata} association
*/
private static buildAssociationPropertyDecl(association: IAssociationMetadata): ts.PropertyDeclaration {
const { associationName, targetModel, joinModel } = association;

private static buildAssociationPropertyDecl(association: IAssociationMetadata, tablesMetadata: ITablesMetadata): ts.PropertyDeclaration[] {
const { associationName, targetModel, joinModel, alias } = association;
const targetModels = [ targetModel ];
joinModel && targetModels.push(joinModel);

return ts.factory.createPropertyDeclaration(
[
...(association.sourceKey ?
[
generateArrowDecorator(
associationName,
targetModels,
{ sourceKey: association.sourceKey }
)
]
: [
generateArrowDecorator(associationName, targetModels)
]
),
],
associationName.includes('Many') ?
pluralize.plural(targetModel) : pluralize.singular(targetModel),

// Use alias if provided, otherwise use target model name
const nameBase = alias || targetModel;
const propertyName = associationName.includes('Many') ?
pluralize.plural(nameBase) : pluralize.singular(nameBase);

let decorator;
if (associationName === 'BelongsToMany') {
// For BelongsToMany, don't pass alias in decorator options
// The alias will be handled by the property name
decorator = generateArrowDecorator(associationName, targetModels);
} else {
// For other associations, pass options normally
const options = {
...(association.sourceKey && { sourceKey: association.sourceKey }),
...(alias && { as: alias })
};
decorator = generateArrowDecorator(
associationName,
targetModels,
Object.keys(options).length > 0 ? options : undefined
);
}

const mainProperty = ts.factory.createPropertyDeclaration(
[decorator],
propertyName,
ts.factory.createToken(ts.SyntaxKind.QuestionToken),
associationName.includes('Many') ?
ts.factory.createArrayTypeNode(ts.factory.createTypeReferenceNode(targetModel, undefined)) :
ts.factory.createTypeReferenceNode(targetModel, undefined),
undefined,
);

const mixinDeclarations = this.generateAssociationMixins(association, tablesMetadata);

return [mainProperty, ...mixinDeclarations];
}

private static createMixinDeclaration(name: string, type: string): ts.PropertyDeclaration {
return ts.factory.createPropertyDeclaration(
[ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
name,
undefined,
ts.factory.createTypeReferenceNode(type, undefined),
undefined
);
}

private static generateAssociationMixins(association: IAssociationMetadata, tablesMetadata: ITablesMetadata): ts.PropertyDeclaration[] {
const { associationName, targetModel, alias } = association;

// Use alias if provided, otherwise use target model name
const nameBase = alias || targetModel;
const singularTarget = pluralize.singular(nameBase);
const pluralTarget = pluralize.plural(nameBase);

// Get the primary key type from the target table's metadata
const targetTable = tablesMetadata[targetModel];
const primaryKeyColumn = Object.values(targetTable.columns).find(col => col.primaryKey);
const primaryKeyType = primaryKeyColumn ?
this.getPrimaryKeyType(primaryKeyColumn.type) :
'number'; // fallback to number if not found

switch (associationName) {
case 'HasMany':
return [
this.createMixinDeclaration(`get${pascalCase(pluralTarget)}`, `HasManyGetAssociationsMixin<${targetModel}>`),
this.createMixinDeclaration(`add${pascalCase(singularTarget)}`, `HasManyAddAssociationMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`add${pascalCase(pluralTarget)}`, `HasManyAddAssociationsMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`set${pascalCase(pluralTarget)}`, `HasManySetAssociationsMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`remove${pascalCase(singularTarget)}`, `HasManyRemoveAssociationMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`remove${pascalCase(pluralTarget)}`, `HasManyRemoveAssociationsMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`has${pascalCase(singularTarget)}`, `HasManyHasAssociationMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`has${pascalCase(pluralTarget)}`, `HasManyHasAssociationsMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`count${pascalCase(pluralTarget)}`, 'HasManyCountAssociationsMixin'),
this.createMixinDeclaration(`create${pascalCase(singularTarget)}`, `HasManyCreateAssociationMixin<${targetModel}>`)
];

case 'HasOne':
return [
this.createMixinDeclaration(`get${pascalCase(singularTarget)}`, `HasOneGetAssociationMixin<${targetModel}>`),
this.createMixinDeclaration(`set${pascalCase(singularTarget)}`, `HasOneSetAssociationMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`create${pascalCase(singularTarget)}`, `HasOneCreateAssociationMixin<${targetModel}>`),
];

case 'BelongsTo':
return [
this.createMixinDeclaration(`get${pascalCase(singularTarget)}`, `BelongsToGetAssociationMixin<${targetModel}>`),
this.createMixinDeclaration(`set${pascalCase(singularTarget)}`, `BelongsToSetAssociationMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`create${pascalCase(singularTarget)}`, `BelongsToCreateAssociationMixin<${targetModel}>`),
];

case 'BelongsToMany':
return [
this.createMixinDeclaration(`get${pascalCase(pluralTarget)}`, `BelongsToManyGetAssociationsMixin<${targetModel}>`),
this.createMixinDeclaration(`set${pascalCase(pluralTarget)}`, `BelongsToManySetAssociationsMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`add${pascalCase(singularTarget)}`, `BelongsToManyAddAssociationMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`add${pascalCase(pluralTarget)}`, `BelongsToManyAddAssociationsMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`create${pascalCase(singularTarget)}`, `BelongsToManyCreateAssociationMixin<${targetModel}>`),
this.createMixinDeclaration(`remove${pascalCase(singularTarget)}`, `BelongsToManyRemoveAssociationMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`remove${pascalCase(pluralTarget)}`, `BelongsToManyRemoveAssociationsMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`has${pascalCase(singularTarget)}`, `BelongsToManyHasAssociationMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`has${pascalCase(pluralTarget)}`, `BelongsToManyHasAssociationsMixin<${targetModel}, ${primaryKeyType}>`),
this.createMixinDeclaration(`count${pascalCase(pluralTarget)}`, 'BelongsToManyCountAssociationsMixin')
];

default:
return [];
}
}

private static getPrimaryKeyType(dbType: string): string {
// Map database types to TypeScript types
switch (dbType.toLowerCase()) {
case 'uuid':
return 'string';
case 'varchar':
case 'char':
case 'text':
return 'string';
case 'int':
case 'integer':
case 'smallint':
case 'bigint':
return 'number';
default:
return 'number'; // default fallback
}
}

/**
Expand All @@ -131,6 +237,7 @@ export class ModelBuilder extends Builder {
*/
private static buildTableClassDeclaration(
tableMetadata: ITableMetadata,
tablesMetadata: ITablesMetadata,
dialect: Dialect,
strict: boolean = true
): string {
Expand All @@ -155,6 +262,45 @@ export class ModelBuilder extends Builder {

generatedCode += '\n';

// Import mixin types from sequelize
generatedCode += nodeToString(generateNamedImports(
[
// HasMany mixins
'HasManyGetAssociationsMixin',
'HasManyAddAssociationMixin',
'HasManyAddAssociationsMixin',
'HasManySetAssociationsMixin',
'HasManyRemoveAssociationMixin',
'HasManyRemoveAssociationsMixin',
'HasManyHasAssociationMixin',
'HasManyHasAssociationsMixin',
'HasManyCountAssociationsMixin',
'HasManyCreateAssociationMixin',
// HasOne mixins
'HasOneGetAssociationMixin',
'HasOneSetAssociationMixin',
'HasOneCreateAssociationMixin',
// BelongsTo mixins
'BelongsToGetAssociationMixin',
'BelongsToSetAssociationMixin',
'BelongsToCreateAssociationMixin',
// BelongsToMany mixins
'BelongsToManyGetAssociationsMixin',
'BelongsToManySetAssociationsMixin',
'BelongsToManyAddAssociationMixin',
'BelongsToManyAddAssociationsMixin',
'BelongsToManyCreateAssociationMixin',
'BelongsToManyRemoveAssociationMixin',
'BelongsToManyRemoveAssociationsMixin',
'BelongsToManyHasAssociationMixin',
'BelongsToManyHasAssociationsMixin',
'BelongsToManyCountAssociationsMixin'
],
'sequelize'
));

generatedCode += '\n';

// Named imports for associations
const importModels = new Set<string>();

Expand Down Expand Up @@ -194,7 +340,7 @@ export class ModelBuilder extends Builder {
...(Object.values(columns).map(c => ts.factory.createPropertySignature(
undefined,
ts.factory.createIdentifier(c.name),
c.autoIncrement || c.allowNull || c.defaultValue !== undefined ?
c.name === 'id' || c.autoIncrement || c.allowNull || c.defaultValue !== undefined ?
ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
ts.factory.createTypeReferenceNode(dialect.mapDbTypeToJs(c.type) ?? 'any', undefined)
)))
Expand Down Expand Up @@ -262,7 +408,7 @@ export class ModelBuilder extends Builder {
[
...Object.values(columns).map(col => this.buildColumnPropertyDecl(col, dialect)),
...tableMetadata.associations && tableMetadata.associations.length ?
tableMetadata.associations.map(a => this.buildAssociationPropertyDecl(a)) : []
tableMetadata.associations.flatMap(a => this.buildAssociationPropertyDecl(a, tablesMetadata)) : []
]
);

Expand Down Expand Up @@ -329,7 +475,7 @@ export class ModelBuilder extends Builder {
for (const tableMetadata of Object.values(tablesMetadata)) {
console.log(`Processing table ${tableMetadata.originName}`);
const tableClassDecl =
ModelBuilder.buildTableClassDeclaration(tableMetadata, this.dialect, this.config.strict);
ModelBuilder.buildTableClassDeclaration(tableMetadata, tablesMetadata, this.dialect, this.config.strict);

writePromises.push((async () => {
const outPath = path.join(outDir, `${tableMetadata.name}.ts`);
Expand Down
30 changes: 27 additions & 3 deletions src/dialects/AssociationsParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ type AssociationRow = [
string, // right key
string, // left table
string, // right table
string? // [join table]
string?, // [join table]
string?, // [left alias] - alias used by leftModel when referring to rightModel
string? // [right alias] - alias used by rightModel when referring to leftModel
];

export interface IAssociationMetadata {
associationName: 'HasOne' | 'HasMany' | 'BelongsTo' | 'BelongsToMany';
targetModel: string;
joinModel?: string;
sourceKey?: string; // Left table key for HasOne and HasMany associations
alias?: string; // Optional alias for the association
}

export interface IForeignKey {
Expand All @@ -42,7 +45,9 @@ const validateRow = (row: AssociationRow): void => {
rightKey,
leftTable,
rightTable,
joinTable
joinTable,
leftAlias,
rightAlias
] = row;

if (!cardinalities.has(cardinality)) {
Expand All @@ -68,6 +73,15 @@ const validateRow = (row: AssociationRow): void => {
if (cardinality === 'N:N' && (!joinTable || !joinTable.length)) {
throw new Error(`Association N:N requires a joinTable in the association row`);
}

// Validate aliases are not empty strings if provided
if (leftAlias !== undefined && leftAlias.length === 0) {
throw new Error(`Left alias cannot be empty string. Use undefined or omit the field instead.`);
}

if (rightAlias !== undefined && rightAlias.length === 0) {
throw new Error(`Right alias cannot be empty string. Use undefined or omit the field instead.`);
}
}

/**
Expand Down Expand Up @@ -109,7 +123,9 @@ export class AssociationsParser {
rightKey,
leftModel,
rightModel,
joinModel
joinModel,
leftAlias,
rightAlias
] = row;

const [
Expand All @@ -135,15 +151,19 @@ export class AssociationsParser {

// 1:1 and 1:N association
if (cardinality !== 'N:N') {
// Left model association (HasOne/HasMany)
associationsMetadata[leftModel].associations.push({
associationName: rightCardinality === '1' ? 'HasOne' : 'HasMany',
targetModel: rightModel,
sourceKey: leftKey,
...(leftAlias && { alias: leftAlias })
});

// Right model association (BelongsTo)
associationsMetadata[rightModel].associations.push({
associationName: 'BelongsTo',
targetModel: leftModel,
...(rightAlias && { alias: rightAlias })
});

associationsMetadata[rightModel].foreignKeys.push({
Expand All @@ -161,16 +181,20 @@ export class AssociationsParser {
};
}

// Left model BelongsToMany association
associationsMetadata[leftModel].associations.push({
associationName: 'BelongsToMany',
targetModel: rightModel,
joinModel: joinModel,
...(leftAlias && { alias: leftAlias })
});

// Right model BelongsToMany association
associationsMetadata[rightModel].associations.push({
associationName: 'BelongsToMany',
targetModel: leftModel,
joinModel: joinModel,
...(rightAlias && { alias: rightAlias })
});

associationsMetadata[joinModel!].foreignKeys.push({
Expand Down
2 changes: 1 addition & 1 deletion src/dialects/DialectPostgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export class DialectPostgres extends Dialect {
this.mapDbTypeToSequelize(column.udt_name).key
.split(' ')[0], // avoids 'DOUBLE PRECISION' key to include PRECISION in the mapping
},
allowNull: !!column.is_nullable && !column.is_primary,
allowNull: column.is_nullable === 'YES' && !column.is_primary,
primaryKey: column.is_primary,
autoIncrement: column.is_sequence,
indices: [],
Expand Down