Start CLI and db schema queries

This commit is contained in:
Garrett Mills 2020-10-05 11:42:16 -05:00
parent c1e7f750fc
commit b131cb589e
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
17 changed files with 933 additions and 6 deletions

View File

@ -2,6 +2,7 @@ import LifecycleUnit from '../../lib/src/lifecycle/Unit.ts'
import {CLIService} from './service/CLI.service.ts' import {CLIService} from './service/CLI.service.ts'
import {Unit} from '../../lib/src/lifecycle/decorators.ts' import {Unit} from '../../lib/src/lifecycle/decorators.ts'
import {Logging} from '../../lib/src/service/logging/Logging.ts' import {Logging} from '../../lib/src/service/logging/Logging.ts'
import {parseArgs} from '../../lib/src/external/std.ts'
@Unit() @Unit()
export default class CLIAppUnit extends LifecycleUnit { export default class CLIAppUnit extends LifecycleUnit {
@ -13,6 +14,24 @@ export default class CLIAppUnit extends LifecycleUnit {
public async up() { public async up() {
this.logger.verbose(`Handling CLI invocation...`) this.logger.verbose(`Handling CLI invocation...`)
const args = Deno.args 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()
}
} }
} }

View File

@ -2,6 +2,7 @@ import LifecycleUnit from '../../lib/src/lifecycle/Unit.ts'
import {CLIService} from './service/CLI.service.ts' import {CLIService} from './service/CLI.service.ts'
import {Unit} from '../../lib/src/lifecycle/decorators.ts' import {Unit} from '../../lib/src/lifecycle/decorators.ts'
import {UsageDirective} from './directive/UsageDirective.ts' import {UsageDirective} from './directive/UsageDirective.ts'
import {AboutDirective} from './directive/AboutDirective.ts'
@Unit() @Unit()
export default class CLIUnit extends LifecycleUnit { export default class CLIUnit extends LifecycleUnit {
@ -11,5 +12,6 @@ export default class CLIUnit extends LifecycleUnit {
public async up() { public async up() {
this.cli.register_directive(this.make(UsageDirective)) this.cli.register_directive(this.make(UsageDirective))
this.cli.register_directive(this.make(AboutDirective))
} }
} }

View File

@ -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))
}
}

View File

