0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat: generate enum types from SQL

This commit is contained in:
Gao Sun 2021-07-03 19:13:05 +08:00
parent 70a4c6f15a
commit efa550834a
No known key found for this signature in database
GPG key ID: 0F0EFA2E36639F31
7 changed files with 145 additions and 23 deletions

View file

@ -9,7 +9,7 @@
"lib"
],
"scripts": {
"generate": "ts-node src/gen/index.ts",
"generate": "ts-node src/gen/index.ts && xo src/db-entries --fix",
"build": "yarn generate && rm -rf lib/ && tsc --p tsconfig.build.json",
"lint": "xo src/"
},
@ -18,10 +18,12 @@
"yarn": "1"
},
"devDependencies": {
"@logto/essentials": "^1.0.2",
"@logto/essentials": "^1.0.5",
"@types/lodash.uniq": "^4.5.6",
"@types/node": "14",
"@types/pluralize": "^0.0.29",
"camelcase": "^6.2.0",
"lodash.uniq": "^4.5.0",
"pluralize": "^8.0.0",
"ts-node": "^10.0.0",
"typescript": "^4.3.4",

View file

@ -0,0 +1,5 @@
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
export enum PasswordEncryptionMethod {
SaltAndPepper = 'SaltAndPepper',
}

View file

@ -1,12 +1,14 @@
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
import { PasswordEncryptionMethod } from './custom-types';
export type UserDBEntry = {
id: string;
userName?: string;
primaryEmail?: string;
primaryPhone?: string;
passwordEncrypted?: string;
passwordEncryptionMethod?: string;
passwordEncryptionMethod?: PasswordEncryptionMethod;
passwordEncryptionSalt?: string;
};

View file

@ -3,22 +3,47 @@ import camelcase from 'camelcase';
import fs from 'fs/promises';
import path from 'path';
import pluralize from 'pluralize';
import { conditionalString } from '@logto/essentials';
import uniq from 'lodash.uniq';
import { conditional, conditionalString } from '@logto/essentials';
import { findFirstParentheses, getType, normalizeWhitespaces, removeParentheses } from './utils';
type Field = {
name: string;
type: string;
type?: string;
customType?: string;
required: boolean;
isArray: boolean;
};
// eslint-disable-next-line @typescript-eslint/ban-types
type FieldWithType = Omit<Field, 'type' | 'customType'> & { type: string };
type Type = {
name: string;
type: 'enum';
values: string[];
};
type GeneratedType = Type & {
tsName: string;
};
type Table = {
name: string;
fields: Field[];
};
type TableWithType = {
name: string;
fields: FieldWithType[];
};
type FileData = {
types: Type[];
tables: Table[];
};
const dir = 'tables';
const generate = async () => {
@ -26,13 +51,12 @@ const generate = async () => {
const generated = await Promise.all(
files
.filter((file) => file.endsWith('.sql'))
.map<Promise<[string, Table[]]>>(async (file) => [
file,
(
await fs.readFile(path.join(dir, file), { encoding: 'utf-8' })
)
.map<Promise<[string, FileData]>>(async (file) => {
const statements = (await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }))
.split(';')
.map((value) => normalizeWhitespaces(value).toLowerCase())
.map((value) => normalizeWhitespaces(value));
const tables = statements
.map((value) => value.toLowerCase())
.filter((value) => value.startsWith('create table'))
.map((value) => findFirstParentheses(value))
.filter((value): value is NonNullable<typeof value> => Boolean(value))
@ -56,17 +80,38 @@ const generate = async () => {
// CAUTION: Only works for single dimension arrays
const isArray = Boolean(/\[.*]/.test(type)) || restJoined.includes('array');
const required = restJoined.includes('not null');
const primitiveType = getType(type);
return {
name,
type: getType(type),
type: primitiveType,
customType: conditional(!primitiveType && type),
isArray,
required,
};
});
return { name, fields };
}),
])
});
const types = statements
.filter((value) => value.toLowerCase().startsWith('create type'))
.map<Type>((value) => {
const breakdowns = value.split(' ');
const name = breakdowns[2];
const data = findFirstParentheses(value);
assert(
name &&
data &&
breakdowns[3]?.toLowerCase() === 'as' &&
breakdowns[4]?.toLowerCase() === 'enum',
'Only support enum custom type'
);
const values = data.body.split(',').map((value) => value.trim().slice(1, -1));
return { name, type: 'enum', values };
});
return [file, { tables, types }];
})
);
const generatedDir = 'src/db-entries';
@ -75,11 +120,61 @@ const generate = async () => {
await fs.rmdir(generatedDir, { recursive: true });
await fs.mkdir(generatedDir, { recursive: true });
const allTypes = generated
.flatMap((data) => data[1].types)
.map<GeneratedType>((type) => ({
...type,
tsName: camelcase(type.name, { pascalCase: true }),
}));
// Generate custom types
await fs.writeFile(
path.join(generatedDir, 'custom-types.ts'),
header +
allTypes
.map(({ tsName, values }) =>
[
`export enum ${tsName} {`,
...values.map((value) => ` ${value} = '${value}',`),
'}',
].join('\n')
)
.join('\n') +
'\n'
);
// Generate DB entry types
await Promise.all(
generated.map(async ([file, tables]) => {
generated.map(async ([file, { tables }]) => {
const customTypes: string[] = [];
const tableWithTypes = tables.map<TableWithType>(({ fields, ...rest }) => ({
...rest,
fields: fields.map(({ type, customType, ...rest }) => {
const finalType = type ?? allTypes.find(({ name }) => name === customType)?.tsName;
assert(finalType, `Type ${customType ?? 'N/A'} not found`);
if (type === undefined) {
customTypes.push(finalType);
}
return { ...rest, type: finalType };
}),
}));
const importTypes =
customTypes.length > 0
? [
'import {',
uniq(customTypes)
.map((value) => ` ${value}`)
.join(',\n'),
"} from './custom-types';",
].join('\n') + '\n\n'
: '';
const content =
header +
tables
importTypes +
tableWithTypes
.map(({ name, fields }) =>
[
`export type ${pluralize(camelcase(name, { pascalCase: true }), 1)}DBEntry = {`,

View file

@ -82,7 +82,7 @@ const getRawType = (value: string): string => {
// eslint-disable-next-line complexity
export const getType = (
value: string
): 'string' | 'number' | 'boolean' | 'Record<string, unknown>' | 'Date' => {
): 'string' | 'number' | 'boolean' | 'Record<string, unknown>' | 'Date' | undefined => {
switch (getRawType(value)) {
case 'bpchar':
case 'char':
@ -117,6 +117,5 @@ export const getType = (
case 'timestamptz':
return 'Date';
default:
throw new Error('Unable to parse type: ' + value);
}
};

View file

@ -1,10 +1,12 @@
create type password_encryption_method as enum ('SaltAndPepper');
create table users (
id varchar(24) not null,
user_name varchar(128) unique,
primary_email varchar(128) unique,
primary_phone varchar(128) unique,
password_encrypted varchar(128),
password_encryption_method varchar(32),
password_encryption_method password_encryption_method,
password_encryption_salt varchar(128),
primary key (id)
);

View file

@ -232,10 +232,10 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@logto/essentials@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@logto/essentials/-/essentials-1.0.2.tgz#02a8c878b4079a63709c35ce8c143370f259bb13"
integrity sha512-GN6m1c5ll7EhOgJbmFwRESkeWGjdoihst8O1aVIxvEOXhLe/79bph4hejsdm59piX3jgKWakEHimpqcBT1kO+Q==
"@logto/essentials@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@logto/essentials/-/essentials-1.0.5.tgz#edc1a376cf82e8829adcf26e8b98db1235cfbfcd"
integrity sha512-zft8VodNOtkhyyQTHiOlksk6UonQBY68dON4cQaUOlpN5JS9z5/zchi4I0j+XB6EeQ7l8ZtVXct2VZBp8m03kw==
dependencies:
lodash.orderby "^4.6.0"
lodash.pick "^4.4.0"
@ -337,6 +337,18 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash.uniq@^4.5.6":
version "4.5.6"
resolved "https://registry.yarnpkg.com/@types/lodash.uniq/-/lodash.uniq-4.5.6.tgz#adb052f6c7eeb38b920c13166e7a972dd960b4c5"
integrity sha512-XHNMXBtiwsWZstZMyxOYjr0e8YYWv0RgPlzIHblTuwBBiWo2MzWVaTBihtBpslb5BglgAWIeBv69qt1+RTRW1A==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.170"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
"@types/minimatch@*":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
@ -2557,6 +2569,11 @@ lodash.truncate@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.13.1, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"