Start CLI and db schema queries
This commit is contained in:
parent
c1e7f750fc
commit
b131cb589e
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
30
cli/src/directive/AboutDirective.ts
Normal file
30
cli/src/directive/AboutDirective.ts
Normal 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))
|
||||
}
|
||||
}
|
@ -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<string | number> }
|
||||
|
||||
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) {
|
||||
|
@ -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('')
|
||||
}
|
||||
}
|
||||
|
@ -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 `██████╗ █████╗ ████████╗ ██████╗ ███╗ ██╗
|
||||
██╔══██╗██╔══██╗╚══██╔══╝██╔═══██╗████╗ ██║
|
||||
██║ ██║███████║ ██║ ██║ ██║██╔██╗ ██║
|
||||
██║ ██║██╔══██║ ██║ ██║ ██║██║╚██╗██║
|
||||
██████╔╝██║ ██║ ██║ ╚██████╔╝██║ ╚████║
|
||||
╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝`
|
||||
}
|
||||
}
|
||||
|
2
lib/src/external/std.ts
vendored
2
lib/src/external/std.ts
vendored
@ -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'
|
||||
|
@ -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<T> {
|
||||
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.
|
||||
* @param {string} value
|
||||
|
156
orm/src/builder/type/AlterDatabase.ts
Normal file
156
orm/src/builder/type/AlterDatabase.ts
Normal 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
|
||||
}
|
||||
}
|
124
orm/src/builder/type/CreateDatabase.ts
Normal file
124
orm/src/builder/type/CreateDatabase.ts
Normal 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
|
||||
}
|
||||
}
|
134
orm/src/builder/type/CreateTable.ts
Normal file
134
orm/src/builder/type/CreateTable.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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<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>
|
||||
}
|
||||
|
@ -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<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
orm/src/schema/migrations/Migration.ts
Normal file
17
orm/src/schema/migrations/Migration.ts
Normal 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
|
||||
}
|
||||
}
|
20
orm/src/schema/migrations/MigrationApply.ts
Normal file
20
orm/src/schema/migrations/MigrationApply.ts
Normal 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
|
||||
}
|
232
orm/src/schema/tree/Database.ts
Normal file
232
orm/src/schema/tree/Database.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
17
orm/src/schema/tree/Table.ts
Normal file
17
orm/src/schema/tree/Table.ts
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user