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:
parent
70a4c6f15a
commit
efa550834a
7 changed files with 145 additions and 23 deletions
|
@ -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",
|
||||
|
|
5
packages/schemas/src/db-entries/custom-types.ts
Normal file
5
packages/schemas/src/db-entries/custom-types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
|
||||
export enum PasswordEncryptionMethod {
|
||||
SaltAndPepper = 'SaltAndPepper',
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = {`,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue