|
1 |
| -import { db, sql } from './db'; |
2 |
| -import { ColumnSchema, TableName, TableSchema } from './types'; |
3 |
| -import Path from 'path'; |
| 1 | +import { db } from './db'; |
| 2 | +import { TableSchema, ColumnType } from './types'; |
4 | 3 | import { Table } from '..';
|
5 | 4 | import Prettier from 'prettier';
|
6 |
| -import { defaults } from 'lodash'; |
| 5 | +import { defaults, flatMap } from 'lodash'; |
| 6 | +import { makeIntrospectionQuery } from './introspectionQuery'; |
| 7 | +import { xByY, xByYAndZ, parseTags } from './utils'; |
7 | 8 |
|
8 | 9 | // @ts-ignore
|
9 | 10 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
10 | 11 | class NotSupportedError extends Error {
|
11 | 12 | message = 'NotSupportedError';
|
12 | 13 | }
|
13 | 14 |
|
14 |
| -function getTableNames(): Promise<TableName[]> { |
15 |
| - return db.manyOrNone(sql(Path.join(__dirname, 'select_table_name.sql'))); |
16 |
| -} |
| 15 | +// Ref: https://github.com/graphile/postgraphile/tree/master/src/postgres/introspection/object |
| 16 | + |
| 17 | +type PgNamespace = { |
| 18 | + kind: 'namespace'; |
| 19 | + id: string; |
| 20 | + name: string; |
| 21 | + description?: string; |
| 22 | + comment?: string; |
| 23 | + tags: Record<string, string | boolean | (string | boolean)[]>; |
| 24 | +}; |
| 25 | + |
| 26 | +type PgProc = { |
| 27 | + kind: 'procedure'; |
| 28 | + id: string; |
| 29 | + name: string; |
| 30 | + description?: string; |
| 31 | + comment?: string; |
| 32 | + namespaceId: string; |
| 33 | + namespaceName: string; |
| 34 | + isStrict: boolean; |
| 35 | + returnsSet: boolean; |
| 36 | + isStable: boolean; |
| 37 | + returnTypeId: string; |
| 38 | + argTypeIds: Array<string>; |
| 39 | + argNames: Array<string>; |
| 40 | + argModes: Array<'i' | 'o' | 'b' | 'v' | 't'>; |
| 41 | + inputArgsCount: number; |
| 42 | + argDefaultsNum: number; |
| 43 | + namespace: PgNamespace; |
| 44 | + tags: Record<string, string | boolean | (string | boolean)[]>; |
| 45 | + cost: number; |
| 46 | + aclExecutable: boolean; |
| 47 | + language: string; |
| 48 | +}; |
| 49 | + |
| 50 | +type PgClass = { |
| 51 | + kind: 'class'; |
| 52 | + id: string; |
| 53 | + name: string; |
| 54 | + description?: string; |
| 55 | + comment?: string; |
| 56 | + classKind: string; |
| 57 | + namespaceId: string; |
| 58 | + namespaceName: string; |
| 59 | + typeId: string; |
| 60 | + isSelectable: boolean; |
| 61 | + isInsertable: boolean; |
| 62 | + isUpdatable: boolean; |
| 63 | + isDeletable: boolean; |
| 64 | + isExtensionConfigurationTable: boolean; |
| 65 | + namespace: PgNamespace; |
| 66 | + type: PgType; |
| 67 | + tags: Record<string, string | boolean | (string | boolean)[]>; |
| 68 | + attributes: Array<PgAttribute>; |
| 69 | + constraints: Array<PgConstraint>; |
| 70 | + foreignConstraints: Array<PgConstraint>; |
| 71 | + primaryKeyConstraint?: PgConstraint; |
| 72 | + aclSelectable: boolean; |
| 73 | + aclInsertable: boolean; |
| 74 | + aclUpdatable: boolean; |
| 75 | + aclDeletable: boolean; |
| 76 | + canUseAsterisk: boolean; |
| 77 | +}; |
| 78 | + |
| 79 | +type PgType = { |
| 80 | + kind: 'type'; |
| 81 | + id: string; |
| 82 | + name: ColumnType; |
| 83 | + description?: string; |
| 84 | + comment?: string; |
| 85 | + namespaceId: string; |
| 86 | + namespaceName: string; |
| 87 | + type: string; |
| 88 | + category: string; |
| 89 | + domainIsNotNull: boolean; |
| 90 | + arrayItemTypeId?: string; |
| 91 | + arrayItemType?: PgType; |
| 92 | + arrayType?: PgType; |
| 93 | + typeLength?: number; |
| 94 | + isPgArray: boolean; |
| 95 | + classId?: string; |
| 96 | + class?: PgClass; |
| 97 | + domainBaseTypeId?: string; |
| 98 | + domainBaseType?: PgType; |
| 99 | + domainTypeModifier?: number; |
| 100 | + tags: Record<string, string | boolean | (string | boolean)[]>; |
| 101 | +}; |
| 102 | + |
| 103 | +type PgAttribute = { |
| 104 | + kind: 'attribute'; |
| 105 | + classId: string; |
| 106 | + num: number; |
| 107 | + name: string; |
| 108 | + description?: string; |
| 109 | + comment?: string; |
| 110 | + typeId: string; |
| 111 | + typeModifier: number; |
| 112 | + isNotNull: boolean; |
| 113 | + hasDefault: boolean; |
| 114 | + identity: '' | 'a' | 'd'; |
| 115 | + class: PgClass; |
| 116 | + type: PgType; |
| 117 | + namespace: PgNamespace; |
| 118 | + tags: Record<string, string | boolean | (string | boolean)[]>; |
| 119 | + aclSelectable: boolean; |
| 120 | + aclInsertable: boolean; |
| 121 | + aclUpdatable: boolean; |
| 122 | + isIndexed?: boolean; |
| 123 | + isUnique?: boolean; |
| 124 | + columnLevelSelectGrant: boolean; |
| 125 | +}; |
| 126 | + |
| 127 | +type PgConstraint = { |
| 128 | + kind: 'constraint'; |
| 129 | + id: string; |
| 130 | + name: string; |
| 131 | + type: string; |
| 132 | + classId: string; |
| 133 | + class: PgClass; |
| 134 | + foreignClassId?: string; |
| 135 | + foreignClass?: PgClass; |
| 136 | + description?: string; |
| 137 | + comment?: string; |
| 138 | + keyAttributeNums: Array<number>; |
| 139 | + keyAttributes: Array<PgAttribute>; |
| 140 | + foreignKeyAttributeNums: Array<number>; |
| 141 | + foreignKeyAttributes: Array<PgAttribute>; |
| 142 | + namespace: PgNamespace; |
| 143 | + isIndexed?: boolean; |
| 144 | + tags: Record<string, string | boolean | (string | boolean)[]>; |
| 145 | +}; |
| 146 | + |
| 147 | +type PgExtension = { |
| 148 | + kind: 'extension'; |
| 149 | + id: string; |
| 150 | + name: string; |
| 151 | + namespaceId: string; |
| 152 | + namespaceName: string; |
| 153 | + relocatable: boolean; |
| 154 | + version: string; |
| 155 | + configurationClassIds?: Array<string>; |
| 156 | + description?: string; |
| 157 | + comment?: string; |
| 158 | + tags: Record<string, string | boolean | (string | boolean)[]>; |
| 159 | +}; |
17 | 160 |
|
18 |
| -function getColumnSchemas(tableName: string): Promise<ColumnSchema[]> { |
19 |
| - return db.manyOrNone(sql(Path.join(__dirname, 'select_table_schema.sql')), [tableName]); |
| 161 | +type PgIndex = { |
| 162 | + kind: 'index'; |
| 163 | + id: string; |
| 164 | + name: string; |
| 165 | + namespaceName: string; |
| 166 | + classId: string; |
| 167 | + numberOfAttributes: number; |
| 168 | + indexType: string; |
| 169 | + isUnique: boolean; |
| 170 | + isPrimary: boolean; |
| 171 | + /* |
| 172 | + Though these exist, we don't want to officially |
| 173 | + support them yet. |
| 174 | + isImmediate: boolean, |
| 175 | + isReplicaIdentity: boolean, |
| 176 | + isValid: boolean, |
| 177 | + */ |
| 178 | + isPartial: boolean; |
| 179 | + attributeNums: Array<number>; |
| 180 | + attributePropertiesAsc?: Array<boolean>; |
| 181 | + attributePropertiesNullsFirst?: Array<boolean>; |
| 182 | + description?: string; |
| 183 | + comment?: string; |
| 184 | + tags: Record<string, string | boolean | (string | boolean)[]>; |
| 185 | +}; |
| 186 | + |
| 187 | +type PgEntity = |
| 188 | + | PgNamespace |
| 189 | + | PgProc |
| 190 | + | PgClass |
| 191 | + | PgType |
| 192 | + | PgAttribute |
| 193 | + | PgConstraint |
| 194 | + | PgExtension |
| 195 | + | PgIndex; |
| 196 | + |
| 197 | +type PgIntrospectionOriginalResultsByKind = { |
| 198 | + __pgVersion: number; |
| 199 | + attribute: PgAttribute[]; |
| 200 | + class: PgClass[]; |
| 201 | + constraint: PgConstraint[]; |
| 202 | + extension: PgExtension[]; |
| 203 | + index: PgIndex[]; |
| 204 | + namespace: PgNamespace[]; |
| 205 | + procedure: PgProc[]; |
| 206 | + type: PgType[]; |
| 207 | +}; |
| 208 | + |
| 209 | +type PgIntrospectionResultsByKind = PgIntrospectionOriginalResultsByKind & { |
| 210 | + attributeByClassIdAndNum: { |
| 211 | + [classId: string]: { [num: string]: PgAttribute }; |
| 212 | + }; |
| 213 | + classById: { [classId: string]: PgClass }; |
| 214 | + extensionById: { [extId: string]: PgExtension }; |
| 215 | + namespaceById: { [namespaceId: string]: PgNamespace }; |
| 216 | + typeById: { [typeId: string]: PgType }; |
| 217 | +}; |
| 218 | + |
| 219 | +export async function getPostgresVersion(): Promise<number> { |
| 220 | + const versionResult = await db.one('show server_version_num;'); |
| 221 | + return Number.parseInt(versionResult.server_version_num, 10); |
20 | 222 | }
|
21 | 223 |
|
22 |
| -export async function getTablesSchemas(): Promise<Array<TableSchema>> { |
23 |
| - const tableNames = await getTableNames(); |
| 224 | +export async function runIntrospectionQuery(): Promise<PgIntrospectionResultsByKind> { |
| 225 | + const version = await getPostgresVersion(); |
| 226 | + const sql = makeIntrospectionQuery(version); |
| 227 | + const kinds = [ |
| 228 | + 'namespace', |
| 229 | + 'class', |
| 230 | + 'attribute', |
| 231 | + 'type', |
| 232 | + 'constraint', |
| 233 | + 'procedure', |
| 234 | + 'extension', |
| 235 | + 'index', |
| 236 | + ] as const; |
| 237 | + type Kinds = PgEntity['kind']; |
| 238 | + type Row = { |
| 239 | + object: PgEntity; |
| 240 | + }; |
| 241 | + const rows: Row[] = await db.many(sql, [['public'], false]); |
| 242 | + |
| 243 | + const originalResultsByType: PgIntrospectionOriginalResultsByKind = { |
| 244 | + __pgVersion: version, |
| 245 | + namespace: [], |
| 246 | + class: [], |
| 247 | + attribute: [], |
| 248 | + type: [], |
| 249 | + constraint: [], |
| 250 | + procedure: [], |
| 251 | + extension: [], |
| 252 | + index: [], |
| 253 | + }; |
| 254 | + |
| 255 | + for (const { object } of rows) { |
| 256 | + originalResultsByType[object.kind].push(object as any); |
| 257 | + } |
| 258 | + |
| 259 | + // Parse tags from comments |
| 260 | + kinds.forEach((kind) => { |
| 261 | + originalResultsByType[kind].forEach((object: PgIntrospectionResultsByKind[Kinds][number]) => { |
| 262 | + // Keep a copy of the raw comment |
| 263 | + object.comment = object.description; |
| 264 | + // https://www.graphile.org/postgraphile/smart-comments/ |
| 265 | + if (object.description) { |
| 266 | + const parsed = parseTags(object.description); |
| 267 | + object.tags = parsed.tags; |
| 268 | + object.description = parsed.text; |
| 269 | + } else { |
| 270 | + object.tags = {}; |
| 271 | + } |
| 272 | + }); |
| 273 | + }); |
24 | 274 |
|
25 |
| - const result = await Promise.all( |
26 |
| - tableNames.map(async (i) => { |
27 |
| - return { tableName: i.table_name, schema: await getColumnSchemas(i.table_name) }; |
28 |
| - }), |
| 275 | + const extensionConfigurationClassIds = flatMap( |
| 276 | + originalResultsByType.extension, |
| 277 | + (e) => e.configurationClassIds, |
29 | 278 | );
|
| 279 | + originalResultsByType.class.forEach((pgClass) => { |
| 280 | + pgClass.isExtensionConfigurationTable = extensionConfigurationClassIds.includes(pgClass.id); |
| 281 | + }); |
| 282 | + |
| 283 | + kinds.forEach((k) => { |
| 284 | + originalResultsByType[k].forEach(Object.freeze); |
| 285 | + }); |
| 286 | + |
| 287 | + const result = originalResultsByType as PgIntrospectionResultsByKind; |
| 288 | + result.namespaceById = xByY(originalResultsByType.namespace, 'id'); |
| 289 | + result.classById = xByY(originalResultsByType.class, 'id'); |
| 290 | + result.typeById = xByY(originalResultsByType.type, 'id'); |
| 291 | + result.attributeByClassIdAndNum = xByYAndZ(originalResultsByType.attribute, 'classId', 'num'); |
| 292 | + result.extensionById = xByY(originalResultsByType.extension, 'id'); |
30 | 293 |
|
31 |
| - return result; |
| 294 | + return Object.freeze(result); |
| 295 | +} |
| 296 | + |
| 297 | +export async function introspectSchemas() { |
| 298 | + const intro = await runIntrospectionQuery(); |
| 299 | + const namespaceIds = intro.namespace.map((n) => n.id); |
| 300 | + const classes = intro.class.filter((c) => namespaceIds.includes(c.namespaceId)); |
| 301 | + return { classes, intro }; |
| 302 | +} |
| 303 | + |
| 304 | +export async function getTablesSchemas(): Promise<Array<TableSchema>> { |
| 305 | + const { classes, intro } = await introspectSchemas(); |
| 306 | + |
| 307 | + return classes.map((klass) => { |
| 308 | + return { |
| 309 | + tableName: klass.name, |
| 310 | + schema: Object.values(intro.attributeByClassIdAndNum[klass.id]).map((attribute) => { |
| 311 | + return { |
| 312 | + column_name: attribute.name, |
| 313 | + udt_name: intro.typeById[attribute.typeId].name, |
| 314 | + is_nullable: attribute.isNotNull ? 'NO' : 'YES', |
| 315 | + }; |
| 316 | + }), |
| 317 | + }; |
| 318 | + }); |
32 | 319 | }
|
33 | 320 |
|
34 | 321 | export function schemaToTableObj(schema: TableSchema): Table {
|
|
0 commit comments