pg-codegen 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ import generate from '@babel/generator';
2
+ import * as t from '@babel/types';
3
+ export const generateCodeTree = (databaseObjects, options) => {
4
+ const { includeTimestamps, includeUUID } = options;
5
+ const schemaFiles = {};
6
+ // Common types
7
+ const commonTypes = [];
8
+ if (includeTimestamps) {
9
+ commonTypes.push(t.exportNamedDeclaration(t.tsTypeAliasDeclaration(t.identifier('Timestamp'), null, t.tsStringKeyword())));
10
+ }
11
+ if (includeUUID) {
12
+ commonTypes.push(t.exportNamedDeclaration(t.tsTypeAliasDeclaration(t.identifier('UUID'), null, t.tsStringKeyword())));
13
+ }
14
+ schemaFiles['schemas/_common.ts'] = commonTypes;
15
+ // Classes & Interfaces per schema
16
+ databaseObjects.forEach((obj) => {
17
+ if (obj.kind === 'class' && obj.classKind === 'r') {
18
+ const schemaName = obj.namespaceName;
19
+ const pascalName = toPascalCase(obj.name);
20
+ const interfaceFields = [];
21
+ const classFields = [];
22
+ const constructorBody = [];
23
+ const usedTypes = new Set();
24
+ databaseObjects.forEach((attr) => {
25
+ if (attr.kind === 'attribute' && attr.classId === obj.id) {
26
+ const fieldType = mapPostgresTypeToTSType(attr.typeId, attr.isNotNull);
27
+ const postgresType = mapPostgresTypeToIdentifier(attr.typeId);
28
+ if (postgresType)
29
+ usedTypes.add(postgresType);
30
+ interfaceFields.push(t.tsPropertySignature(t.identifier(attr.name), t.tsTypeAnnotation(fieldType)));
31
+ classFields.push(t.classProperty(t.identifier(attr.name), undefined, t.tsTypeAnnotation(fieldType)));
32
+ constructorBody.push(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.thisExpression(), t.identifier(attr.name)), t.memberExpression(t.identifier('data'), t.identifier(attr.name)))));
33
+ }
34
+ });
35
+ const interfaceDeclaration = t.exportNamedDeclaration(t.tsInterfaceDeclaration(t.identifier(pascalName), null, [], t.tsInterfaceBody(interfaceFields)));
36
+ const data = t.identifier('data');
37
+ data.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(pascalName)));
38
+ const classImplements = t.tsExpressionWithTypeArguments(t.identifier(pascalName));
39
+ const classDeclaration = t.exportNamedDeclaration(t.classDeclaration(t.identifier(pascalName), null, t.classBody([
40
+ ...classFields,
41
+ t.classMethod('constructor', t.identifier('constructor'), [data], t.blockStatement(constructorBody))
42
+ ])));
43
+ classDeclaration.declaration.implements = [classImplements];
44
+ const filePath = `schemas/${schemaName}.ts`;
45
+ if (!schemaFiles[filePath])
46
+ schemaFiles[filePath] = [];
47
+ if (usedTypes.size > 0) {
48
+ const existingImports = schemaFiles[filePath].find((s) => t.isImportDeclaration(s) && s.source.value === './_common');
49
+ if (!existingImports) {
50
+ schemaFiles[filePath].unshift(t.importDeclaration(Array.from(usedTypes).map((type) => t.importSpecifier(t.identifier(type), t.identifier(type))), t.stringLiteral('./_common')));
51
+ }
52
+ else {
53
+ const current = new Set(existingImports.specifiers.map((s) => s.local.name));
54
+ Array.from(usedTypes).forEach((type) => {
55
+ if (!current.has(type)) {
56
+ existingImports.specifiers.push(t.importSpecifier(t.identifier(type), t.identifier(type)));
57
+ }
58
+ });
59
+ }
60
+ }
61
+ schemaFiles[filePath].push(interfaceDeclaration, classDeclaration);
62
+ }
63
+ });
64
+ // index.ts exports
65
+ const indexFileStatements = [];
66
+ Object.keys(schemaFiles).forEach((filePath) => {
67
+ const schemaName = filePath.replace('schemas/', '').replace('.ts', '');
68
+ if (schemaName === '_common')
69
+ return;
70
+ if (schemaName === 'public') {
71
+ indexFileStatements.push(t.exportAllDeclaration(t.stringLiteral(`./${filePath.replace('.ts', '')}`)));
72
+ }
73
+ else {
74
+ indexFileStatements.push(t.importDeclaration([t.importNamespaceSpecifier(t.identifier(schemaName))], t.stringLiteral(`./${filePath.replace('.ts', '')}`)), t.exportNamedDeclaration(null, [
75
+ t.exportSpecifier(t.identifier(schemaName), t.identifier(schemaName))
76
+ ]));
77
+ }
78
+ });
79
+ const fileTree = {};
80
+ Object.entries(schemaFiles).forEach(([filePath, statements]) => {
81
+ fileTree[filePath] = generate(t.program(statements)).code;
82
+ });
83
+ fileTree['index.ts'] = generate(t.program(indexFileStatements)).code;
84
+ return fileTree;
85
+ };
86
+ const toPascalCase = (str) => str
87
+ .replace(/[_-](\w)/g, (_, c) => c.toUpperCase())
88
+ .replace(/^\w/, (c) => c.toUpperCase());
89
+ const mapPostgresTypeToTSType = (typeId, isNotNull) => {
90
+ const optionalType = (type) => isNotNull ? type : t.tsUnionType([type, t.tsNullKeyword()]);
91
+ switch (typeId) {
92
+ case '20': // BIGINT
93
+ case '21': // SMALLINT
94
+ case '23': // INTEGER
95
+ case '1700': // NUMERIC
96
+ return optionalType(t.tsNumberKeyword());
97
+ case '25': // TEXT
98
+ case '1043': // VARCHAR
99
+ return optionalType(t.tsStringKeyword());
100
+ case '1114': // TIMESTAMP
101
+ case '1184': // TIMESTAMPTZ
102
+ return optionalType(t.tsTypeReference(t.identifier('Timestamp')));
103
+ case '2950': // UUID
104
+ return optionalType(t.tsTypeReference(t.identifier('UUID')));
105
+ case '16': // BOOLEAN
106
+ return optionalType(t.tsBooleanKeyword());
107
+ default:
108
+ return optionalType(t.tsAnyKeyword());
109
+ }
110
+ };
111
+ // Map Postgres type OIDs to type names for imports
112
+ const mapPostgresTypeToIdentifier = (typeId) => {
113
+ switch (typeId) {
114
+ case '1114': // TIMESTAMP
115
+ case '1184': // TIMESTAMPTZ
116
+ return 'Timestamp';
117
+ case '2950': // UUID
118
+ return 'UUID';
119
+ default:
120
+ return null;
121
+ }
122
+ };
@@ -0,0 +1,70 @@
1
+ import generate from '@babel/generator';
2
+ import * as t from '@babel/types';
3
+ export const generateCode = (databaseObjects, options) => {
4
+ const { includeTimestamps, includeUUID } = options;
5
+ const namespaces = {};
6
+ // Organize objects by namespace
7
+ databaseObjects.forEach((obj) => {
8
+ if (obj.kind === 'namespace') {
9
+ namespaces[obj.name] = [];
10
+ }
11
+ });
12
+ // Generate interfaces for tables (Class objects with kind 'r')
13
+ databaseObjects.forEach((obj) => {
14
+ if (obj.kind === 'class' && obj.classKind === 'r') {
15
+ const namespace = obj.namespaceName;
16
+ const fields = [];
17
+ // Find attributes for the class
18
+ databaseObjects.forEach((attr) => {
19
+ if (attr.kind === 'attribute' && attr.classId === obj.id) {
20
+ fields.push(t.tsPropertySignature(t.identifier(attr.name), t.tsTypeAnnotation(mapPostgresTypeToTSType(attr.typeId, attr.isNotNull))));
21
+ }
22
+ });
23
+ // Create the interface declaration
24
+ const interfaceDeclaration = t.tsInterfaceDeclaration(t.identifier(obj.name), null, [], t.tsInterfaceBody(fields));
25
+ // Add to the namespace
26
+ if (namespaces[namespace]) {
27
+ namespaces[namespace].push(interfaceDeclaration);
28
+ }
29
+ }
30
+ });
31
+ // Generate the final AST
32
+ const programBody = [];
33
+ if (includeTimestamps) {
34
+ programBody.push(t.tsTypeAliasDeclaration(t.identifier('Timestamp'), null, t.tsStringKeyword()));
35
+ }
36
+ if (includeUUID) {
37
+ programBody.push(t.tsTypeAliasDeclaration(t.identifier('UUID'), null, t.tsStringKeyword()));
38
+ }
39
+ Object.keys(namespaces).forEach((namespace) => {
40
+ const namespaceDeclaration = t.tsModuleDeclaration(t.identifier(namespace), t.tsModuleBlock(namespaces[namespace]));
41
+ namespaceDeclaration.declare = true;
42
+ programBody.push(namespaceDeclaration);
43
+ });
44
+ const program = t.program(programBody);
45
+ // Generate the code
46
+ return generate(program).code;
47
+ };
48
+ // Map Postgres type OIDs to TypeScript types
49
+ const mapPostgresTypeToTSType = (typeId, isNotNull) => {
50
+ const optionalType = (type) => isNotNull ? type : t.tsUnionType([type, t.tsNullKeyword()]);
51
+ switch (typeId) {
52
+ case '20': // BIGINT
53
+ case '21': // SMALLINT
54
+ case '23': // INTEGER
55
+ case '1700': // NUMERIC
56
+ return optionalType(t.tsNumberKeyword());
57
+ case '25': // TEXT
58
+ case '1043': // VARCHAR
59
+ return optionalType(t.tsStringKeyword());
60
+ case '1114': // TIMESTAMP
61
+ case '1184': // TIMESTAMPTZ
62
+ return optionalType(t.tsTypeReference(t.identifier('Timestamp')));
63
+ case '2950': // UUID
64
+ return optionalType(t.tsTypeReference(t.identifier('UUID')));
65
+ case '16': // BOOLEAN
66
+ return optionalType(t.tsBooleanKeyword());
67
+ default:
68
+ return optionalType(t.tsAnyKeyword());
69
+ }
70
+ };
package/esm/index.js ADDED
@@ -0,0 +1,69 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { generateCodeTree } from './codegen/codegen';
4
+ import getIntrospectionRows from './introspect';
5
+ import { Logger } from '@launchql/server-utils';
6
+ import { getPgEnvOptions } from '@launchql/types';
7
+ import { getRootPgPool } from '@launchql/server-utils';
8
+ const log = new Logger('codegen');
9
+ (async () => {
10
+ const env = getPgEnvOptions();
11
+ const pool = getRootPgPool(env);
12
+ const options = {
13
+ // @ts-ignore
14
+ client: pool, // hope this is ok?
15
+ introspectionOptions: {
16
+ pgLegacyFunctionsOnly: false,
17
+ pgIgnoreRBAC: true,
18
+ },
19
+ namespacesToIntrospect: ['collections_public'],
20
+ includeExtensions: false,
21
+ };
22
+ const outputDirectory = path.join(__dirname, '../../../__fixtures__/output');
23
+ try {
24
+ // Clean the output directory
25
+ await fs.rm(outputDirectory, { recursive: true, force: true });
26
+ log.info(`Cleaned output directory: ${outputDirectory}`);
27
+ // Fetch introspection rows
28
+ const rows = await getIntrospectionRows(options);
29
+ log.info('Introspection Rows Fetched:', rows);
30
+ // Generate TypeScript code
31
+ const codegenOptions = {
32
+ includeTimestamps: true,
33
+ includeUUID: true,
34
+ };
35
+ const generatedCode = generateCodeTree(rows, codegenOptions);
36
+ log.info('Generated TypeScript Code Tree:', generatedCode);
37
+ // Write the generated code to files
38
+ await writeGeneratedFiles(outputDirectory, generatedCode);
39
+ log.info(`Generated files written to ${outputDirectory}`);
40
+ }
41
+ catch (error) {
42
+ log.error('Failed to fetch introspection rows or generate code:', error);
43
+ }
44
+ })();
45
+ /**
46
+ * Writes the generated code files to the specified directory.
47
+ *
48
+ * @param outputDir The base directory to write the files.
49
+ * @param fileTree The file tree containing file paths and content.
50
+ */
51
+ const writeGeneratedFiles = async (outputDir, fileTree) => {
52
+ try {
53
+ // Ensure the output directory exists
54
+ await fs.mkdir(outputDir, { recursive: true });
55
+ // Write each file to its corresponding path
56
+ for (const [filePath, content] of Object.entries(fileTree)) {
57
+ const fullPath = path.join(outputDir, filePath);
58
+ // Ensure the directory for the file exists
59
+ const dirName = path.dirname(fullPath);
60
+ await fs.mkdir(dirName, { recursive: true });
61
+ // Write the file content
62
+ await fs.writeFile(fullPath, content, 'utf8');
63
+ }
64
+ }
65
+ catch (error) {
66
+ log.error(`Failed to write files to ${outputDir}:`, error);
67
+ throw error;
68
+ }
69
+ };
@@ -0,0 +1,26 @@
1
+ import { makeIntrospectionQuery } from './query';
2
+ import { Logger } from '@launchql/server-utils';
3
+ const log = new Logger('codegen');
4
+ export const getIntrospectionRows = async (options) => {
5
+ const { client, introspectionOptions, namespacesToIntrospect, includeExtensions = false, } = options;
6
+ try {
7
+ // Query for server version in integer format
8
+ const res = await client.query('SHOW server_version_num');
9
+ const serverVersionNum = parseInt(res.rows[0].server_version_num, 10);
10
+ // Generate the introspection query
11
+ const introspectionQuery = makeIntrospectionQuery(serverVersionNum, introspectionOptions);
12
+ // Execute the introspection query
13
+ const queryResult = await client.query(introspectionQuery, [
14
+ namespacesToIntrospect,
15
+ includeExtensions,
16
+ ]);
17
+ // Map the result rows to the `DatabaseObject` type
18
+ const rows = queryResult.rows.map((result) => result.object);
19
+ return rows;
20
+ }
21
+ catch (error) {
22
+ log.error('Error during introspection:', error);
23
+ throw error;
24
+ }
25
+ };
26
+ export default getIntrospectionRows;