jsonschema2ddl
Version:
Convert JSON Schema to DDL
312 lines (280 loc) • 9.01 kB
text/typescript
import * as crypto from "crypto";
import { COLUMNS_TYPES, COLUMNS_TYPES_PREFERENCE, FK_TYPES } from "./types";
import { db_column_name, get_one_schema } from "./utils";
interface ColumnParams {
name: string;
database_flavor?: string;
comment?: string;
constraints?: any;
jsonschema_type?: string;
jsonschema_fields?: any;
}
interface ForeignKeyColumnParams extends ColumnParams {
table_ref: Table;
}
interface TableParams {
ref: string;
name: string;
database_flavor: string;
columns?: Column[];
primary_key?: Column;
comment?: string;
indexes?: string[];
unique_columns?: string[];
jsonschema_fields: any;
}
interface ExpandColumnsParams {
table_definitions?: any;
columns_definitions?: any;
referenced?: boolean;
}
/**
* Object to encapsulate a Column.
*
* Attributes:
* name(str): name of the Column.
* database_flavor(str): postgres or redshift.Defaults to postgres.
* comment(str): comment of the Column.Defaults to None.
* constraints(Dict): other columns constraints (not implemented).
* jsonschema_fields(Dict): Original fields in the jsonschema.
*/
export class Column {
name: string;
database_flavor: string;
comment: string;
constraints: any = {};
jsonschema_type: string;
jsonschema_fields: any;
constructor({ name, database_flavor, comment, constraints, jsonschema_type, jsonschema_fields }: ColumnParams) {
this.name = name;
this.database_flavor = database_flavor || "postgres";
this.comment = comment || '';
this.constraints = constraints || {};
this.jsonschema_type = jsonschema_type || '';
this.jsonschema_fields = jsonschema_fields || {};
}
public get max_length(): number {
return this.jsonschema_fields["maxLength"] || 256;
}
/**
* Data type of the columns.
*
* It accounts of the mapping of the original type to the db types.
*
* Returns:
* str: data type of the column.
*/
public get data_type(): string {
if ("format" in this.jsonschema_fields && this.jsonschema_fields["format"] in COLUMNS_TYPES_PREFERENCE) {
this.jsonschema_type = this.jsonschema_fields["format"]
}
return COLUMNS_TYPES[this.database_flavor][this.jsonschema_type]
.replace('{}', String(this.max_length));
}
public get is_pk(): boolean {
return !!this.jsonschema_fields["pk"];
}
/**
* Returns true if the column is a index.
*
* Returns:
* bool: True if it is index.
*/
public get is_index(): boolean {
return !!this.jsonschema_fields["index"];
}
/**
* Returns true if the column is a unique.
*
* Returns:
* bool: True if it is unique.
*/
public get is_unique(): boolean {
return !!this.jsonschema_fields["unique"];
}
/**
* Returns true if the column is a foreign key.
*
* Returns:
* bool: True if it is foreign key
*/
public get is_fk(): boolean {
return false;
}
hash() {
return crypto.createHash('sha256').update(this.name).digest('hex');
}
toString() {
return `Column(name=${this.name} data_type=${this.data_type})`;
}
}
/**
* Object to encapsulate a Table.
*
* Attributes:
* ref(str): id or reference to the table in the jsonschema.
* name(str): name of the table.
* database_flavor(str): postgres or redshift.Defaults to postgres.
* columns(List[Column]): columns of the table.
* primary_key(Column): Primary key column of the table.
* comment(str): comment of the table.Defaults to None.
* indexes(List[str]): Table indexeses(not implemented).
* unique_columns(List[str]): Table unique constraints(not implemented).
* jsonschema_fields(Dict): Original fields in the jsonschema.
*/
export class Table {
ref: string;
name: string;
database_flavor: string;
columns: Column[];
jsonschema_fields: any;
primary_key?: Column;
comment?: string;
indexes?: string[];
unique_columns?: string[];
constructor({ ref, name, database_flavor, columns, primary_key, comment, indexes, unique_columns, jsonschema_fields }: TableParams) {
this.ref = ref;
this.name = name;
this.database_flavor = database_flavor || "postgres";
this.columns = columns || [];
this.primary_key = primary_key;
this.comment = comment;
this.indexes = indexes || [];
this.unique_columns = unique_columns || [];
this.jsonschema_fields = jsonschema_fields || {};
}
private _expanded: boolean = false;
/**
* Expand the columns definitions of the
*
* Args:
* table_definitions(Dict, optional): Dictionary with the rest of the
* tables definitions.It is used for recursive calls to get the
* foreign keys.Defaults to dict().
* columns_definitions(Dict, optional): Dictionary with the definition
* of columns outside the main properties field.Defaults to dict().
* referenced(bool, optional): Whether or not the table is referenced
* by others.Used to make sure there is a Primary Key defined.
* Defaults to False.
*
* @param table_definitions
* @param columns_definitions
* @param referenced
* @returns
*/
expand_columns({ table_definitions = {}, columns_definitions = {}, referenced }: ExpandColumnsParams) {
if (this._expanded) {
console.log("Already expanded table. Skipping...");
return this;
}
const props: Record<string, any> = this.jsonschema_fields["properties"];
for (let [col_name, col_object] of Object.entries(props)) {
console.log(`Creating column ${col_name}`);
col_name = db_column_name(col_name);
console.log(`Renamed column to ${col_name}`);
let col: Column;
if ("$ref" in col_object) {
console.log(`Expanding ${col_name} reference ${col_object["$ref"]}`);
console.log(JSON.stringify(table_definitions, null, 2));
if (col_object["$ref"] in table_definitions) {
const myref = col_object["$ref"];
console.log(`Column is a FK! Expanding ${myref} before continue...`);
table_definitions[myref] =
table_definitions[myref].expand_columns({
table_definitions,
referenced: true,
});
col = new FKColumn({
table_ref: table_definitions[myref],
name: col_name,
database_flavor: this.database_flavor,
});
} else if (col_object["$ref"] in columns_definitions) {
console.log("Column ref a type that is not a object. Copy Column from columns definitions");
const myref = col_object["$ref"];
const ref_col = columns_definitions[myref];
const col_as_dict = { ...ref_col, "name": col_name };
col = new Column(col_as_dict);
} else {
console.log("Skipping ref as it is not in table definitions neither in columns definitions");
continue;
}
} else {
if (!("type" in col_object)) {
col_object = get_one_schema(col_object);
}
col = new Column({
name: col_name,
database_flavor: this.database_flavor,
jsonschema_type: col_object["type"],
jsonschema_fields: col_object,
});
}
this.columns.push(col);
if (col.is_pk) {
this.primary_key = col;
}
console.log(`New created column ${col}`);
}
if (referenced && !this.primary_key) {
console.log("Creating id column for the table in order to reference it as PK");
let idcol = new Column({
name: "id",
database_flavor: this.database_flavor,
jsonschema_type: "id",
});
this.columns.push(idcol);
this.primary_key = idcol;
}
this.columns = this._deduplicate_columns(this.columns);
return this;
}
_deduplicate_columns(columns: Column[]): Column[] {
return columns.reduce((a: Column[], c: Column) => {
if (!a.find(x => x.name === c.name)) {
a.push(c);
}
return a;
}, []);
}
}
/**
* Special type of Column object to represent a foreign key
*
* Attributes:
* table_ref(Table): Pointer to the foreign table object
*/
export class FKColumn extends Column {
table_ref: Table;
constructor(params: ForeignKeyColumnParams) {
super(params);
this.table_ref = params.table_ref;
}
/**
* Data type of the foreign key.
*
* Accounts of the data type of the primary key of the foreing table.
*
* Returns:
* str: the column data type.
*/
public get data_type(): string {
const data_type_ref = this.table_ref.primary_key?.data_type || '';
if ("varchar" === data_type_ref) {
return data_type_ref;
}
return FK_TYPES[data_type_ref] || "bigint";
}
/**
* Returns true if the column is a foreign key.
*
* Returns:
* bool: True if it is foreign key.
*/
public get is_fk(): boolean {
return true;
}
toString() {
return `FKColumn(name=${this.name} data_type=${this.data_type} table_ref.name=${this.table_ref.name})`;
}
}