mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Include schema package
This commit is contained in:
Submodule packages/schema deleted from e14f525d53
6
packages/schema/.editorconfig
Normal file
6
packages/schema/.editorconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
31
packages/schema/.github/workflows/tests.yml
vendored
Normal file
31
packages/schema/.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- run: docker-compose up -d
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- run: "while ! docker-compose logs mysql | grep -q 'mysqld: ready for connections.'; do sleep 2; done"
|
||||
- run: "while ! docker-compose logs postgres | grep -q 'database system is ready to accept connections'; do sleep 2; done"
|
||||
|
||||
- run: npm test
|
||||
106
packages/schema/.gitignore
vendored
Normal file
106
packages/schema/.gitignore
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
.DS_Store
|
||||
21
packages/schema/LICENSE
Normal file
21
packages/schema/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 knex
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
217
packages/schema/README.md
Normal file
217
packages/schema/README.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# @directus/schema
|
||||
|
||||
Utility for extracting information about the database schema
|
||||
|
||||
## Usage
|
||||
|
||||
The package is initialized by passing it an instance of Knex:
|
||||
|
||||
```ts
|
||||
import knex from 'knex';
|
||||
import schema from '@directus/schema';
|
||||
|
||||
const database = knex({
|
||||
client: 'mysql',
|
||||
connection: {
|
||||
host: '127.0.0.1',
|
||||
user: 'your_database_user',
|
||||
password: 'your_database_password',
|
||||
database: 'myapp_test',
|
||||
charset: 'utf8',
|
||||
},
|
||||
});
|
||||
|
||||
const inspector = schema(database);
|
||||
|
||||
export default inspector;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```ts
|
||||
import inspector from './inspector';
|
||||
|
||||
async function logTables() {
|
||||
const tables = await inspector.tables();
|
||||
console.log(tables);
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Note: MySQL doesn't support the `schema` parameter, as schema and database are ambiguous in MySQL.
|
||||
|
||||
Note 2: Some database types might return slightly more information than others. See the type files for a specific overview what to expect from driver to driver.
|
||||
|
||||
Note 3: MSSQL doesn't support comment for either tables or columns
|
||||
|
||||
### Tables
|
||||
|
||||
#### `tables(): Promise<string[]>`
|
||||
|
||||
Retrieve all tables in the current database.
|
||||
|
||||
```ts
|
||||
await inspector.tables();
|
||||
// => ['articles', 'images', 'reviews']
|
||||
```
|
||||
|
||||
#### `tableInfo(table?: string): Promise<Table | Table[]>`
|
||||
|
||||
Retrieve the table info for the given table, or all tables if no table is specified
|
||||
|
||||
```ts
|
||||
await inspector.tableInfo('articles');
|
||||
// => {
|
||||
// name: 'articles',
|
||||
// schema: 'project',
|
||||
// comment: 'Informational blog posts'
|
||||
// }
|
||||
|
||||
await inspector.tableInfo();
|
||||
// => [
|
||||
// {
|
||||
// name: 'articles',
|
||||
// schema: 'project',
|
||||
// comment: 'Informational blog posts'
|
||||
// },
|
||||
// { ... },
|
||||
// { ... }
|
||||
// ]
|
||||
```
|
||||
|
||||
#### `hasTable(table: string): Promise<boolean>`
|
||||
|
||||
Check if a table exists in the current database.
|
||||
|
||||
```ts
|
||||
await inspector.hasTable('articles');
|
||||
// => true
|
||||
```
|
||||
|
||||
### Columns
|
||||
|
||||
#### `columns(table?: string): Promise<{ table: string, column: string }[]>`
|
||||
|
||||
Retrieve all columns in a given table, or all columns if no table is specified
|
||||
|
||||
```ts
|
||||
await inspector.columns();
|
||||
// => [
|
||||
// {
|
||||
// "table": "articles",
|
||||
// "column": "id"
|
||||
// },
|
||||
// {
|
||||
// "table": "articles",
|
||||
// "column": "title"
|
||||
// },
|
||||
// {
|
||||
// "table": "images",
|
||||
// "column": "id"
|
||||
// }
|
||||
// ]
|
||||
|
||||
await inspector.columns('articles');
|
||||
// => [
|
||||
// {
|
||||
// "table": "articles",
|
||||
// "column": "id"
|
||||
// },
|
||||
// {
|
||||
// "table": "articles",
|
||||
// "column": "title"
|
||||
// }
|
||||
// ]
|
||||
```
|
||||
|
||||
#### `columnInfo(table?: string, column?: string): Promise<Column[] | Column>`
|
||||
|
||||
Retrieve all columns from a given table. Returns all columns if `table` parameter is undefined.
|
||||
|
||||
```ts
|
||||
await inspector.columnInfo('articles');
|
||||
// => [
|
||||
// {
|
||||
// name: "id",
|
||||
// table: "articles",
|
||||
// type: "VARCHAR",
|
||||
// defaultValue: null,
|
||||
// maxLength: null,
|
||||
// isNullable: false,
|
||||
// isPrimaryKey: true,
|
||||
// hasAutoIncrement: true,
|
||||
// foreignKeyColumn: null,
|
||||
// foreignKeyTable: null,
|
||||
// comment: "Primary key for the articles collection"
|
||||
// },
|
||||
// { ... },
|
||||
// { ... }
|
||||
// ]
|
||||
|
||||
await inspector.columnInfo('articles', 'id');
|
||||
// => {
|
||||
// name: "id",
|
||||
// table: "articles",
|
||||
// type: "VARCHAR",
|
||||
// defaultValue: null,
|
||||
// maxLength: null,
|
||||
// isNullable: false,
|
||||
// isPrimaryKey: true,
|
||||
// hasAutoIncrement: true,
|
||||
// foreignKeyColumn: null,
|
||||
// foreignKeyTable: null,
|
||||
// comment: "Primary key for the articles collection"
|
||||
// }
|
||||
```
|
||||
|
||||
#### `primary(table: string): Promise<string>`
|
||||
|
||||
Retrieve the primary key column for a given table
|
||||
|
||||
```ts
|
||||
await inspector.primary('articles');
|
||||
// => "id"
|
||||
```
|
||||
|
||||
### Misc.
|
||||
|
||||
#### `withSchema(schema: string): void`
|
||||
|
||||
_Not supported in MySQL_
|
||||
|
||||
Set the schema to use. Note: this is set on the inspector instance and only has to be done once:
|
||||
|
||||
```ts
|
||||
inspector.withSchema('my-schema');
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||
|
||||
Please make sure to update tests as appropriate.
|
||||
|
||||
### Tests
|
||||
|
||||
First start docker containers:
|
||||
|
||||
```shell
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
Then run tests:
|
||||
|
||||
```shell
|
||||
$ npm test
|
||||
```
|
||||
|
||||
Standard mocha filter (grep) can be used:
|
||||
|
||||
```shell
|
||||
$ npm test -- -g '.tableInfo'
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://choosealicense.com/licenses/mit/)
|
||||
22
packages/schema/docker-compose.yml
Normal file
22
packages/schema/docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
version: '3.1'
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: secret
|
||||
MYSQL_DATABASE: test_db
|
||||
ports:
|
||||
- 5100:3306
|
||||
volumes:
|
||||
- ./test/seed/mysql.sql:/docker-entrypoint-initdb.d/seed.sql
|
||||
postgres:
|
||||
image: postgres:12.3
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: secret
|
||||
POSTGRES_DB: test_db
|
||||
ports:
|
||||
- 5101:5432
|
||||
volumes:
|
||||
- ./test/seed/postgres.sql:/docker-entrypoint-initdb.d/seed.sql
|
||||
3476
packages/schema/package-lock.json
generated
Normal file
3476
packages/schema/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
packages/schema/package.json
Normal file
51
packages/schema/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@directus/schema",
|
||||
"version": "0.0.25",
|
||||
"description": "Utility for extracting information about existing DB schema",
|
||||
"main": "dist/lib/index.js",
|
||||
"types": "dist/lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepare": "npm run build",
|
||||
"lint": "prettier --check .",
|
||||
"test": "npm run lint && ts-mocha test/**/*.spec.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/directus/next.git"
|
||||
},
|
||||
"keywords": [
|
||||
"sql",
|
||||
"knex",
|
||||
"schema",
|
||||
"mysql",
|
||||
"postgresql",
|
||||
"sqlite3",
|
||||
"javascript"
|
||||
],
|
||||
"author": "Rijk van Zanten <rijkvanzanten@me.com>",
|
||||
"license": "GPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/directus/next/issues"
|
||||
},
|
||||
"homepage": "https://github.com/directus/next#readme",
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.14",
|
||||
"@types/lodash.flatten": "^4.4.6",
|
||||
"@types/mocha": "^8.0.3",
|
||||
"@types/node": "^14.0.13",
|
||||
"chai": "^4.2.0",
|
||||
"husky": "^4.2.5",
|
||||
"knex": "^0.21.1",
|
||||
"lint-staged": "^10.2.11",
|
||||
"mocha": "^8.2.0",
|
||||
"mysql": "^2.18.1",
|
||||
"pg": "^8.4.0",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-mocha": "^7.0.0",
|
||||
"typescript": "^3.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.flatten": "^4.4.0"
|
||||
}
|
||||
}
|
||||
284
packages/schema/src/dialects/mssql.ts
Normal file
284
packages/schema/src/dialects/mssql.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import Knex from 'knex';
|
||||
import { Schema } from '../types/schema-inspector';
|
||||
import { Table } from '../types/table';
|
||||
import { Column } from '../types/column';
|
||||
|
||||
type RawTable = {
|
||||
TABLE_NAME: string;
|
||||
TABLE_SCHEMA: string;
|
||||
TABLE_CATALOG: string;
|
||||
};
|
||||
|
||||
type RawColumn = {
|
||||
TABLE_NAME: string;
|
||||
COLUMN_NAME: string;
|
||||
COLUMN_DEFAULT: any | null;
|
||||
DATA_TYPE: string;
|
||||
CHARACTER_MAXIMUM_LENGTH: number | null;
|
||||
NUMERIC_PRECISION: number | null;
|
||||
NUMERIC_SCALE: number | null;
|
||||
IS_NULLABLE: 'YES' | 'NO';
|
||||
COLLATION_NAME: string | null;
|
||||
CONSTRAINT_TABLE_NAME: string | null;
|
||||
CONSTRAINT_COLUMN_NAME: string | null;
|
||||
EXTRA: string | null;
|
||||
UPDATE_RULE: string | null;
|
||||
DELETE_RULE: string | null;
|
||||
|
||||
/** @TODO Extend with other possible values */
|
||||
COLUMN_KEY: 'PRI' | null;
|
||||
PK_SET: 'PRIMARY' | null;
|
||||
};
|
||||
|
||||
export default class MSSQL implements Schema {
|
||||
knex: Knex;
|
||||
|
||||
constructor(knex: Knex) {
|
||||
this.knex = knex;
|
||||
}
|
||||
|
||||
// Tables
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* List all existing tables in the current schema/database
|
||||
*/
|
||||
async tables() {
|
||||
const records = await this.knex
|
||||
.select<{ TABLE_NAME: string }[]>('TABLE_NAME')
|
||||
.from('INFORMATION_SCHEMA.TABLES')
|
||||
.where({
|
||||
TABLE_TYPE: 'BASE TABLE',
|
||||
TABLE_CATALOG: this.knex.client.database(),
|
||||
});
|
||||
return records.map(({ TABLE_NAME }) => TABLE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table info for a given table. If table parameter is undefined, it will return all tables
|
||||
* in the current schema/database
|
||||
*/
|
||||
tableInfo(): Promise<Table[]>;
|
||||
tableInfo(table: string): Promise<Table>;
|
||||
async tableInfo<T>(table?: string) {
|
||||
const query = this.knex
|
||||
.select('TABLE_NAME', 'TABLE_SCHEMA', 'TABLE_CATALOG', 'TABLE_TYPE')
|
||||
.from('information_schema.tables')
|
||||
.where({
|
||||
TABLE_CATALOG: this.knex.client.database(),
|
||||
TABLE_TYPE: 'BASE TABLE',
|
||||
});
|
||||
|
||||
if (table) {
|
||||
const rawTable: RawTable = await query
|
||||
.andWhere({ table_name: table })
|
||||
.first();
|
||||
|
||||
return {
|
||||
name: rawTable.TABLE_NAME,
|
||||
schema: rawTable.TABLE_SCHEMA,
|
||||
catalog: rawTable.TABLE_CATALOG,
|
||||
} as T extends string ? Table : Table[];
|
||||
}
|
||||
|
||||
const records: RawTable[] = await query;
|
||||
|
||||
return records.map(
|
||||
(rawTable): Table => {
|
||||
return {
|
||||
name: rawTable.TABLE_NAME,
|
||||
schema: rawTable.TABLE_SCHEMA,
|
||||
catalog: rawTable.TABLE_CATALOG,
|
||||
};
|
||||
}
|
||||
) as T extends string ? Table : Table[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the current schema/database
|
||||
*/
|
||||
async hasTable(table: string): Promise<boolean> {
|
||||
const result = await this.knex
|
||||
.count<{ count: 0 | 1 }>({ count: '*' })
|
||||
.from('information_schema.tables')
|
||||
.where({ TABLE_CATALOG: this.knex.client.database(), table_name: table })
|
||||
.first();
|
||||
return (result && result.count === 1) || false;
|
||||
}
|
||||
|
||||
// Columns
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* Get all the available columns in the current schema/database. Can be filtered to a specific table
|
||||
*/
|
||||
async columns(table?: string) {
|
||||
const query = this.knex
|
||||
.select<{ TABLE_NAME: string; COLUMN_NAME: string }[]>(
|
||||
'TABLE_NAME',
|
||||
'COLUMN_NAME'
|
||||
)
|
||||
.from('INFORMATION_SCHEMA.COLUMNS')
|
||||
.where({ TABLE_CATALOG: this.knex.client.database() });
|
||||
|
||||
if (table) {
|
||||
query.andWhere({ TABLE_NAME: table });
|
||||
}
|
||||
|
||||
const records = await query;
|
||||
|
||||
return records.map(({ TABLE_NAME, COLUMN_NAME }) => ({
|
||||
table: TABLE_NAME,
|
||||
column: COLUMN_NAME,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column info for all columns, columns in a given table, or a specific column.
|
||||
*/
|
||||
columnInfo(): Promise<Column[]>;
|
||||
columnInfo(table: string): Promise<Column[]>;
|
||||
columnInfo(table: string, column: string): Promise<Column>;
|
||||
async columnInfo<T>(table?: string, column?: string) {
|
||||
const dbName = this.knex.client.database();
|
||||
const query = this.knex
|
||||
.select(
|
||||
'c.TABLE_NAME',
|
||||
'c.COLUMN_NAME',
|
||||
'c.COLUMN_DEFAULT',
|
||||
'c.DATA_TYPE',
|
||||
'c.CHARACTER_MAXIMUM_LENGTH',
|
||||
'c.NUMERIC_PRECISION',
|
||||
'c.NUMERIC_SCALE',
|
||||
'c.IS_NULLABLE',
|
||||
'c.COLLATION_NAME',
|
||||
'pk.CONSTRAINT_TABLE_NAME',
|
||||
'pk.CONSTRAINT_COLUMN_NAME',
|
||||
'pk.CONSTRAINT_NAME',
|
||||
'pk.PK_SET',
|
||||
'rc.UPDATE_RULE',
|
||||
'rc.DELETE_RULE',
|
||||
'rc.MATCH_OPTION'
|
||||
)
|
||||
.from(dbName + '.INFORMATION_SCHEMA.COLUMNS as c')
|
||||
.joinRaw(
|
||||
`left join (
|
||||
select CONSTRAINT_NAME AS CONSTRAINT_NAME, TABLE_NAME as CONSTRAINT_TABLE_NAME, COLUMN_NAME AS CONSTRAINT_COLUMN_NAME, CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, PK_SET = CASE
|
||||
WHEN CONSTRAINT_NAME like '%pk%' THEN 'PRIMARY'
|
||||
ELSE NULL
|
||||
END from ${dbName}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
) as pk
|
||||
ON [c].[TABLE_NAME] = [pk].[CONSTRAINT_TABLE_NAME]
|
||||
AND [c].[TABLE_CATALOG] = [pk].[CONSTRAINT_CATALOG]
|
||||
AND [c].[COLUMN_NAME] = [pk].[CONSTRAINT_COLUMN_NAME]
|
||||
`
|
||||
)
|
||||
.joinRaw(
|
||||
`left join (
|
||||
select CONSTRAINT_NAME,CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, MATCH_OPTION, DELETE_RULE, UPDATE_RULE from ${dbName}.INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
|
||||
|
||||
) as rc
|
||||
ON [pk].[CONSTRAINT_NAME] = [rc].[CONSTRAINT_NAME]
|
||||
AND [pk].[CONSTRAINT_CATALOG] = [rc].[CONSTRAINT_CATALOG]
|
||||
AND [pk].[CONSTRAINT_SCHEMA] = [rc].[CONSTRAINT_SCHEMA]`
|
||||
)
|
||||
.joinRaw(
|
||||
`
|
||||
LEFT JOIN
|
||||
(SELECT
|
||||
COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') as EXTRA,
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
TABLE_CATALOG
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1) AS ac
|
||||
ON [c].[TABLE_NAME] = [ac].[TABLE_NAME]
|
||||
AND [c].[TABLE_CATALOG] = [ac].[TABLE_CATALOG]
|
||||
AND [c].[COLUMN_NAME] = [ac].[COLUMN_NAME]
|
||||
`
|
||||
)
|
||||
.where({
|
||||
'c.TABLE_CATALOG': this.knex.client.database(),
|
||||
});
|
||||
|
||||
if (table) {
|
||||
query.andWhere({ 'c.TABLE_NAME': table });
|
||||
}
|
||||
|
||||
if (column) {
|
||||
const rawColumn: RawColumn = await query
|
||||
.andWhere({ 'c.column_name': column })
|
||||
.first();
|
||||
|
||||
return {
|
||||
name: rawColumn.COLUMN_NAME,
|
||||
table: rawColumn.TABLE_NAME,
|
||||
type: rawColumn.DATA_TYPE,
|
||||
default_value: parseDefault(rawColumn.COLUMN_DEFAULT),
|
||||
max_length: rawColumn.CHARACTER_MAXIMUM_LENGTH,
|
||||
precision: rawColumn.NUMERIC_PRECISION,
|
||||
scale: rawColumn.NUMERIC_SCALE,
|
||||
is_nullable: rawColumn.IS_NULLABLE === 'YES',
|
||||
is_primary_key: rawColumn.PK_SET === 'PRIMARY',
|
||||
has_auto_increment: rawColumn.EXTRA === '1',
|
||||
foreign_key_column: rawColumn.CONSTRAINT_COLUMN_NAME,
|
||||
foreign_key_table: rawColumn.CONSTRAINT_TABLE_NAME,
|
||||
} as Column;
|
||||
}
|
||||
|
||||
const records: RawColumn[] = await query;
|
||||
|
||||
return records.map(
|
||||
(rawColumn): Column => {
|
||||
return {
|
||||
name: rawColumn.COLUMN_NAME,
|
||||
table: rawColumn.TABLE_NAME,
|
||||
type: rawColumn.DATA_TYPE,
|
||||
default_value: parseDefault(rawColumn.COLUMN_DEFAULT),
|
||||
max_length: rawColumn.CHARACTER_MAXIMUM_LENGTH,
|
||||
precision: rawColumn.NUMERIC_PRECISION,
|
||||
scale: rawColumn.NUMERIC_SCALE,
|
||||
is_nullable: rawColumn.IS_NULLABLE === 'YES',
|
||||
is_primary_key: rawColumn.PK_SET === 'PRIMARY',
|
||||
has_auto_increment: rawColumn.EXTRA === '1',
|
||||
foreign_key_column: rawColumn.CONSTRAINT_COLUMN_NAME,
|
||||
foreign_key_table: rawColumn.CONSTRAINT_TABLE_NAME,
|
||||
};
|
||||
}
|
||||
) as Column[];
|
||||
|
||||
function parseDefault(value: any) {
|
||||
// MariaDB returns string NULL for not-nullable varchar fields
|
||||
if (value === 'NULL' || value === 'null') return null;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the current schema/database
|
||||
*/
|
||||
async hasColumn(table: string, column: string): Promise<boolean> {
|
||||
const { count } = this.knex
|
||||
.count<{ count: 0 | 1 }>({ count: '*' })
|
||||
.from('information_schema.tables')
|
||||
.where({
|
||||
TABLE_CATALOG: this.knex.client.database(),
|
||||
TABLE_NAME: table,
|
||||
COLUMN_NAME: column,
|
||||
})
|
||||
.first();
|
||||
return !!count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary key column for the given table
|
||||
*/
|
||||
async primary(table: string) {
|
||||
const results = await this.knex.raw(
|
||||
`SELECT Col.Column_Name from INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab, INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col WHERE Col.Constraint_Name = Tab.Constraint_Name AND Col.Table_Name = Tab.Table_Name AND Constraint_Type = 'PRIMARY KEY' AND Col.Table_Name = '${table}'`
|
||||
);
|
||||
return results[0]['Column_Name'] as string;
|
||||
}
|
||||
}
|
||||
283
packages/schema/src/dialects/mysql.ts
Normal file
283
packages/schema/src/dialects/mysql.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import Knex from 'knex';
|
||||
import { Schema } from '../types/schema-inspector';
|
||||
import { Table } from '../types/table';
|
||||
import { Column } from '../types/column';
|
||||
|
||||
type RawTable = {
|
||||
TABLE_NAME: string;
|
||||
TABLE_SCHEMA: string;
|
||||
TABLE_COMMENT: string | null;
|
||||
ENGINE: string;
|
||||
TABLE_COLLATION: string;
|
||||
};
|
||||
|
||||
type RawColumn = {
|
||||
TABLE_NAME: string;
|
||||
COLUMN_NAME: string;
|
||||
COLUMN_DEFAULT: any | null;
|
||||
DATA_TYPE: string;
|
||||
CHARACTER_MAXIMUM_LENGTH: number | null;
|
||||
NUMERIC_PRECISION: number | null;
|
||||
NUMERIC_SCALE: number | null;
|
||||
IS_NULLABLE: 'YES' | 'NO';
|
||||
COLLATION_NAME: string | null;
|
||||
COLUMN_COMMENT: string | null;
|
||||
REFERENCED_TABLE_NAME: string | null;
|
||||
REFERENCED_COLUMN_NAME: string | null;
|
||||
UPDATE_RULE: string | null;
|
||||
DELETE_RULE: string | null;
|
||||
|
||||
/** @TODO Extend with other possible values */
|
||||
COLUMN_KEY: 'PRI' | null;
|
||||
EXTRA: 'auto_increment' | null;
|
||||
CONSTRAINT_NAME: 'PRIMARY' | null;
|
||||
};
|
||||
|
||||
export default class MySQL implements Schema {
|
||||
knex: Knex;
|
||||
|
||||
constructor(knex: Knex) {
|
||||
this.knex = knex;
|
||||
}
|
||||
|
||||
// Tables
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* List all existing tables in the current schema/database
|
||||
*/
|
||||
async tables() {
|
||||
const records = await this.knex
|
||||
.select<{ TABLE_NAME: string }[]>('TABLE_NAME')
|
||||
.from('INFORMATION_SCHEMA.TABLES')
|
||||
.where({
|
||||
TABLE_TYPE: 'BASE TABLE',
|
||||
TABLE_SCHEMA: this.knex.client.database(),
|
||||
});
|
||||
return records.map(({ TABLE_NAME }) => TABLE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table info for a given table. If table parameter is undefined, it will return all tables
|
||||
* in the current schema/database
|
||||
*/
|
||||
tableInfo(): Promise<Table[]>;
|
||||
tableInfo(table: string): Promise<Table>;
|
||||
async tableInfo<T>(table?: string) {
|
||||
const query = this.knex
|
||||
.select(
|
||||
'TABLE_NAME',
|
||||
'ENGINE',
|
||||
'TABLE_SCHEMA',
|
||||
'TABLE_COLLATION',
|
||||
'TABLE_COMMENT'
|
||||
)
|
||||
.from('information_schema.tables')
|
||||
.where({
|
||||
table_schema: this.knex.client.database(),
|
||||
table_type: 'BASE TABLE',
|
||||
});
|
||||
|
||||
if (table) {
|
||||
const rawTable: RawTable = await query
|
||||
.andWhere({ table_name: table })
|
||||
.first();
|
||||
|
||||
return {
|
||||
name: rawTable.TABLE_NAME,
|
||||
schema: rawTable.TABLE_SCHEMA,
|
||||
comment: rawTable.TABLE_COMMENT,
|
||||
collation: rawTable.TABLE_COLLATION,
|
||||
engine: rawTable.ENGINE,
|
||||
} as T extends string ? Table : Table[];
|
||||
}
|
||||
|
||||
const records: RawTable[] = await query;
|
||||
|
||||
return records.map(
|
||||
(rawTable): Table => {
|
||||
return {
|
||||
name: rawTable.TABLE_NAME,
|
||||
schema: rawTable.TABLE_SCHEMA,
|
||||
comment: rawTable.TABLE_COMMENT,
|
||||
collation: rawTable.TABLE_COLLATION,
|
||||
engine: rawTable.ENGINE,
|
||||
};
|
||||
}
|
||||
) as T extends string ? Table : Table[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the current schema/database
|
||||
*/
|
||||
async hasTable(table: string): Promise<boolean> {
|
||||
const result = await this.knex
|
||||
.count<{ count: 0 | 1 }>({ count: '*' })
|
||||
.from('information_schema.tables')
|
||||
.where({ table_schema: this.knex.client.database(), table_name: table })
|
||||
.first();
|
||||
return (result && result.count === 1) || false;
|
||||
}
|
||||
|
||||
// Columns
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* Get all the available columns in the current schema/database. Can be filtered to a specific table
|
||||
*/
|
||||
async columns(table?: string) {
|
||||
const query = this.knex
|
||||
.select<{ TABLE_NAME: string; COLUMN_NAME: string }[]>(
|
||||
'TABLE_NAME',
|
||||
'COLUMN_NAME'
|
||||
)
|
||||
.from('INFORMATION_SCHEMA.COLUMNS')
|
||||
.where({ TABLE_SCHEMA: this.knex.client.database() });
|
||||
|
||||
if (table) {
|
||||
query.andWhere({ TABLE_NAME: table });
|
||||
}
|
||||
|
||||
const records = await query;
|
||||
|
||||
return records.map(({ TABLE_NAME, COLUMN_NAME }) => ({
|
||||
table: TABLE_NAME,
|
||||
column: COLUMN_NAME,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column info for all columns, columns in a given table, or a specific column.
|
||||
*/
|
||||
columnInfo(): Promise<Column[]>;
|
||||
columnInfo(table: string): Promise<Column[]>;
|
||||
columnInfo(table: string, column: string): Promise<Column>;
|
||||
async columnInfo<T>(table?: string, column?: string) {
|
||||
const query = this.knex
|
||||
.select(
|
||||
'c.TABLE_NAME',
|
||||
'c.COLUMN_NAME',
|
||||
'c.COLUMN_DEFAULT',
|
||||
'c.DATA_TYPE',
|
||||
'c.CHARACTER_MAXIMUM_LENGTH',
|
||||
'c.IS_NULLABLE',
|
||||
'c.COLUMN_KEY',
|
||||
'c.EXTRA',
|
||||
'c.COLLATION_NAME',
|
||||
'c.COLUMN_COMMENT',
|
||||
'c.NUMERIC_PRECISION',
|
||||
'c.NUMERIC_SCALE',
|
||||
'fk.REFERENCED_TABLE_NAME',
|
||||
'fk.REFERENCED_COLUMN_NAME',
|
||||
'fk.CONSTRAINT_NAME',
|
||||
'rc.UPDATE_RULE',
|
||||
'rc.DELETE_RULE',
|
||||
'rc.MATCH_OPTION'
|
||||
)
|
||||
.from('INFORMATION_SCHEMA.COLUMNS as c')
|
||||
.leftJoin('INFORMATION_SCHEMA.KEY_COLUMN_USAGE as fk', function () {
|
||||
this.on('c.TABLE_NAME', '=', 'fk.TABLE_NAME')
|
||||
.andOn('fk.COLUMN_NAME', '=', 'c.COLUMN_NAME')
|
||||
.andOn('fk.CONSTRAINT_SCHEMA', '=', 'c.TABLE_SCHEMA');
|
||||
})
|
||||
.leftJoin(
|
||||
'INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS as rc',
|
||||
function () {
|
||||
this.on('rc.TABLE_NAME', '=', 'fk.TABLE_NAME')
|
||||
.andOn('rc.CONSTRAINT_NAME', '=', 'fk.CONSTRAINT_NAME')
|
||||
.andOn('rc.CONSTRAINT_SCHEMA', '=', 'fk.CONSTRAINT_SCHEMA');
|
||||
}
|
||||
)
|
||||
.where({
|
||||
'c.TABLE_SCHEMA': this.knex.client.database(),
|
||||
});
|
||||
|
||||
if (table) {
|
||||
query.andWhere({ 'c.TABLE_NAME': table });
|
||||
}
|
||||
|
||||
if (column) {
|
||||
const rawColumn: RawColumn = await query
|
||||
.andWhere({ 'c.column_name': column })
|
||||
.first();
|
||||
|
||||
return {
|
||||
name: rawColumn.COLUMN_NAME,
|
||||
table: rawColumn.TABLE_NAME,
|
||||
type: rawColumn.DATA_TYPE,
|
||||
default_value: parseDefault(rawColumn.COLUMN_DEFAULT),
|
||||
max_length: rawColumn.CHARACTER_MAXIMUM_LENGTH,
|
||||
precision: rawColumn.NUMERIC_PRECISION,
|
||||
scale: rawColumn.NUMERIC_SCALE,
|
||||
is_nullable: rawColumn.IS_NULLABLE === 'YES',
|
||||
is_primary_key: rawColumn.CONSTRAINT_NAME === 'PRIMARY',
|
||||
has_auto_increment: rawColumn.EXTRA === 'auto_increment',
|
||||
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
|
||||
foreign_key_table: rawColumn.REFERENCED_TABLE_NAME,
|
||||
comment: rawColumn.COLUMN_COMMENT,
|
||||
// onDelete: rawColumn.DELETE_RULE,
|
||||
// onUpdate: rawColumn.UPDATE_RULE,
|
||||
} as Column;
|
||||
}
|
||||
|
||||
const records: RawColumn[] = await query;
|
||||
|
||||
return records.map(
|
||||
(rawColumn): Column => {
|
||||
return {
|
||||
name: rawColumn.COLUMN_NAME,
|
||||
table: rawColumn.TABLE_NAME,
|
||||
type: rawColumn.DATA_TYPE,
|
||||
default_value: parseDefault(rawColumn.COLUMN_DEFAULT),
|
||||
max_length: rawColumn.CHARACTER_MAXIMUM_LENGTH,
|
||||
precision: rawColumn.NUMERIC_PRECISION,
|
||||
scale: rawColumn.NUMERIC_SCALE,
|
||||
is_nullable: rawColumn.IS_NULLABLE === 'YES',
|
||||
is_primary_key: rawColumn.CONSTRAINT_NAME === 'PRIMARY',
|
||||
has_auto_increment: rawColumn.EXTRA === 'auto_increment',
|
||||
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
|
||||
foreign_key_table: rawColumn.REFERENCED_TABLE_NAME,
|
||||
comment: rawColumn.COLUMN_COMMENT,
|
||||
// onDelete: rawColumn.DELETE_RULE,
|
||||
// onUpdate: rawColumn.UPDATE_RULE,
|
||||
};
|
||||
}
|
||||
) as Column[];
|
||||
|
||||
function parseDefault(value: any) {
|
||||
// MariaDB returns string NULL for not-nullable varchar fields
|
||||
if (value === 'NULL' || value === 'null') return null;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the current schema/database
|
||||
*/
|
||||
async hasColumn(table: string, column: string): Promise<boolean> {
|
||||
const result = await this.knex
|
||||
.count<{ count: 0 | 1 }>('*', { as: 'count' })
|
||||
.from('information_schema.columns')
|
||||
.where({
|
||||
table_schema: this.knex.client.database(),
|
||||
table_name: table,
|
||||
column_name: column,
|
||||
})
|
||||
.first();
|
||||
return !!(result && result.count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary key column for the given table
|
||||
*/
|
||||
async primary(table: string) {
|
||||
const results = await this.knex.raw(
|
||||
`SHOW KEYS FROM ?? WHERE Key_name = 'PRIMARY'`,
|
||||
table
|
||||
);
|
||||
if (results.length && results[0].length) {
|
||||
return results[0][0]['Column_name'] as string;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
252
packages/schema/src/dialects/oracledb.ts
Normal file
252
packages/schema/src/dialects/oracledb.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import Knex from 'knex';
|
||||
import { Schema } from '../types/schema-inspector';
|
||||
import { Table } from '../types/table';
|
||||
import { Column } from '../types/column';
|
||||
|
||||
type RawTable = {
|
||||
TABLE_NAME: string;
|
||||
SCHEMA_NAME: string;
|
||||
};
|
||||
|
||||
type RawColumn = {
|
||||
TABLE_NAME: string;
|
||||
COLUMN_NAME: string;
|
||||
DATA_DEFAULT: any | null;
|
||||
DATA_TYPE: string;
|
||||
DATA_LENGTH: number | null;
|
||||
DATA_PRECISION: number | null;
|
||||
DATA_SCALE: number | null;
|
||||
NULLABLE: 'YES' | 'NO';
|
||||
COLUMN_COMMENT: string | null;
|
||||
REFERENCED_TABLE_NAME: string | null;
|
||||
REFERENCED_COLUMN_NAME: string | null;
|
||||
UPDATE_RULE: string | null;
|
||||
DELETE_RULE: string | null;
|
||||
|
||||
/** @TODO Extend with other possible values */
|
||||
COLUMN_KEY: 'PRI' | null;
|
||||
CONTRAINT_NAME: string | null;
|
||||
CONSTRAINT_TYPE: 'P' | null;
|
||||
};
|
||||
|
||||
export default class oracleDB implements Schema {
|
||||
knex: Knex;
|
||||
|
||||
constructor(knex: Knex) {
|
||||
this.knex = knex;
|
||||
}
|
||||
|
||||
// Tables
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* List all existing tables in the current schema/database
|
||||
*/
|
||||
async tables() {
|
||||
const records = await this.knex
|
||||
.select<{ TABLE_NAME: string }[]>('TABLE_NAME')
|
||||
.from('DBA_TABLES');
|
||||
return records.map(({ TABLE_NAME }) => TABLE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table info for a given table. If table parameter is undefined, it will return all tables
|
||||
* in the current schema/database
|
||||
*/
|
||||
tableInfo(): Promise<Table[]>;
|
||||
tableInfo(table: string): Promise<Table>;
|
||||
async tableInfo<T>(table?: string) {
|
||||
const query = this.knex.select('TABLE_NAME', 'OWNER').from('DBA_TABLES');
|
||||
|
||||
if (table) {
|
||||
const rawTable: RawTable = await query
|
||||
.andWhere({ TABLE_NAME: table })
|
||||
.first();
|
||||
|
||||
return {
|
||||
name: rawTable.TABLE_NAME,
|
||||
schema: rawTable.SCHEMA_NAME,
|
||||
} as T extends string ? Table : Table[];
|
||||
}
|
||||
|
||||
const records: RawTable[] = await query;
|
||||
|
||||
return records.map(
|
||||
(rawTable): Table => {
|
||||
return {
|
||||
name: rawTable.TABLE_NAME,
|
||||
schema: rawTable.SCHEMA_NAME,
|
||||
};
|
||||
}
|
||||
) as T extends string ? Table : Table[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the current schema/database
|
||||
*/
|
||||
async hasTable(table: string): Promise<boolean> {
|
||||
const result = await this.knex
|
||||
.count<{ count: 0 | 1 }>({ count: '*' })
|
||||
.from('DBA_TABLES')
|
||||
.where({ TABLE_NAME: table })
|
||||
.first();
|
||||
return (result && result.count === 1) || false;
|
||||
}
|
||||
|
||||
// Columns
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* Get all the available columns in the current schema/database. Can be filtered to a specific table
|
||||
*/
|
||||
async columns(table?: string) {
|
||||
const query = this.knex
|
||||
.select<{ TABLE_NAME: string; COLUMN_NAME: string }[]>(
|
||||
'TABLE_NAME',
|
||||
'COLUMN_NAME'
|
||||
)
|
||||
.from('DBA_TAB_COLUMNS');
|
||||
|
||||
if (table) {
|
||||
query.andWhere({ TABLE_NAME: table });
|
||||
}
|
||||
|
||||
const records = await query;
|
||||
|
||||
return records.map(({ TABLE_NAME, COLUMN_NAME }) => ({
|
||||
table: TABLE_NAME,
|
||||
column: COLUMN_NAME,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column info for all columns, columns in a given table, or a specific column.
|
||||
*/
|
||||
columnInfo(): Promise<Column[]>;
|
||||
columnInfo(table: string): Promise<Column[]>;
|
||||
columnInfo(table: string, column: string): Promise<Column>;
|
||||
async columnInfo<T>(table?: string, column?: string) {
|
||||
const query = this.knex
|
||||
.select(
|
||||
'c.TABLE_NAME',
|
||||
'c.COLUMN_NAME',
|
||||
'c.DATA_DEFAULT',
|
||||
'c.DATA_TYPE',
|
||||
'c.DATA_LENGTH',
|
||||
'c.DATA_PRECISION',
|
||||
'c.DATA_SCALE',
|
||||
'c.NULLABLE',
|
||||
'pk.CONSTRAINT_NAME',
|
||||
'pk.CONSTRAINT_TYPE',
|
||||
'cm.COMMENTS AS COLUMN_COMMENT',
|
||||
'fk.TABLE_NAME as REFERENCE_TABLE_NAME',
|
||||
'fk.COLUMN_NAME as REFERENCED_COLUMN_NAME',
|
||||
'rc.DELETE_RULE',
|
||||
'rc.SEARCH_CONDITION'
|
||||
)
|
||||
.from('DBA_TAB_COLUMNS as c')
|
||||
.leftJoin('DBA_COL_COMMENTS as cm', function () {
|
||||
this.on('c.TABLE_NAME', '=', 'cm.TABLE_NAME')
|
||||
.andOn('cm.COLUMN_NAME', '=', 'c.COLUMN_NAME')
|
||||
.andOn('cm.OWNER', '=', 'c.OWNER');
|
||||
})
|
||||
.leftJoin('all_constraints as pk', function () {
|
||||
this.on('c.TABLE_NAME', '=', 'pk.TABLE_NAME')
|
||||
.andOn('c.CONSTRAINT_NAME', '=', 'pk.CONSTRAINT_NAME')
|
||||
.andOn('c.OWNER', '=', 'pk.OWNER');
|
||||
})
|
||||
.where({ 'pk.CONSTRAINT_TYPE': 'P' })
|
||||
.leftJoin('all_constraints as fk', function () {
|
||||
this.on('c.TABLE_NAME', '=', 'fk.TABLE_NAME')
|
||||
.andOn('c.CONSTRAINT_NAME', '=', 'fk.CONSTRAINT_NAME')
|
||||
.andOn('c.OWNER', '=', 'fk.OWNER');
|
||||
})
|
||||
.where({ 'fk.CONSTRAINT_TYPE': 'R' })
|
||||
.leftJoin('all_constraints as rc', function () {
|
||||
this.on('c.TABLE_NAME', '=', 'rc.TABLE_NAME')
|
||||
.andOn('c.CONSTRAINT_NAME', '=', 'rc.CONSTRAINT_NAME')
|
||||
.andOn('c.OWNER', '=', 'rc.OWNER');
|
||||
});
|
||||
|
||||
if (table) {
|
||||
query.andWhere({ 'c.TABLE_NAME': table });
|
||||
}
|
||||
|
||||
if (column) {
|
||||
const rawColumn: RawColumn = await query
|
||||
.andWhere({ 'c.column_name': column })
|
||||
.first();
|
||||
|
||||
return {
|
||||
name: rawColumn.COLUMN_NAME,
|
||||
table: rawColumn.TABLE_NAME,
|
||||
type: rawColumn.DATA_TYPE,
|
||||
default_value: rawColumn.DATA_DEFAULT,
|
||||
max_length: rawColumn.DATA_LENGTH,
|
||||
precision: rawColumn.DATA_PRECISION,
|
||||
scale: rawColumn.DATA_SCALE,
|
||||
is_nullable: rawColumn.NULLABLE === 'YES',
|
||||
is_primary_key: rawColumn.CONSTRAINT_TYPE === 'P',
|
||||
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
|
||||
foreign_key_table: rawColumn.REFERENCED_TABLE_NAME,
|
||||
comment: rawColumn.COLUMN_COMMENT,
|
||||
} as Column;
|
||||
}
|
||||
|
||||
const records: RawColumn[] = await query;
|
||||
|
||||
return records.map(
|
||||
(rawColumn): Column => {
|
||||
return {
|
||||
name: rawColumn.COLUMN_NAME,
|
||||
table: rawColumn.TABLE_NAME,
|
||||
type: rawColumn.DATA_TYPE,
|
||||
default_value: rawColumn.DATA_DEFAULT,
|
||||
max_length: rawColumn.DATA_DEFAULT,
|
||||
precision: rawColumn.DATA_PRECISION,
|
||||
scale: rawColumn.DATA_SCALE,
|
||||
is_nullable: rawColumn.NULLABLE === 'YES',
|
||||
is_primary_key: rawColumn.CONSTRAINT_TYPE === 'P',
|
||||
has_auto_increment: rawColumn.DATA_DEFAULT,
|
||||
foreign_key_column: rawColumn.REFERENCED_COLUMN_NAME,
|
||||
foreign_key_table: rawColumn.REFERENCED_TABLE_NAME,
|
||||
comment: rawColumn.COLUMN_COMMENT,
|
||||
};
|
||||
}
|
||||
) as Column[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the current schema/database
|
||||
*/
|
||||
async hasColumn(table: string, column: string): Promise<boolean> {
|
||||
const { count } = this.knex
|
||||
.count<{ count: 0 | 1 }>({ count: '*' })
|
||||
.from('DBA_TAB_COLUMNS')
|
||||
.where({
|
||||
table_schema: this.knex.client.database(),
|
||||
table_name: table,
|
||||
column_name: column,
|
||||
})
|
||||
.first();
|
||||
return !!count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary key column for the given table
|
||||
*/
|
||||
|
||||
async primary(table: string): Promise<string> {
|
||||
const { column_name } = await this.knex
|
||||
.select('all_constraints.column_name')
|
||||
.from('all_constraints')
|
||||
|
||||
.where({
|
||||
'all_constraints.CONSTRAINT_TYPE': 'P',
|
||||
'all_constraints.TABLE_NAME': table,
|
||||
})
|
||||
.first();
|
||||
|
||||
return column_name;
|
||||
}
|
||||
}
|
||||
372
packages/schema/src/dialects/postgres.ts
Normal file
372
packages/schema/src/dialects/postgres.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import Knex from 'knex';
|
||||
import { Schema } from '../types/schema-inspector';
|
||||
import { Table } from '../types/table';
|
||||
import { Column } from '../types/column';
|
||||
|
||||
type RawTable = {
|
||||
table_name: string;
|
||||
table_schema: 'public' | string;
|
||||
table_comment: string | null;
|
||||
};
|
||||
|
||||
type RawColumn = {
|
||||
column_name: string;
|
||||
table_name: string;
|
||||
table_schema: string;
|
||||
data_type: string;
|
||||
column_default: any | null;
|
||||
character_maximum_length: number | null;
|
||||
is_nullable: 'YES' | 'NO';
|
||||
is_primary: null | 'YES';
|
||||
numeric_precision: null | number;
|
||||
numeric_scale: null | number;
|
||||
serial: null | string;
|
||||
column_comment: string | null;
|
||||
referenced_table_schema: null | string;
|
||||
referenced_table_name: null | string;
|
||||
referenced_column_name: null | string;
|
||||
};
|
||||
|
||||
export default class Postgres implements Schema {
|
||||
knex: Knex;
|
||||
schema: string;
|
||||
explodedSchema: string[];
|
||||
|
||||
constructor(knex: Knex) {
|
||||
this.knex = knex;
|
||||
const config = knex.client.config;
|
||||
if (!config.searchPath) {
|
||||
this.schema = 'public';
|
||||
this.explodedSchema = [this.schema];
|
||||
} else if (typeof config.searchPath === 'string') {
|
||||
this.schema = config.searchPath;
|
||||
this.explodedSchema = [config.searchPath];
|
||||
} else {
|
||||
this.schema = config.searchPath[0];
|
||||
this.explodedSchema = config.searchPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Postgres specific
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* Set the schema to be used in other methods
|
||||
*/
|
||||
withSchema(schema: string) {
|
||||
this.schema = schema;
|
||||
this.explodedSchema = [this.schema];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Postgres default value to JS
|
||||
* Eg `'example'::character varying` => `example`
|
||||
*/
|
||||
parseDefaultValue(type: string) {
|
||||
if (type.startsWith('nextval(')) return null; // auto-increment
|
||||
|
||||
const parts = type.split('::');
|
||||
|
||||
let value = parts[0];
|
||||
|
||||
if (value.startsWith("'") && value.endsWith("'")) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
if (parts[1] && parts[1].includes('json')) return JSON.parse(value);
|
||||
if (parts[1] && (parts[1].includes('char') || parts[1].includes('text')))
|
||||
return String(value);
|
||||
|
||||
if (Number.isNaN(Number(value))) return value;
|
||||
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
// Tables
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* List all existing tables in the current schema/database
|
||||
*/
|
||||
async tables() {
|
||||
const records = await this.knex
|
||||
.select<{ tablename: string }[]>('tablename')
|
||||
.from('pg_catalog.pg_tables')
|
||||
.whereIn('schemaname', this.explodedSchema);
|
||||
return records.map(({ tablename }) => tablename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table info for a given table. If table parameter is undefined, it will return all tables
|
||||
* in the current schema/database
|
||||
*/
|
||||
tableInfo(): Promise<Table[]>;
|
||||
tableInfo(table: string): Promise<Table>;
|
||||
async tableInfo(table?: string) {
|
||||
const query = this.knex
|
||||
.select(
|
||||
'table_name',
|
||||
'table_schema',
|
||||
this.knex
|
||||
.select(this.knex.raw('obj_description(oid)'))
|
||||
.from('pg_class')
|
||||
.where({ relkind: 'r' })
|
||||
.andWhere({ relname: 'table_name ' })
|
||||
.as('table_comment')
|
||||
)
|
||||
.from('information_schema.tables')
|
||||
.whereIn('table_schema', this.explodedSchema)
|
||||
.andWhere({ table_catalog: this.knex.client.database() })
|
||||
.andWhere({ table_type: 'BASE TABLE' })
|
||||
.orderBy('table_name', 'asc');
|
||||
|
||||
if (table) {
|
||||
const rawTable: RawTable = await query
|
||||
.andWhere({ table_name: table })
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
return {
|
||||
name: rawTable.table_name,
|
||||
schema: rawTable.table_schema,
|
||||
comment: rawTable.table_comment,
|
||||
} as Table;
|
||||
}
|
||||
|
||||
const records = await query;
|
||||
|
||||
return records.map(
|
||||
(rawTable: RawTable): Table => {
|
||||
return {
|
||||
name: rawTable.table_name,
|
||||
schema: rawTable.table_schema,
|
||||
comment: rawTable.table_comment,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the current schema/database
|
||||
*/
|
||||
async hasTable(table: string) {
|
||||
const subquery = this.knex
|
||||
.select()
|
||||
.from('information_schema.tables')
|
||||
.whereIn('table_schema', this.explodedSchema)
|
||||
.andWhere({ table_name: table });
|
||||
const record = await this.knex
|
||||
.select<{ exists: boolean }>(this.knex.raw('exists (?)', [subquery]))
|
||||
.first();
|
||||
return record?.exists || false;
|
||||
}
|
||||
|
||||
// Columns
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* Get all the available columns in the current schema/database. Can be filtered to a specific table
|
||||
*/
|
||||
async columns(table?: string) {
|
||||
const query = this.knex
|
||||
.select<{ table_name: string; column_name: string }[]>(
|
||||
'table_name',
|
||||
'column_name'
|
||||
)
|
||||
.from('information_schema.columns')
|
||||
.whereIn('table_schema', this.explodedSchema);
|
||||
|
||||
if (table) {
|
||||
query.andWhere({ table_name: table });
|
||||
}
|
||||
|
||||
const records = await query;
|
||||
|
||||
return records.map(({ table_name, column_name }) => ({
|
||||
table: table_name,
|
||||
column: column_name,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column info for all columns, columns in a given table, or a specific column.
|
||||
*/
|
||||
columnInfo(): Promise<Column[]>;
|
||||
columnInfo(table: string): Promise<Column[]>;
|
||||
columnInfo(table: string, column: string): Promise<Column>;
|
||||
async columnInfo<T>(table?: string, column?: string) {
|
||||
const { knex } = this;
|
||||
|
||||
const query = knex
|
||||
.select(
|
||||
'c.column_name',
|
||||
'c.table_name',
|
||||
'c.data_type',
|
||||
'c.column_default',
|
||||
'c.character_maximum_length',
|
||||
'c.is_nullable',
|
||||
'c.numeric_precision',
|
||||
'c.numeric_scale',
|
||||
'c.table_schema',
|
||||
|
||||
knex
|
||||
.select(knex.raw(`'YES'`))
|
||||
.from('pg_index')
|
||||
.join('pg_attribute', function () {
|
||||
this.on('pg_attribute.attrelid', '=', 'pg_index.indrelid').andOn(
|
||||
knex.raw('pg_attribute.attnum = any(pg_index.indkey)')
|
||||
);
|
||||
})
|
||||
.whereRaw('pg_index.indrelid = c.table_name::regclass')
|
||||
.andWhere(knex.raw('pg_attribute.attname = c.column_name'))
|
||||
.andWhere(knex.raw('pg_index.indisprimary'))
|
||||
.as('is_primary'),
|
||||
|
||||
knex
|
||||
.select(
|
||||
knex.raw(
|
||||
'pg_catalog.col_description(pg_catalog.pg_class.oid, c.ordinal_position:: int)'
|
||||
)
|
||||
)
|
||||
.from('pg_catalog.pg_class')
|
||||
.whereRaw(
|
||||
`pg_catalog.pg_class.oid = (select('"' || c.table_name || '"'):: regclass:: oid)`
|
||||
)
|
||||
.andWhere({ 'pg_catalog.pg_class.relname': 'c.table_name' })
|
||||
.as('column_comment'),
|
||||
|
||||
knex.raw(
|
||||
'pg_get_serial_sequence(c.table_name, c.column_name) as serial'
|
||||
),
|
||||
|
||||
'ffk.referenced_table_schema',
|
||||
'ffk.referenced_table_name',
|
||||
'ffk.referenced_column_name'
|
||||
)
|
||||
.from(knex.raw('information_schema.columns c'))
|
||||
.joinRaw(
|
||||
`
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
k1.table_schema,
|
||||
k1.table_name,
|
||||
k1.column_name,
|
||||
k2.table_schema AS referenced_table_schema,
|
||||
k2.table_name AS referenced_table_name,
|
||||
k2.column_name AS referenced_column_name
|
||||
FROM
|
||||
information_schema.key_column_usage k1
|
||||
JOIN information_schema.referential_constraints fk using (
|
||||
constraint_schema, constraint_name
|
||||
)
|
||||
JOIN information_schema.key_column_usage k2 ON k2.constraint_schema = fk.unique_constraint_schema
|
||||
AND k2.constraint_name = fk.unique_constraint_name
|
||||
AND k2.ordinal_position = k1.position_in_unique_constraint
|
||||
) ffk ON ffk.table_name = c.table_name
|
||||
AND ffk.column_name = c.column_name
|
||||
`
|
||||
)
|
||||
.whereIn('c.table_schema', this.explodedSchema);
|
||||
|
||||
if (table) {
|
||||
query.andWhere({ 'c.table_name': table });
|
||||
}
|
||||
|
||||
if (column) {
|
||||
const rawColumn = await query
|
||||
.andWhere({ 'c.column_name': column })
|
||||
.first();
|
||||
|
||||
return {
|
||||
name: rawColumn.column_name,
|
||||
table: rawColumn.table_name,
|
||||
type: rawColumn.data_type,
|
||||
default_value: rawColumn.column_default
|
||||
? this.parseDefaultValue(rawColumn.column_default)
|
||||
: null,
|
||||
max_length: rawColumn.character_maximum_length,
|
||||
precision: rawColumn.numeric_precision,
|
||||
scale: rawColumn.numeric_scale,
|
||||
is_nullable: rawColumn.is_nullable === 'YES',
|
||||
is_primary_key: rawColumn.is_primary === 'YES',
|
||||
has_auto_increment: rawColumn.serial !== null,
|
||||
foreign_key_column: rawColumn.referenced_column_name,
|
||||
foreign_key_table: rawColumn.referenced_table_name,
|
||||
comment: rawColumn.column_comment,
|
||||
schema: rawColumn.table_schema,
|
||||
foreign_key_schema: rawColumn.referenced_table_schema,
|
||||
} as T extends string ? Column : Column[];
|
||||
}
|
||||
|
||||
const records: RawColumn[] = await query;
|
||||
|
||||
return records.map(
|
||||
(rawColumn): Column => {
|
||||
return {
|
||||
name: rawColumn.column_name,
|
||||
table: rawColumn.table_name,
|
||||
type: rawColumn.data_type,
|
||||
default_value: rawColumn.column_default
|
||||
? this.parseDefaultValue(rawColumn.column_default)
|
||||
: null,
|
||||
max_length: rawColumn.character_maximum_length,
|
||||
precision: rawColumn.numeric_precision,
|
||||
scale: rawColumn.numeric_scale,
|
||||
is_nullable: rawColumn.is_nullable === 'YES',
|
||||
is_primary_key: rawColumn.is_primary === 'YES',
|
||||
has_auto_increment: rawColumn.serial !== null,
|
||||
foreign_key_column: rawColumn.referenced_column_name,
|
||||
foreign_key_table: rawColumn.referenced_table_name,
|
||||
|
||||
comment: rawColumn.column_comment,
|
||||
schema: rawColumn.table_schema,
|
||||
foreign_key_schema: rawColumn.referenced_table_schema,
|
||||
};
|
||||
}
|
||||
) as T extends string ? Column : Column[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given table contains the given column
|
||||
*/
|
||||
async hasColumn(table: string, column: string) {
|
||||
const subquery = this.knex
|
||||
.select()
|
||||
.from('information_schema.columns')
|
||||
.whereIn('table_schema', this.explodedSchema)
|
||||
.andWhere({
|
||||
table_name: table,
|
||||
column_name: column,
|
||||
});
|
||||
const record = await this.knex
|
||||
.select<{ exists: boolean }>(this.knex.raw('exists (?)', [subquery]))
|
||||
.first();
|
||||
return record?.exists || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary key column for the given table
|
||||
*/
|
||||
async primary(table: string): Promise<string> {
|
||||
const result = await this.knex
|
||||
.select('information_schema.key_column_usage.column_name')
|
||||
.from('information_schema.key_column_usage')
|
||||
.leftJoin(
|
||||
'information_schema.table_constraints',
|
||||
'information_schema.table_constraints.constraint_name',
|
||||
'information_schema.key_column_usage.constraint_name'
|
||||
)
|
||||
.whereIn(
|
||||
'information_schema.table_constraints.table_schema',
|
||||
this.explodedSchema
|
||||
)
|
||||
.andWhere({
|
||||
'information_schema.table_constraints.constraint_type': 'PRIMARY KEY',
|
||||
'information_schema.table_constraints.table_name': table,
|
||||
})
|
||||
.first();
|
||||
|
||||
return result ? result.column_name : null;
|
||||
}
|
||||
}
|
||||
193
packages/schema/src/dialects/sqlite.ts
Normal file
193
packages/schema/src/dialects/sqlite.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import Knex from 'knex';
|
||||
import flatten from 'lodash.flatten';
|
||||
import { Schema } from '../types/schema-inspector';
|
||||
import { Table } from '../types/table';
|
||||
import { Column } from '../types/column';
|
||||
import extractMaxLength from '../utils/extract-max-length';
|
||||
|
||||
type RawColumn = {
|
||||
cid: number;
|
||||
name: string;
|
||||
type: string;
|
||||
notnull: 0 | 1;
|
||||
dflt_value: any;
|
||||
pk: 0 | 1;
|
||||
};
|
||||
|
||||
export default class SQLite implements Schema {
|
||||
knex: Knex;
|
||||
|
||||
constructor(knex: Knex) {
|
||||
this.knex = knex;
|
||||
}
|
||||
|
||||
// Tables
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* List all existing tables in the current schema/database
|
||||
*/
|
||||
async tables(): Promise<string[]> {
|
||||
const records = await this.knex
|
||||
.select('name')
|
||||
.from('sqlite_master')
|
||||
.whereRaw(`type = 'table' AND name NOT LIKE 'sqlite_%'`);
|
||||
return records.map(({ name }) => name) as string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table info for a given table. If table parameter is undefined, it will return all tables
|
||||
* in the current schema/database
|
||||
*/
|
||||
tableInfo(): Promise<Table[]>;
|
||||
tableInfo(table: string): Promise<Table>;
|
||||
async tableInfo(table?: string) {
|
||||
const query = this.knex
|
||||
.select('name', 'sql')
|
||||
.from('sqlite_master')
|
||||
.where({ type: 'table' })
|
||||
.andWhereRaw(`name NOT LIKE 'sqlite_%'`);
|
||||
|
||||
if (table) {
|
||||
query.andWhere({ name: table });
|
||||
}
|
||||
|
||||
let records = await query;
|
||||
|
||||
records = records.map((table) => ({
|
||||
name: table.name,
|
||||
sql: table.sql,
|
||||
}));
|
||||
|
||||
if (table) {
|
||||
return records[0];
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the current schema/database
|
||||
*/
|
||||
async hasTable(table: string): Promise<boolean> {
|
||||
const results = await this.knex
|
||||
.select(1)
|
||||
.from('sqlite_master')
|
||||
.where({ type: 'table', name: table });
|
||||
return results.length > 0;
|
||||
}
|
||||
|
||||
// Columns
|
||||
// ===============================================================================================
|
||||
|
||||
/**
|
||||
* Get all the available columns in the current schema/database. Can be filtered to a specific table
|
||||
*/
|
||||
async columns(table?: string): Promise<{ table: string; column: string }[]> {
|
||||
if (table) {
|
||||
const columns = await this.knex.raw<RawColumn[]>(
|
||||
`PRAGMA table_info(??)`,
|
||||
table
|
||||
);
|
||||
return columns.map((column) => ({ table, column: column.name }));
|
||||
}
|
||||
|
||||
const tables = await this.tables();
|
||||
const columnsPerTable = await Promise.all(
|
||||
tables.map(async (table) => await this.columns(table))
|
||||
);
|
||||
return flatten(columnsPerTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column info for all columns, columns in a given table, or a specific column.
|
||||
*/
|
||||
columnInfo(): Promise<Column[]>;
|
||||
columnInfo(table: string): Promise<Column[]>;
|
||||
columnInfo(table: string, column: string): Promise<Column>;
|
||||
async columnInfo(table?: string, column?: string) {
|
||||
const getColumnsForTable = async (table: string): Promise<Column[]> => {
|
||||
const tablesWithAutoIncrementPrimaryKeys = (
|
||||
await this.knex
|
||||
.select('name')
|
||||
.from('sqlite_master')
|
||||
.whereRaw(`sql LIKE "%AUTOINCREMENT%"`)
|
||||
).map(({ name }) => name);
|
||||
|
||||
const columns: RawColumn[] = await this.knex.raw(
|
||||
`PRAGMA table_info(??)`,
|
||||
table
|
||||
);
|
||||
|
||||
const foreignKeys = await this.knex.raw<
|
||||
{ table: string; from: string; to: string }[]
|
||||
>(`PRAGMA foreign_key_list(??)`, table);
|
||||
|
||||
return columns.map(
|
||||
(raw): Column => {
|
||||
const foreignKey = foreignKeys.find((fk) => fk.from === raw.name);
|
||||
|
||||
return {
|
||||
name: raw.name,
|
||||
table: table,
|
||||
type: raw.type,
|
||||
default_value: raw.dflt_value,
|
||||
max_length: extractMaxLength(raw.dflt_value),
|
||||
/** @NOTE SQLite3 doesn't support precision/scale */
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: raw.notnull === 0,
|
||||
is_primary_key: raw.pk === 1,
|
||||
has_auto_increment:
|
||||
raw.pk === 1 &&
|
||||
tablesWithAutoIncrementPrimaryKeys.includes(table),
|
||||
foreign_key_column: foreignKey?.to || null,
|
||||
foreign_key_table: foreignKey?.table || null,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!table) {
|
||||
const tables = await this.tables();
|
||||
const columnsPerTable = await Promise.all(
|
||||
tables.map(async (table) => await getColumnsForTable(table))
|
||||
);
|
||||
return flatten(columnsPerTable);
|
||||
}
|
||||
|
||||
if (table && !column) {
|
||||
return await getColumnsForTable(table);
|
||||
}
|
||||
|
||||
const columns = await getColumnsForTable(table);
|
||||
return columns.find((columnInfo) => columnInfo.name === column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the current schema/database
|
||||
*/
|
||||
async hasColumn(table: string, column: string): Promise<boolean> {
|
||||
let isColumn = false;
|
||||
const results = await this.knex.raw(
|
||||
`SELECT COUNT(*) AS ct FROM pragma_table_info('${table}') WHERE name='${column}'`
|
||||
);
|
||||
const resultsVal = results[0]['ct'];
|
||||
if (resultsVal !== 0) {
|
||||
isColumn = true;
|
||||
}
|
||||
return isColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary key column for the given table
|
||||
*/
|
||||
async primary(table: string): Promise<string> {
|
||||
const columns = await this.knex.raw<RawColumn[]>(
|
||||
`PRAGMA table_info(??)`,
|
||||
table
|
||||
);
|
||||
const pkColumn = columns.find((col) => col.pk !== 0);
|
||||
return pkColumn!.name;
|
||||
}
|
||||
}
|
||||
30
packages/schema/src/index.ts
Normal file
30
packages/schema/src/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import Knex from 'knex';
|
||||
import { SchemaConstructor } from './types/schema-inspector';
|
||||
|
||||
export default function Schema(knex: Knex) {
|
||||
let constructor: SchemaConstructor;
|
||||
|
||||
switch (knex.client.constructor.name) {
|
||||
case 'Client_MySQL':
|
||||
constructor = require('./dialects/mysql').default;
|
||||
break;
|
||||
case 'Client_PG':
|
||||
constructor = require('./dialects/postgres').default;
|
||||
break;
|
||||
case 'Client_SQLite3':
|
||||
constructor = require('./dialects/sqlite').default;
|
||||
break;
|
||||
case 'Client_Oracledb':
|
||||
case 'Client_Oracle':
|
||||
constructor = require('./dialects/oracledb').default;
|
||||
break;
|
||||
case 'Client_MSSQL':
|
||||
constructor = require('./dialects/mssql').default;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Error('Unsupported driver used: ' + knex.client.constructor.name);
|
||||
}
|
||||
|
||||
return new constructor(knex);
|
||||
}
|
||||
22
packages/schema/src/types/column.ts
Normal file
22
packages/schema/src/types/column.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export interface Column {
|
||||
name: string;
|
||||
table: string;
|
||||
type: string;
|
||||
default_value: any | null;
|
||||
max_length: number | null;
|
||||
precision: number | null;
|
||||
scale: number | null;
|
||||
|
||||
is_nullable: boolean;
|
||||
is_primary_key: boolean;
|
||||
has_auto_increment: boolean;
|
||||
foreign_key_column: string | null;
|
||||
foreign_key_table: string | null;
|
||||
|
||||
// Not supported in SQLite or MSSQL
|
||||
comment?: string | null;
|
||||
|
||||
// Postgres Only
|
||||
schema?: string;
|
||||
foreign_key_schema?: string | null;
|
||||
}
|
||||
30
packages/schema/src/types/schema-inspector.ts
Normal file
30
packages/schema/src/types/schema-inspector.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import Knex from 'knex';
|
||||
import { Table } from './table';
|
||||
import { Column } from './column';
|
||||
|
||||
export interface Schema {
|
||||
knex: Knex;
|
||||
|
||||
tables(): Promise<string[]>;
|
||||
|
||||
tableInfo(): Promise<Table[]>;
|
||||
tableInfo(table: string): Promise<Table>;
|
||||
|
||||
hasTable(table: string): Promise<boolean>;
|
||||
|
||||
columns(table?: string): Promise<{ table: string; column: string }[]>;
|
||||
|
||||
columnInfo(): Promise<Column[]>;
|
||||
columnInfo(table?: string): Promise<Column[]>;
|
||||
columnInfo(table: string, column: string): Promise<Column>;
|
||||
|
||||
hasColumn(table: string, column: string): Promise<boolean>;
|
||||
primary(table: string): Promise<string | null>;
|
||||
|
||||
// Not in MySQL
|
||||
withSchema?(schema: string): void;
|
||||
}
|
||||
|
||||
export interface SchemaConstructor {
|
||||
new(knex: Knex): Schema;
|
||||
}
|
||||
20
packages/schema/src/types/table.ts
Normal file
20
packages/schema/src/types/table.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface Table {
|
||||
name: string;
|
||||
|
||||
// Not supported in SQLite + comment in mssql
|
||||
comment?: string | null;
|
||||
schema?: string;
|
||||
|
||||
// MySQL Only
|
||||
collation?: string;
|
||||
engine?: string;
|
||||
|
||||
// Postgres Only
|
||||
owner?: string;
|
||||
|
||||
// SQLite Only
|
||||
sql?: string;
|
||||
|
||||
//MSSQL only
|
||||
catalog?: string;
|
||||
}
|
||||
14
packages/schema/src/utils/extract-max-length.ts
Normal file
14
packages/schema/src/utils/extract-max-length.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Extracts the length value out of a given datatype
|
||||
* For example: `varchar(32)` => 32
|
||||
*/
|
||||
export default function extractMaxLength(type: string): null | number {
|
||||
const regex = /\(([^)]+)\)/;
|
||||
const matches = regex.exec(type);
|
||||
|
||||
if (matches && matches.length > 0 && matches[1]) {
|
||||
return Number(matches[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
436
packages/schema/test/mysql.spec.ts
Normal file
436
packages/schema/test/mysql.spec.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
import Knex from 'knex';
|
||||
import { expect } from 'chai';
|
||||
import schemaInspector from '../src';
|
||||
import { SchemaInspector } from '../src/types/schema-inspector';
|
||||
|
||||
describe('mysql', () => {
|
||||
let database: Knex;
|
||||
let inspector: SchemaInspector;
|
||||
|
||||
before(() => {
|
||||
database = Knex({
|
||||
client: 'mysql',
|
||||
connection: {
|
||||
host: '127.0.0.1',
|
||||
port: 5100,
|
||||
user: 'root',
|
||||
password: 'secret',
|
||||
database: 'test_db',
|
||||
charset: 'utf8',
|
||||
},
|
||||
});
|
||||
inspector = schemaInspector(database);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await database.destroy();
|
||||
});
|
||||
|
||||
describe('.tables', () => {
|
||||
it('returns tables', async () => {
|
||||
expect(await inspector.tables()).to.deep.equal([
|
||||
'page_visits',
|
||||
'teams',
|
||||
'users',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.tableInfo', () => {
|
||||
it('returns information for all tables', async () => {
|
||||
expect(await inspector.tableInfo()).to.deep.equal([
|
||||
{
|
||||
name: 'page_visits',
|
||||
schema: 'test_db',
|
||||
comment: '',
|
||||
collation: 'latin1_swedish_ci',
|
||||
engine: 'InnoDB',
|
||||
},
|
||||
{
|
||||
name: 'teams',
|
||||
schema: 'test_db',
|
||||
comment: '',
|
||||
collation: 'latin1_swedish_ci',
|
||||
engine: 'InnoDB',
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
schema: 'test_db',
|
||||
comment: '',
|
||||
collation: 'latin1_swedish_ci',
|
||||
engine: 'InnoDB',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns information for specific table', async () => {
|
||||
expect(await inspector.tableInfo('teams')).to.deep.equal({
|
||||
collation: 'latin1_swedish_ci',
|
||||
comment: '',
|
||||
engine: 'InnoDB',
|
||||
name: 'teams',
|
||||
schema: 'test_db',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.hasTable', () => {
|
||||
it('returns if table exists or not', async () => {
|
||||
expect(await inspector.hasTable('teams')).to.equal(true);
|
||||
expect(await inspector.hasTable('foobar')).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.columns', () => {
|
||||
it('returns information for all tables', async () => {
|
||||
expect(await inspector.columns()).to.deep.equal([
|
||||
{ table: 'page_visits', column: 'request_path' },
|
||||
{ table: 'page_visits', column: 'user_agent' },
|
||||
{ table: 'page_visits', column: 'created_at' },
|
||||
{ table: 'teams', column: 'id' },
|
||||
{ table: 'teams', column: 'name' },
|
||||
{ table: 'teams', column: 'description' },
|
||||
{ table: 'teams', column: 'credits' },
|
||||
{ table: 'teams', column: 'created_at' },
|
||||
{ table: 'teams', column: 'activated_at' },
|
||||
{ table: 'users', column: 'id' },
|
||||
{ table: 'users', column: 'team_id' },
|
||||
{ table: 'users', column: 'email' },
|
||||
{ table: 'users', column: 'password' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns information for specific table', async () => {
|
||||
expect(await inspector.columns('teams')).to.deep.equal([
|
||||
{ column: 'id', table: 'teams' },
|
||||
{ column: 'name', table: 'teams' },
|
||||
{ column: 'description', table: 'teams' },
|
||||
{ column: 'credits', table: 'teams' },
|
||||
{ column: 'created_at', table: 'teams' },
|
||||
{ column: 'activated_at', table: 'teams' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.columnInfo', () => {
|
||||
it('returns information for all columns in all tables', async () => {
|
||||
expect(await inspector.columnInfo()).to.deep.equal([
|
||||
{
|
||||
name: 'team_id',
|
||||
table: 'users',
|
||||
type: 'int',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 10,
|
||||
scale: 0,
|
||||
is_nullable: false,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: 'id',
|
||||
foreign_key_table: 'teams',
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
table: 'teams',
|
||||
type: 'int',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 10,
|
||||
scale: 0,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
has_auto_increment: true,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
table: 'users',
|
||||
type: 'int',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 10,
|
||||
scale: 0,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
has_auto_increment: true,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'request_path',
|
||||
table: 'page_visits',
|
||||
type: 'varchar',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'user_agent',
|
||||
table: 'page_visits',
|
||||
type: 'varchar',
|
||||
default_value: null,
|
||||
max_length: 200,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
table: 'page_visits',
|
||||
type: 'datetime',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
table: 'teams',
|
||||
type: 'varchar',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
table: 'teams',
|
||||
type: 'text',
|
||||
default_value: null,
|
||||
max_length: 65535,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'credits',
|
||||
table: 'teams',
|
||||
type: 'int',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 10,
|
||||
scale: 0,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: 'Remaining usage credits',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
table: 'teams',
|
||||
type: 'datetime',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'activated_at',
|
||||
table: 'teams',
|
||||
type: 'date',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
table: 'users',
|
||||
type: 'varchar',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
table: 'users',
|
||||
type: 'varchar',
|
||||
default_value: null,
|
||||
max_length: 60,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns information for all columns in specific table', async () => {
|
||||
expect(await inspector.columnInfo('teams')).to.deep.equal([
|
||||
{
|
||||
name: 'id',
|
||||
table: 'teams',
|
||||
type: 'int',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 10,
|
||||
scale: 0,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
has_auto_increment: true,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
table: 'teams',
|
||||
type: 'varchar',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
table: 'teams',
|
||||
type: 'text',
|
||||
default_value: null,
|
||||
max_length: 65535,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'credits',
|
||||
table: 'teams',
|
||||
type: 'int',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 10,
|
||||
scale: 0,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: 'Remaining usage credits',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
table: 'teams',
|
||||
type: 'datetime',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
{
|
||||
name: 'activated_at',
|
||||
table: 'teams',
|
||||
type: 'date',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns information for a specific column in a specific table', async () => {
|
||||
expect(await inspector.columnInfo('teams', 'name')).to.deep.equal({
|
||||
name: 'name',
|
||||
table: 'teams',
|
||||
type: 'varchar',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.primary', () => {
|
||||
it('returns primary key for a table', async () => {
|
||||
expect(await inspector.primary('teams')).to.equal('id');
|
||||
expect(await inspector.primary('page_visits')).to.equal(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
520
packages/schema/test/postgres.spec.ts
Normal file
520
packages/schema/test/postgres.spec.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
import Knex from 'knex';
|
||||
import { expect } from 'chai';
|
||||
import schemaInspector from '../src';
|
||||
import { SchemaInspector } from '../src/types/schema-inspector';
|
||||
|
||||
describe('postgres-no-search-path', () => {
|
||||
let database: Knex;
|
||||
let inspector: SchemaInspector;
|
||||
|
||||
before(() => {
|
||||
database = Knex({
|
||||
client: 'pg',
|
||||
connection: {
|
||||
host: '127.0.0.1',
|
||||
port: 5101,
|
||||
user: 'postgres',
|
||||
password: 'secret',
|
||||
database: 'test_db',
|
||||
charset: 'utf8',
|
||||
},
|
||||
});
|
||||
inspector = schemaInspector(database);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await database.destroy();
|
||||
});
|
||||
|
||||
describe('.tables', () => {
|
||||
it('returns tables', async () => {
|
||||
expect(await inspector.tables()).to.deep.equal([
|
||||
'teams',
|
||||
'users',
|
||||
'page_visits',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.tableInfo', () => {
|
||||
it('returns information for all tables', async () => {
|
||||
expect(await inspector.tableInfo()).to.deep.equal([
|
||||
{ name: 'page_visits', schema: 'public', comment: null },
|
||||
{ name: 'teams', schema: 'public', comment: null },
|
||||
{ name: 'users', schema: 'public', comment: null },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns information for specific table', async () => {
|
||||
expect(await inspector.tableInfo('teams')).to.deep.equal({
|
||||
comment: null,
|
||||
name: 'teams',
|
||||
schema: 'public',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.hasTable', () => {
|
||||
it('returns if table exists or not', async () => {
|
||||
expect(await inspector.hasTable('teams')).to.equal(true);
|
||||
expect(await inspector.hasTable('foobar')).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.columns', () => {
|
||||
it('returns information for all tables', async () => {
|
||||
database.transaction(async (trx) => {
|
||||
expect(await schemaInspector(trx).columns()).to.deep.equal([
|
||||
{ table: 'page_visits', column: 'user_agent' },
|
||||
{ table: 'teams', column: 'name' },
|
||||
{ table: 'teams', column: 'created_at' },
|
||||
{ table: 'users', column: 'team_id' },
|
||||
{ table: 'users', column: 'id' },
|
||||
{ table: 'users', column: 'password' },
|
||||
{ table: 'teams', column: 'activated_at' },
|
||||
{ table: 'users', column: 'email' },
|
||||
{ table: 'teams', column: 'credits' },
|
||||
{ table: 'page_visits', column: 'created_at' },
|
||||
{ table: 'teams', column: 'id' },
|
||||
{ table: 'teams', column: 'description' },
|
||||
{ table: 'page_visits', column: 'request_path' },
|
||||
]);
|
||||
});
|
||||
expect(await inspector.columns()).to.deep.equal([
|
||||
{ table: 'page_visits', column: 'user_agent' },
|
||||
{ table: 'teams', column: 'name' },
|
||||
{ table: 'teams', column: 'created_at' },
|
||||
{ table: 'users', column: 'team_id' },
|
||||
{ table: 'users', column: 'id' },
|
||||
{ table: 'users', column: 'password' },
|
||||
{ table: 'teams', column: 'activated_at' },
|
||||
{ table: 'users', column: 'email' },
|
||||
{ table: 'teams', column: 'credits' },
|
||||
{ table: 'page_visits', column: 'created_at' },
|
||||
{ table: 'teams', column: 'id' },
|
||||
{ table: 'teams', column: 'description' },
|
||||
{ table: 'page_visits', column: 'request_path' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns information for specific table', async () => {
|
||||
expect(await inspector.columns('teams')).to.deep.equal([
|
||||
{ table: 'teams', column: 'id' },
|
||||
{ table: 'teams', column: 'name' },
|
||||
{ table: 'teams', column: 'description' },
|
||||
{ table: 'teams', column: 'credits' },
|
||||
{ table: 'teams', column: 'created_at' },
|
||||
{ table: 'teams', column: 'activated_at' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.columnInfo', () => {
|
||||
it('returns information for all columns in all tables', async () => {
|
||||
expect(await inspector.columnInfo()).to.deep.equal([
|
||||
{
|
||||
name: 'team_id',
|
||||
table: 'users',
|
||||
type: 'integer',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 32,
|
||||
scale: 0,
|
||||
is_nullable: false,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: 'id',
|
||||
foreign_key_table: 'teams',
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: 'public',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
table: 'teams',
|
||||
type: 'text',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
table: 'users',
|
||||
type: 'integer',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 32,
|
||||
scale: 0,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
has_auto_increment: true,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'request_path',
|
||||
table: 'page_visits',
|
||||
type: 'character varying',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
table: 'teams',
|
||||
type: 'character varying',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
table: 'page_visits',
|
||||
type: 'timestamp without time zone',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
table: 'users',
|
||||
type: 'character varying',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
table: 'users',
|
||||
type: 'character varying',
|
||||
default_value: null,
|
||||
max_length: 60,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
table: 'teams',
|
||||
type: 'timestamp without time zone',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'activated_at',
|
||||
table: 'teams',
|
||||
type: 'date',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
table: 'teams',
|
||||
type: 'integer',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 32,
|
||||
scale: 0,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
has_auto_increment: true,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'user_agent',
|
||||
table: 'page_visits',
|
||||
type: 'character varying',
|
||||
default_value: null,
|
||||
max_length: 200,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'credits',
|
||||
table: 'teams',
|
||||
type: 'integer',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 32,
|
||||
scale: 0,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns information for all columns in specific table', async () => {
|
||||
expect(await inspector.columnInfo('teams')).to.deep.equal([
|
||||
{
|
||||
name: 'id',
|
||||
table: 'teams',
|
||||
type: 'integer',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 32,
|
||||
scale: 0,
|
||||
is_nullable: false,
|
||||
is_primary_key: true,
|
||||
has_auto_increment: true,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
table: 'teams',
|
||||
type: 'character varying',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
table: 'teams',
|
||||
type: 'text',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'credits',
|
||||
table: 'teams',
|
||||
type: 'integer',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: 32,
|
||||
scale: 0,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
table: 'teams',
|
||||
type: 'timestamp without time zone',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
{
|
||||
name: 'activated_at',
|
||||
table: 'teams',
|
||||
type: 'date',
|
||||
default_value: null,
|
||||
max_length: null,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
schema: 'public',
|
||||
foreign_key_schema: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns information for a specific column in a specific table', async () => {
|
||||
expect(await inspector.columnInfo('teams', 'name')).to.deep.equal({
|
||||
schema: 'public',
|
||||
name: 'name',
|
||||
table: 'teams',
|
||||
type: 'character varying',
|
||||
default_value: null,
|
||||
max_length: 100,
|
||||
precision: null,
|
||||
scale: null,
|
||||
is_nullable: true,
|
||||
is_primary_key: false,
|
||||
has_auto_increment: false,
|
||||
foreign_key_schema: null,
|
||||
foreign_key_column: null,
|
||||
foreign_key_table: null,
|
||||
comment: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.primary', () => {
|
||||
it('returns primary key for a table', async () => {
|
||||
expect(await inspector.primary('teams')).to.equal('id');
|
||||
expect(await inspector.primary('page_visits')).to.equal(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.transaction', () => {
|
||||
it('works with transactions transaction', async () => {
|
||||
database.transaction(async (trx) => {
|
||||
expect(await schemaInspector(trx).primary('teams')).to.equal('id');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('postgres-with-search-path', () => {
|
||||
let database: Knex;
|
||||
let inspector: SchemaInspector;
|
||||
|
||||
before(() => {
|
||||
database = Knex({
|
||||
searchPath: ['public', 'test'],
|
||||
client: 'pg',
|
||||
connection: {
|
||||
host: '127.0.0.1',
|
||||
port: 5101,
|
||||
user: 'postgres',
|
||||
password: 'secret',
|
||||
database: 'test_db',
|
||||
charset: 'utf8',
|
||||
},
|
||||
});
|
||||
inspector = schemaInspector(database);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await database.destroy();
|
||||
});
|
||||
|
||||
describe('.primary', () => {
|
||||
it('returns primary key for a table', async () => {
|
||||
expect(await inspector.primary('test')).to.equal('id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.transaction', () => {
|
||||
it('works with transactions transaction', async () => {
|
||||
database.transaction(async (trx) => {
|
||||
expect(await schemaInspector(trx).primary('test')).to.equal('id');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
25
packages/schema/test/seed/mysql.sql
Normal file
25
packages/schema/test/seed/mysql.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
create table teams (
|
||||
id int not null auto_increment primary key,
|
||||
name varchar(100),
|
||||
description text,
|
||||
credits integer(11) comment "Remaining usage credits",
|
||||
created_at datetime,
|
||||
activated_at date
|
||||
);
|
||||
|
||||
create table users (
|
||||
id int not null auto_increment primary key,
|
||||
team_id int not null,
|
||||
email varchar(100),
|
||||
password varchar(60),
|
||||
constraint fk_team_id
|
||||
foreign key (team_id)
|
||||
references teams (id)
|
||||
);
|
||||
|
||||
-- One table without a primary key
|
||||
create table page_visits (
|
||||
request_path varchar(100),
|
||||
user_agent varchar(200),
|
||||
created_at datetime
|
||||
);
|
||||
33
packages/schema/test/seed/postgres.sql
Normal file
33
packages/schema/test/seed/postgres.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
create table teams (
|
||||
id serial primary key,
|
||||
name varchar(100),
|
||||
description text,
|
||||
credits integer,
|
||||
created_at timestamp,
|
||||
activated_at date
|
||||
);
|
||||
comment on column teams.credits is 'Remaining usage credits';
|
||||
|
||||
create table users (
|
||||
id serial primary key,
|
||||
team_id int not null,
|
||||
email varchar(100),
|
||||
password varchar(60),
|
||||
constraint fk_team_id
|
||||
foreign key (team_id)
|
||||
references teams (id)
|
||||
);
|
||||
|
||||
-- One table without a primary key
|
||||
create table page_visits (
|
||||
request_path varchar(100),
|
||||
user_agent varchar(200),
|
||||
created_at timestamp
|
||||
);
|
||||
|
||||
-- One table in a schema
|
||||
create schema test;
|
||||
create table test.test (
|
||||
id serial primary key,
|
||||
number int not null
|
||||
);
|
||||
13
packages/schema/tsconfig.json
Normal file
13
packages/schema/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"lib": ["es2019", "es2018", "es2017", "es7", "es6", "dom"],
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user