@ -1,14 +1,25 @@
import AppClass from '../../../lib/src/lifecycle/AppClass.ts' import AppClass from '../../../lib/src/lifecycle/AppClass.ts'
import {Logging} from '../../../lib/src/service/logging/Logging.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<string | number> }
export abstract class Directive extends AppClass { export abstract class Directive extends AppClass {
public abstract readonly keyword: string public abstract readonly keyword: string
public abstract readonly help: string public abstract readonly help: string
protected argv: string[] = []
protected parsed_argv: ParsedArguments = {}
static options() { static options() {
return [] return []
} }
public prepare(argv: string[], parsed_arguments: ParsedArguments) {
this.argv = argv
this.parsed_argv = parsed_arguments
}
public abstract invoke(): any public abstract invoke(): any
success(message: any) { success(message: any) {

View File

@ -1,10 +1,47 @@
import {Directive} from './Directive.ts' 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 { export class UsageDirective extends Directive {
public readonly keyword = 'help' public readonly keyword = 'help'
public readonly help = 'Display usage information' public readonly help = 'Display usage information'
constructor(
protected readonly cli: CLIService,
) { super() }
public async invoke() { 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('')
} }
} }

View File

@ -27,7 +27,6 @@ export class CLIService extends AppClass {
protected readonly logger: Logging, protected readonly logger: Logging,
) { super() } ) { super() }
/** /**
* Find a registered directive using its keyword, if one exists. * Find a registered directive using its keyword, if one exists.
* @param {string} keyword * @param {string} keyword
@ -57,4 +56,17 @@ export class CLIService extends AppClass {
this.logger.verbose(`Registering CLI directive with keyword: ${directive.keyword}`) this.logger.verbose(`Registering CLI directive with keyword: ${directive.keyword}`)
this.directives.push(directive) this.directives.push(directive)
} }
public show_logo() {
return true
}
public get_logo() {
return `██████╗ █████╗ ████████╗ ██████╗ ███╗ ██╗
`
}
} }

View File

@ -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 * 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 { 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 * 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'

View File

@ -6,6 +6,9 @@ import {Update} from './type/Update.ts'
import {Insert} from './type/Insert.ts' import {Insert} from './type/Insert.ts'
import {Delete} from './type/Delete.ts' import {Delete} from './type/Delete.ts'
import {Truncate} from './type/Truncate.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. * Wrap a string so it gets included in the query unescaped.
@ -99,6 +102,33 @@ export class Builder<T> {
return new Truncate<T>(target, alias) return new Truncate<T>(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<T> {
return new CreateDatabase<T>(name)
}
/**
* Get a new ALTER DATABASE statement.
* @param {string} [name] - optionally, the name of the database
* @return AlterDatabase
*/
public alter_database(name?: string): AlterDatabase<T> {
return new AlterDatabase<T>(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<T> {
return new CreateTable<T>(name)
}
/** /**
* Wrap a string so it gets included in the query unescaped. * Wrap a string so it gets included in the query unescaped.
* @param {string} value * @param {string} value

View File

@ -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<T> extends ConnectionMutable<T> {
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
}
}

View File

@ -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<T> extends ConnectionMutable<T> {
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
}
}

View File

@ -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<T> extends ConnectionMutable<T> {
protected _name?: string
protected _column_defs: Collection<ColumnDefinition> = new Collection<ColumnDefinition>()
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
}
}

View File

@ -1,4 +1,8 @@
import {QueryResult} from './types.ts' 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. * 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 base class for database connections.
* @abstract * @abstract
*/ */
export abstract class Connection { export abstract class Connection extends AppClass {
constructor( constructor(
/** /**
@ -26,7 +30,7 @@ export abstract class Connection {
* This connection's config object * This connection's config object
*/ */
public readonly config: any = {}, public readonly config: any = {},
) {} ) { super() }
/** /**
* Open the connection. * Open the connection.
@ -47,4 +51,13 @@ export abstract class Connection {
*/ */
public abstract async close(): Promise<void> public abstract async close(): Promise<void>
public abstract async databases(): Promise<Collection<Database>>
public abstract async database(name: string): Promise<Database | undefined>
public abstract async database_as_schema(name: string): Promise<Database>
public abstract async tables(database_name: string): Promise<Collection<Table>>
public abstract async table(database_name: string, table_name: string): Promise<Table | undefined>
} }

View File

@ -3,6 +3,10 @@ import { Client } from '../../../lib/src/external/db.ts'
import {collect, Collection} from '../../../lib/src/collection/Collection.ts' import {collect, Collection} from '../../../lib/src/collection/Collection.ts'
import { QueryResult, QueryRow } from './types.ts' import { QueryResult, QueryRow } from './types.ts'
import { logger } from '../../../lib/src/service/logging/global.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. * Database connection class for PostgreSQL connections.
@ -56,4 +60,73 @@ export default class PostgresConnection extends Connection {
await this._client.end() await this._client.end()
} }
public async databases() {
const query = (new Builder).select('datname')
.from('pg_database')
.target_connection(this)
const database_names: Collection<string> = (await query.execute()).rows.pluck('datname')
const databases: Collection<Database> = new Collection<Database>()
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<string> = (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<string> = (await query.execute()).rows.pluck('tablename')
const tables: Collection<Table> = new Collection<Table>()
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<string> = (await query.execute()).rows.pluck('tablename')
if ( table_names.includes(table_name) ) {
return this.make(Table, this, database_name, table_name)
}
}
}
} }

View File

@ -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
}
}

View File

@ -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<MigrationApply> {
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
}

View File

@ -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<Table>
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)
}
}
}

View File

@ -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
}
}