diff --git a/cli/src/CLIAppUnit.ts b/cli/src/CLIAppUnit.ts index 11c7f59..3cf0b16 100644 --- a/cli/src/CLIAppUnit.ts +++ b/cli/src/CLIAppUnit.ts @@ -2,6 +2,7 @@ import LifecycleUnit from '../../lib/src/lifecycle/Unit.ts' import {CLIService} from './service/CLI.service.ts' import {Unit} from '../../lib/src/lifecycle/decorators.ts' import {Logging} from '../../lib/src/service/logging/Logging.ts' +import {parseArgs} from '../../lib/src/external/std.ts' @Unit() export default class CLIAppUnit extends LifecycleUnit { @@ -13,6 +14,24 @@ export default class CLIAppUnit extends LifecycleUnit { public async up() { this.logger.verbose(`Handling CLI invocation...`) const args = Deno.args - console.log('args', {args}) + const parsed = parseArgs(args) + console.log('args', {args, parsed}) + + const keyword = parsed._[0] || '' + const directive = this.cli.get_directive_by_keyword(String(keyword)) + + this.logger.verbose(`Parsed directive: "${keyword}"`) + + if ( !directive ) { + const help = this.cli.get_directive_by_keyword('help') + if ( !help ) throw new Error('Usage directive not registered!') + + if ( keyword ) this.logger.error(`Invalid directive keyword: ${keyword}`) + await help.prepare(args, parsed) + await help.invoke() + } else { + await directive.prepare(args, parsed) + await directive.invoke() + } } } diff --git a/cli/src/CLIUnit.ts b/cli/src/CLIUnit.ts index f536a12..9b60264 100644 --- a/cli/src/CLIUnit.ts +++ b/cli/src/CLIUnit.ts @@ -2,6 +2,7 @@ import LifecycleUnit from '../../lib/src/lifecycle/Unit.ts' import {CLIService} from './service/CLI.service.ts' import {Unit} from '../../lib/src/lifecycle/decorators.ts' import {UsageDirective} from './directive/UsageDirective.ts' +import {AboutDirective} from './directive/AboutDirective.ts' @Unit() export default class CLIUnit extends LifecycleUnit { @@ -11,5 +12,6 @@ export default class CLIUnit extends LifecycleUnit { public async up() { this.cli.register_directive(this.make(UsageDirective)) + this.cli.register_directive(this.make(AboutDirective)) } } diff --git a/cli/src/directive/AboutDirective.ts b/cli/src/directive/AboutDirective.ts new file mode 100644 index 0000000..66e5486 --- /dev/null +++ b/cli/src/directive/AboutDirective.ts @@ -0,0 +1,30 @@ +import {Directive} from './Directive.ts' +import {Injectable} from '../../../di/src/decorator/Injection.ts' +import {CLIService} from '../service/CLI.service.ts' + +@Injectable() +export class AboutDirective extends Directive { + public readonly keyword = 'about' + public readonly help = 'Display information about Daton' + + constructor( + protected readonly cli: CLIService, + ) { super() } + + public async invoke() { + if ( this.cli.show_logo() ) { + console.log('') + console.log(this.cli.get_logo()) + } + + [ + '', + 'Daton is an opinionated application framework written for Deno. It provides a rich library of utilities, an ORM, dependency injector, routing stack, and logic for controllers, models, middleware, and configuration.', + '', + `Daton was created by and is © ${(new Date).getFullYear()} Garrett Mills. It is licensed for use by the terms of the MIT license.`, + '', + 'Source code: https://code.garrettmills.dev/garrettmills/daton', + '', + ].map(x => console.log(x)) + } +} diff --git a/cli/src/directive/Directive.ts b/cli/src/directive/Directive.ts index cebafa9..2562037 100644 --- a/cli/src/directive/Directive.ts +++ b/cli/src/directive/Directive.ts @@ -1,14 +1,25 @@ import AppClass from '../../../lib/src/lifecycle/AppClass.ts' import {Logging} from '../../../lib/src/service/logging/Logging.ts' +import {parseArgs} from '../../../lib/src/external/std.ts' + +export type ParsedArguments = { [key: string]: string | number | Array } export abstract class Directive extends AppClass { public abstract readonly keyword: string public abstract readonly help: string + protected argv: string[] = [] + protected parsed_argv: ParsedArguments = {} + static options() { return [] } + public prepare(argv: string[], parsed_arguments: ParsedArguments) { + this.argv = argv + this.parsed_argv = parsed_arguments + } + public abstract invoke(): any success(message: any) { diff --git a/cli/src/directive/UsageDirective.ts b/cli/src/directive/UsageDirective.ts index 615f9c4..9b1ff31 100644 --- a/cli/src/directive/UsageDirective.ts +++ b/cli/src/directive/UsageDirective.ts @@ -1,10 +1,47 @@ import {Directive} from './Directive.ts' +import {CLIService} from "../service/CLI.service.ts"; +import {Injectable} from "../../../di/src/decorator/Injection.ts"; +@Injectable() export class UsageDirective extends Directive { public readonly keyword = 'help' public readonly help = 'Display usage information' + constructor( + protected readonly cli: CLIService, + ) { super() } + public async invoke() { - console.log('Hello, from Daton CLI.') + console.log('') + if ( this.cli.show_logo() ) { + console.log(this.cli.get_logo()) + console.log('') + } + + console.log('Welcome to the Daton CLI. This tool can help you interact with your Daton application.\n') + console.log('To get started, specify one of the directives below. You can always specify the --help option to get more information about a directive.') + console.log('') + + const usages = this.cli.get_directives().map((directive: Directive): string[] => { + return [directive.keyword, directive.help] + }) + + const pad_length = usages.max(grp => grp[0].length) + 2 + + const padded_usages = usages.map(grp => { + const keyword = grp[0] + if ( keyword.length < pad_length ) { + const pad = Array(pad_length - keyword.length).fill(' ').join('') + return [`${pad}${keyword}`, grp[1]] + } + + return grp + }) + + padded_usages.each(grp => { + console.log(`${grp[0]} : ${grp[1]}`) + }) + + console.log('') } } diff --git a/cli/src/service/CLI.service.ts b/cli/src/service/CLI.service.ts index b481891..8e845c7 100644 --- a/cli/src/service/CLI.service.ts +++ b/cli/src/service/CLI.service.ts @@ -27,7 +27,6 @@ export class CLIService extends AppClass { protected readonly logger: Logging, ) { super() } - /** * Find a registered directive using its keyword, if one exists. * @param {string} keyword @@ -57,4 +56,17 @@ export class CLIService extends AppClass { this.logger.verbose(`Registering CLI directive with keyword: ${directive.keyword}`) this.directives.push(directive) } + + public show_logo() { + return true + } + + public get_logo() { + return `██████╗ █████╗ ████████╗ ██████╗ ███╗ ██╗ +██╔══██╗██╔══██╗╚══██╔══╝██╔═══██╗████╗ ██║ +██║ ██║███████║ ██║ ██║ ██║██╔██╗ ██║ +██║ ██║██╔══██║ ██║ ██║ ██║██║╚██╗██║ +██████╔╝██║ ██║ ██║ ╚██████╔╝██║ ╚████║ +╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝` + } } diff --git a/lib/src/external/std.ts b/lib/src/external/std.ts index 3102465..237ec8c 100644 --- a/lib/src/external/std.ts +++ b/lib/src/external/std.ts @@ -4,4 +4,4 @@ export * as path from 'https://deno.land/std@0.67.0/path/mod.ts' export * as fs from 'https://deno.land/std@0.67.0/fs/mod.ts' export { generate as uuid } from 'https://deno.land/std@0.67.0/uuid/v4.ts' export * as file_server from 'https://deno.land/std@0.67.0/http/file_server.ts' -// export { moment } from 'https://deno.land/x/moment/moment.ts' +export { parse as parseArgs } from 'https://deno.land/std@0.67.0/flags/mod.ts' diff --git a/orm/src/builder/Builder.ts b/orm/src/builder/Builder.ts index dc1f2bd..c4727ba 100644 --- a/orm/src/builder/Builder.ts +++ b/orm/src/builder/Builder.ts @@ -6,6 +6,9 @@ import {Update} from './type/Update.ts' import {Insert} from './type/Insert.ts' import {Delete} from './type/Delete.ts' import {Truncate} from './type/Truncate.ts' +import {CreateDatabase} from "./type/CreateDatabase.ts"; +import {AlterDatabase} from "./type/AlterDatabase.ts"; +import {CreateTable} from "./type/CreateTable.ts"; /** * Wrap a string so it gets included in the query unescaped. @@ -99,6 +102,33 @@ export class Builder { return new Truncate(target, alias) } + /** + * Get a new CREATE DATABASE statement. + * @param {string} [name] - optionally, the name of the new database + * @return CreateDatabase + */ + public create_database(name?: string): CreateDatabase { + return new CreateDatabase(name) + } + + /** + * Get a new ALTER DATABASE statement. + * @param {string} [name] - optionally, the name of the database + * @return AlterDatabase + */ + public alter_database(name?: string): AlterDatabase { + return new AlterDatabase(name) + } + + /** + * Get a new CREATE TABLE statement. + * @param {string} [name] - optionally, the name of the new table + * @return CreateTable + */ + public create_table(name?: string): CreateTable { + return new CreateTable(name) + } + /** * Wrap a string so it gets included in the query unescaped. * @param {string} value diff --git a/orm/src/builder/type/AlterDatabase.ts b/orm/src/builder/type/AlterDatabase.ts new file mode 100644 index 0000000..bc59521 --- /dev/null +++ b/orm/src/builder/type/AlterDatabase.ts @@ -0,0 +1,156 @@ +import ConnectionMutable from './ConnectionMutable.ts' +import {MalformedSQLGrammarError} from './Select.ts' +import {escape, EscapedValue} from "../types.ts"; + +/** + * Base query builder class for ALTER DATABASE queries. + * @extends ConnectionMutable + */ +export class AlterDatabase extends ConnectionMutable { + protected _name?: string + protected _rename?: string + protected _owner?: string + protected _tablespace?: string + protected _conn_limit?: number + protected _is_template?: boolean + protected _allow_connections?: boolean + + protected _reset_all: boolean = false + protected _config_resets: string[] = [] + protected _config_from_current: string[] = [] + protected _config_sets: (string|EscapedValue)[][] = [] + + constructor(name?: string) { + super() + if ( name ) this._name = name + } + + sql(level: number = 0): string { + if ( !this._name ) { + throw new MalformedSQLGrammarError(`Database name to alter is undefined.`) + } + + const queries = [] + + const option_pairs: (string|boolean|number)[][] = [] + if ( typeof this._allow_connections !== 'undefined' ) { + option_pairs.push(['ALLOW_CONNECTIONS', this._allow_connections]) + } + + if ( typeof this._conn_limit !== 'undefined' ) { + option_pairs.push(['CONNECTION LIMIT', this._conn_limit]) + } + + if ( typeof this._is_template !== 'undefined' ) { + option_pairs.push(['IS_TEMPLATE', this._is_template]) + } + + if ( option_pairs.length > 0 ) { + option_pairs.some(pair => { + queries.push(`ALTER DATABASE ${this._name} WITH ${pair[0]} ${pair[1]}`) + }) + } + + if ( this._owner ) { + queries.push(`ALTER DATABASE ${this._name} OWNER TO ${this._owner}`) + } + + if ( this._tablespace ) { + queries.push(`ALTER DATABASE ${this._name} SET TABLESPACE ${this._tablespace}`) + } + + if ( this._reset_all ) { + queries.push(`ALTER DATABASE ${this._name} RESET ALL`) + } + + if ( this._config_resets.length > 0 ) { + this._config_resets.some(param => { + queries.push(`ALTER DATABASE ${this._name} RESET ${param}`) + }) + } + + if ( this._config_from_current.length > 0 ) { + this._config_from_current.some(param => { + queries.push(`ALTER DATABASE ${this._name} SET ${param} FROM CURRENT`) + }) + } + + if ( this._config_sets.length > 0 ) { + this._config_sets.some(set => { + queries.push(`ALTER DATABASE ${this._name} SET ${set[0]} TO ${escape(set[1])}`) + }) + } + + // This needs to happen last + if ( this._rename ) { + queries.push(`ALTER DATABASE ${this._name} RENAME TO ${this._rename}`) + } + + return queries.join(';\n') + } + + name(name: string): this { + this._name = name + return this + } + + rename_to(name: string): this { + this._rename = name + return this + } + + owner(user: string): this { + this._owner = user + return this + } + + tablespace(name: string): this { + this._tablespace = name + return this + } + + connection_limit(max: number): this { + this._conn_limit = max + return this + } + + as_template(): this { + this._is_template = true + return this + } + + not_as_template(): this { + this._is_template = false + return this + } + + disallow_connections(): this { + this._allow_connections = false + return this + } + + allow_connections(): this { + this._allow_connections = true + return this + } + + reset_all(): this { + this._reset_all = true + return this + } + + reset(parameter: string): this { + this._config_resets.push(parameter) + return this + } + + set_from_current(parameter: string): this { + this._config_from_current.push(parameter) + return this + } + + set(parameter: string, value: EscapedValue): this { + this._config_sets.push([parameter, value]) + return this + } +} diff --git a/orm/src/builder/type/CreateDatabase.ts b/orm/src/builder/type/CreateDatabase.ts new file mode 100644 index 0000000..a4421d8 --- /dev/null +++ b/orm/src/builder/type/CreateDatabase.ts @@ -0,0 +1,124 @@ +import ConnectionMutable from './ConnectionMutable.ts' +import {MalformedSQLGrammarError} from './Select.ts' + +/** + * Base query builder class for CREATE DATABASE queries. + * @extends ConnectionMutable + */ +export class CreateDatabase extends ConnectionMutable { + protected _name?: string + protected _owner?: string + protected _template?: string + protected _encoding?: string + protected _lc_collate?: string + protected _lc_ctype?: string + protected _tablespace?: string + protected _conn_limit?: number + protected _is_template?: boolean + protected _allow_connections?: boolean + + constructor(name?: string) { + super() + if ( name ) this._name = name + } + + sql(level: number = 0): string { + const indent = Array(level).fill(' ').join('') + + if ( !this._name ) { + throw new MalformedSQLGrammarError(`Database name to create is undefined.`) + } + + const key_pairs: (string|number)[][] = [] + if ( this._owner ) { + key_pairs.push(['OWNER', this._owner]) + } + + if ( this._template ) { + key_pairs.push(['TEMPLATE', this._template]) + } + + if ( this._encoding ) { + key_pairs.push(['ENCODING', this._encoding]) + } + + if ( this._lc_collate ) { + key_pairs.push(['LC_COLLATE', this._lc_collate]) + } + + if ( this._lc_ctype ) { + key_pairs.push(['LC_CTYPE', this._lc_ctype]) + } + + if ( this._tablespace ) { + key_pairs.push(['TABLESPACE', this._tablespace]) + } + + if ( this._conn_limit ) { + key_pairs.push(['CONNECTION LIMIT', this._conn_limit]) + } + + if ( typeof this._allow_connections !== 'undefined' ) { + key_pairs.push(['ALLOW_CONNECTIONS', this._allow_connections ? 'TRUE' : 'FALSE']) + } + + if ( typeof this._is_template !== 'undefined' ) { + key_pairs.push(['IS_TEMPLATE', this._is_template ? 'TRUE' : 'FALSE']) + } + + return [ + `CREATE DATABASE ${this._name}`, + ...(key_pairs.length < 1 ? [] : [`${indent}WITH`, ...key_pairs.map(x => `${indent}${indent}${x[0]} ${x[1]}`)]), + ].filter(x => String(x).trim()).join(`\n${indent}`) + } + + name(name: string): this { + this._name = name + return this + } + + owner(user: string): this { + this._owner = user + return this + } + + template(name: string): this { + this._template = name + return this + } + + encoding(name: string): this { + this._encoding = name + return this + } + + lc_collate(type: string): this { + this._lc_collate = type + return this + } + + lc_ctype(type: string): this { + this._lc_ctype = type + return this + } + + tablespace(name: string): this { + this._tablespace = name + return this + } + + connection_limit(max: number): this { + this._conn_limit = max + return this + } + + as_template(): this { + this._is_template = true + return this + } + + disallow_connections(): this { + this._allow_connections = false + return this + } +} diff --git a/orm/src/builder/type/CreateTable.ts b/orm/src/builder/type/CreateTable.ts new file mode 100644 index 0000000..6c2965a --- /dev/null +++ b/orm/src/builder/type/CreateTable.ts @@ -0,0 +1,134 @@ +import ConnectionMutable from "./ConnectionMutable.ts"; +import {Type} from "../../db/types.ts"; +import {Collection} from "../../../../lib/src/collection/Collection.ts"; +import {MalformedSQLGrammarError} from "./Select.ts"; + +export interface ColumnDefinition { + with_name: string, + with_type: Type, + type_size?: string | number, + null: boolean, + as_primary_key: boolean, + with_default?: string, + sql: (level: number) => string, + check_expression?: string, + check_no_inherit: boolean, +} + +export class ColumnFluency implements ColumnDefinition { + public with_name: string = '' + public with_type: Type = Type.varchar + public null: boolean = false + public as_primary_key: boolean = false + public with_default?: string + public type_size?: string | number + public check_expression?: string + public check_no_inherit: boolean = false + + public clone() { + const col = new ColumnFluency() + col.with_name = this.with_name + col.with_type = this.with_type + col.null = this.null + col.as_primary_key = this.as_primary_key + col.with_default = this.with_default + col.type_size = this.type_size + col.check_expression = this.check_expression + col.check_no_inherit = this.check_no_inherit + return col + } + + public name(name: string): this { + this.with_name = name + return this + } + + public type(type: Type, size?: string | number): this { + this.with_type = type + if ( size ) this.type_size = size + return this + } + + public nullable() { + this.null = true + return this + } + + public primary_key() { + this.as_primary_key = true + return this + } + + public default(val: string) { + this.with_default = val + } + + public check(expression: string, inherit: boolean = true): this { + this.check_expression = expression + this.check_no_inherit = !inherit + return this + } + + public sql(level: number = 0): string { + const indent = Array(level).fill(' ').join('') + + const parts = [] + if ( this.null ) parts.push('NULL') + else parts.push('NOT NULL') + if ( this.as_primary_key ) parts.push('PRIMARY KEY') + if ( this.with_default ) parts.push(`DEFAULT ${this.with_default}`) + if ( this.check_expression ) { + parts.push(`CHECK (${this.check_expression})${this.check_no_inherit ? ' NO INHERIT' : ''}`) + } + + return `${indent}${this.with_name} ${this.with_type}${this.type_size ? '('+this.type_size+')' : ''}${parts.length > 0 ? ' '+parts.join(' ') : ''}` + } +} + +export type FluencyFunction = (col: ColumnFluency) => any + +export class CreateTable extends ConnectionMutable { + protected _name?: string + protected _column_defs: Collection = new Collection() + + constructor(name?: string) { + super() + if ( name ) this._name = name + } + + sql(level: number = 0): string { + const indent = Array(level).fill(' ').join('') + + if ( !this._name ) { + throw new MalformedSQLGrammarError(`Missing required table name for create statement.`) + } + + const column_sql = this._column_defs.map(x => `${indent}${x.sql(level + 1)}`) + + return [ + `CREATE TABLE ${this._name} (`, + column_sql.join(`,\n${indent}`), + ')' + ].filter(x => String(x).trim()).join(`\n${indent}`) + } + + name(name: string): this { + this._name = name + return this + } + + column(name_or_fluency_fn: string | FluencyFunction, type?: Type, type_size?: string | number): this { + const col = new ColumnFluency() + + if ( typeof name_or_fluency_fn === 'string' ) { + if ( !type ) throw new MalformedSQLGrammarError(`Missing type for column: ${name_or_fluency_fn}`) + col.name(name_or_fluency_fn) + .type(type, type_size) + } else { + name_or_fluency_fn(col) + } + + this._column_defs.push(col) + return this + } +} diff --git a/orm/src/db/Connection.ts b/orm/src/db/Connection.ts index 457b7ad..e65faec 100644 --- a/orm/src/db/Connection.ts +++ b/orm/src/db/Connection.ts @@ -1,4 +1,8 @@ import {QueryResult} from './types.ts' +import {Collection} from "../../../lib/src/collection/Collection.ts"; +import {Database} from "../schema/tree/Database.ts"; +import AppClass from "../../../lib/src/lifecycle/AppClass.ts"; +import {Table} from "../schema/tree/Table.ts"; /** * Error thrown when a connection is used before it is ready. @@ -14,7 +18,7 @@ export class ConnectionNotReadyError extends Error { * Abstract base class for database connections. * @abstract */ -export abstract class Connection { +export abstract class Connection extends AppClass { constructor( /** @@ -26,7 +30,7 @@ export abstract class Connection { * This connection's config object */ public readonly config: any = {}, - ) {} + ) { super() } /** * Open the connection. @@ -47,4 +51,13 @@ export abstract class Connection { */ public abstract async close(): Promise + public abstract async databases(): Promise> + + public abstract async database(name: string): Promise + + public abstract async database_as_schema(name: string): Promise + + public abstract async tables(database_name: string): Promise> + + public abstract async table(database_name: string, table_name: string): Promise } diff --git a/orm/src/db/PostgresConnection.ts b/orm/src/db/PostgresConnection.ts index ab34fde..7d2346f 100644 --- a/orm/src/db/PostgresConnection.ts +++ b/orm/src/db/PostgresConnection.ts @@ -3,6 +3,10 @@ import { Client } from '../../../lib/src/external/db.ts' import {collect, Collection} from '../../../lib/src/collection/Collection.ts' import { QueryResult, QueryRow } from './types.ts' import { logger } from '../../../lib/src/service/logging/global.ts' +import {Database} from "../schema/tree/Database.ts"; +import {escape} from "../builder/types.ts"; +import {Table} from "../schema/tree/Table.ts"; +import {Builder} from "../builder/Builder.ts"; /** * Database connection class for PostgreSQL connections. @@ -56,4 +60,73 @@ export default class PostgresConnection extends Connection { await this._client.end() } + public async databases() { + const query = (new Builder).select('datname') + .from('pg_database') + .target_connection(this) + + const database_names: Collection = (await query.execute()).rows.pluck('datname') + + const databases: Collection = new Collection() + + for ( const name of database_names ) { + const db = this.make(Database, this, name) + await db.introspect() + databases.push(db) + } + + return databases + } + + public async database(name: string) { + const query = (new Builder).select('datname') + .from('pg_database') + .target_connection(this) + + const database_names: Collection = (await query.execute()).rows.pluck('datname') + + if ( database_names.includes(name) ) { + const db = this.make(Database, this, name) + await db.introspect() + return db + } + } + + public async database_as_schema(name: string) { + const db = await this.database(name) + if ( db ) return db + + return this.make(Database, this, name) + } + + public async tables(database_name: string) { + const query = (new Builder).select('tablename') + .from('pg_catalog.pg_tables') + .where('pg_tables.tableowner', '=', database_name) + .target_connection(this) + + const table_names: Collection = (await query.execute()).rows.pluck('tablename') + const tables: Collection
= new Collection
() + + for ( const name of table_names ) { + tables.push(this.make(Table, this, database_name, name)) + } + + return tables + } + + public async table(database_name: string, table_name: string) { + const database = await this.database(database_name) + if ( database ) { + const query = (new Builder).select('tablename') + .from('pg_catalog.pg_tables') + .where('tableowner', '=', database_name) + .target_connection(this) + + const table_names: Collection = (await query.execute()).rows.pluck('tablename') + if ( table_names.includes(table_name) ) { + return this.make(Table, this, database_name, table_name) + } + } + } } diff --git a/orm/src/schema/migrations/Migration.ts b/orm/src/schema/migrations/Migration.ts new file mode 100644 index 0000000..bbbcba3 --- /dev/null +++ b/orm/src/schema/migrations/Migration.ts @@ -0,0 +1,17 @@ +import AppClass from '../../../../lib/src/lifecycle/AppClass.ts' + +export default abstract class Migration extends AppClass { + public static get_current_epoch(): number { + return (new Date).getTime() + } + + public abstract readonly ordering_epoch: number + + public abstract up(): any + + public abstract down(): any + + public should_apply(): any { + return true + } +} diff --git a/orm/src/schema/migrations/MigrationApply.ts b/orm/src/schema/migrations/MigrationApply.ts new file mode 100644 index 0000000..7d309ab --- /dev/null +++ b/orm/src/schema/migrations/MigrationApply.ts @@ -0,0 +1,20 @@ +import {Model} from '../../model/Model.ts' +import {Field} from '../../model/Field.ts' +import {Type} from '../../db/types.ts' + +export default class MigrationApply extends Model { + protected static table = 'daton_migration_applies' + protected static key = 'daton_migration_apply_id' + + @Field(Type.serial) + public daton_migration_apply_id!: number + + @Field(Type.bigint) + public ordering_epoch!: number + + @Field(Type.varchar) + public migration_file_name!: string + + @Field(Type.timestamp) + public applied_date!: Date +} diff --git a/orm/src/schema/tree/Database.ts b/orm/src/schema/tree/Database.ts new file mode 100644 index 0000000..7dfec9a --- /dev/null +++ b/orm/src/schema/tree/Database.ts @@ -0,0 +1,232 @@ +import AppClass from '../../../../lib/src/lifecycle/AppClass.ts' +import {Connection} from '../../db/Connection.ts' +import {Collection} from '../../../../lib/src/collection/Collection.ts' +import {Table} from './Table.ts' +import {Builder} from '../../builder/Builder.ts' +import {Join} from '../../builder/type/join/Join.ts' + +export class Database extends AppClass { + private _table_cache?: Collection
+ protected _rename?: string + + protected _owner?: string + protected _owner_override?: string + public owner(set?: string) { + if ( set ) { + this._owner_override = set + return this + } else { + return this._owner_override || this._owner + } + } + + protected _encoding?: string + protected _encoding_override?: string + public encoding(set?: string) { + if ( set ) { + this._encoding_override = set + return this + } else { + return this._encoding_override || this._encoding + } + } + + protected _lc_collate?: string + protected _lc_collate_override?: string + public lc_collate(set?: string) { + if ( set ) { + this._lc_collate_override = set + return this + } else { + return this._lc_collate_override || this._lc_collate + } + } + + protected _lc_ctype?: string + protected _lc_ctype_override?: string + public lc_ctype(set?: string) { + if ( set ) { + this._lc_ctype_override = set + return this + } else { + return this._lc_ctype_override || this._lc_ctype + } + } + + protected _tablespace?: string + protected _tablespace_override?: string + public tablespace(set?: string) { + if ( set ) { + this._tablespace_override = set + return this + } else { + return this._tablespace_override || this._tablespace + } + } + + protected _is_template?: boolean + protected _is_template_override?: boolean + public is_template(set?: boolean) { + if ( typeof set !== 'undefined' ) { + this._is_template_override = set + return this + } else { + return (typeof this._is_template_override !== 'undefined' ? this._is_template_override : this._is_template) + } + } + + protected _allow_conn?: boolean + protected _allow_conn_override?: boolean + public allow_conn(set?: boolean) { + if ( typeof set !== 'undefined' ) { + this._allow_conn_override = set + return this + } else { + return (typeof this._allow_conn_override !== 'undefined' ? this._allow_conn_override : this._allow_conn) + } + } + + protected _conn_limit?: number + protected _conn_limit_override?: number + public connection_limit(set?: number) { + if ( typeof set !== 'undefined' ) { + this._conn_limit_override = set + } else { + return (typeof this._conn_limit_override !== 'undefined' ? this._conn_limit_override : this._conn_limit) + } + } + + constructor( + public readonly connection: Connection, + public readonly name: string, + ) { super() } + + rename(name: string): this { + this._rename = name + return this + } + + async introspect() { + this._table_cache = await this.connection.tables(this.name) + this._table_cache.each((table: Table) => table.set_database(this)) + + const query = (new Builder).select( + 'pg_catalog.pg_get_userbyid(db.datdba) AS owner', + 'pg_encoding_to_char(db.encoding) AS encoding_char', + 'datcollate', + 'datctype', + 'tablespace.spcname AS tablespace', + 'datistemplate', + 'datallowconn', + 'datconnlimit' + ) + .from('pg_catalog.pg_database', 'db') + .join('pg_catalog.pg_tablespace', 'tablespace', (join: Join) => { + join.whereRaw('db.dattablespace', '=', 'tablespace.oid') + }) + .where('db.datname', '=', this.name) + .target_connection(this.connection) + + const info: any = (await query.execute()).rows.first() + if ( info ) { + this._owner = info.owner + this._encoding = info.encoding_char + this._lc_collate = info.datcollate + this._lc_ctype = info.datctype + this._tablespace = info.tablespace + this._is_template = info.datistemplate + this._allow_conn = info.datallowconn + this._conn_limit = info.datconnlimit + } + } + + async tables(invalidate_cache: boolean = false) { + if ( !this._table_cache || invalidate_cache ) { + this._table_cache = await this.connection.tables(this.name) + this._table_cache.each((table: Table) => table.set_database(this)) + } + + return this._table_cache + } + + async table(name: string, invalidate_cache: boolean = false) { + if ( !this._table_cache || invalidate_cache ) { + this._table_cache = await this.connection.tables(this.name) + this._table_cache.each((table: Table) => table.set_database(this)) + } + + return this._table_cache.firstWhere('name', '=', name) + } + + public is_dirty() { + return ( + (this._owner_override && this._owner_override !== this._owner) + || (this._encoding_override && this._encoding_override !== this._encoding) + || (this._lc_collate_override && this._lc_collate_override !== this._lc_collate) + || (this._lc_ctype_override && this._lc_ctype_override !== this._lc_ctype) + || (this._tablespace_override && this._tablespace_override !== this._tablespace) + || (typeof this._is_template_override !== 'undefined' && this._is_template_override !== this._is_template) + || (typeof this._allow_conn_override !== 'undefined' && this._allow_conn_override !== this._allow_conn) + || (typeof this._conn_limit_override !== 'undefined' && this._conn_limit_override !== this._conn_limit) + || this._rename + ) + } + + public async exists() { + return !!(await this.connection.database(this.name)) + } + + public async save() { + if ( await this.exists() ) { + const query = (new Builder).alter_database().name(this.name) + + if ( this._owner_override && this._owner_override !== this._owner ) { + query.owner(this._owner_override) + } + + if ( typeof this._allow_conn_override !== 'undefined' && this._allow_conn_override !== this._allow_conn ) { + if ( this._allow_conn_override ) query.allow_connections() + else query.disallow_connections() + } + + if ( typeof this._conn_limit_override !== 'undefined' && this._conn_limit_override !== this._conn_limit ) { + query.connection_limit(this._conn_limit_override) + } + + if ( typeof this._is_template_override !== 'undefined' && this._is_template_override !== this._is_template ) { + if ( this._is_template_override ) query.as_template() + else query.not_as_template() + } + + if ( this._rename ) { + query.rename_to(this._rename) + } + + if ( this._owner_override && this._owner_override !== this._owner ) { + query.owner(this._owner_override) + } + + if ( this._tablespace_override && this._tablespace_override !== this._tablespace ) { + query.tablespace(this._tablespace_override) + } + + if ( query.sql().trim().length < 1 ) return + + await query.execute_in_connection(this.connection) + } else { + const query = (new Builder).create_database() + .name(this.name) + + if ( this._owner_override ) query.owner(this._owner_override) + if ( this._is_template_override ) query.as_template() + if ( this._encoding_override ) query.encoding(this._encoding_override) + if ( this._lc_ctype_override ) query.lc_collate(this._lc_ctype_override) + if ( this._lc_ctype ) query.lc_ctype(this._lc_ctype) + if ( this._tablespace ) query.tablespace(this._tablespace) + if ( !this._allow_conn_override && typeof this._allow_conn_override !== 'undefined' ) query.disallow_connections() + if ( this._conn_limit_override && typeof this._conn_limit_override !== 'undefined' ) query.connection_limit(this._conn_limit_override) + + await query.execute_in_connection(this.connection) + } + } +} diff --git a/orm/src/schema/tree/Table.ts b/orm/src/schema/tree/Table.ts new file mode 100644 index 0000000..b1fb37c --- /dev/null +++ b/orm/src/schema/tree/Table.ts @@ -0,0 +1,17 @@ +import AppClass from '../../../../lib/src/lifecycle/AppClass.ts' +import {Connection} from "../../db/Connection.ts"; +import {Database} from "./Database.ts"; + +export class Table extends AppClass { + protected parent_database?: Database + + constructor( + public readonly connection: Connection, + public readonly database_name: string, + public readonly name: string, + ) { super() } + + public set_database(db: Database) { + this.parent_database = db + } +}