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"
|
"lib"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"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",
|
"build": "yarn generate && rm -rf lib/ && tsc --p tsconfig.build.json",
|
||||||
"lint": "xo src/"
|
"lint": "xo src/"
|
||||||
},
|
},
|
||||||
|
@ -18,10 +18,12 @@
|
||||||
"yarn": "1"
|
"yarn": "1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@logto/essentials": "^1.0.2",
|
"@logto/essentials": "^1.0.5",
|
||||||
|
"@types/lodash.uniq": "^4.5.6",
|
||||||
"@types/node": "14",
|
"@types/node": "14",
|
||||||
"@types/pluralize": "^0.0.29",
|
"@types/pluralize": "^0.0.29",
|
||||||
"camelcase": "^6.2.0",
|
"camelcase": "^6.2.0",
|
||||||
|
"lodash.uniq": "^4.5.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "^4.3.4",
|
"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.
|
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
|
||||||
|
import { PasswordEncryptionMethod } from './custom-types';
|
||||||
|
|
||||||
export type UserDBEntry = {
|
export type UserDBEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
primaryEmail?: string;
|
primaryEmail?: string;
|
||||||
primaryPhone?: string;
|
primaryPhone?: string;
|
||||||
passwordEncrypted?: string;
|
passwordEncrypted?: string;
|
||||||
passwordEncryptionMethod?: string;
|
passwordEncryptionMethod?: PasswordEncryptionMethod;
|
||||||
passwordEncryptionSalt?: string;
|
passwordEncryptionSalt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,22 +3,47 @@ import camelcase from 'camelcase';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import pluralize from 'pluralize';
|
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';
|
import { findFirstParentheses, getType, normalizeWhitespaces, removeParentheses } from './utils';
|
||||||
|
|
||||||
type Field = {
|
type Field = {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type?: string;
|
||||||
|
customType?: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
isArray: 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 = {
|
type Table = {
|
||||||
name: string;
|
name: string;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TableWithType = {
|
||||||
|
name: string;
|
||||||
|
fields: FieldWithType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type FileData = {
|
||||||
|
types: Type[];
|
||||||
|
tables: Table[];
|
||||||
|
};
|
||||||
|
|
||||||
const dir = 'tables';
|
const dir = 'tables';
|
||||||
|
|
||||||
const generate = async () => {
|
const generate = async () => {
|
||||||
|
@ -26,13 +51,12 @@ const generate = async () => {
|
||||||
const generated = await Promise.all(
|
const generated = await Promise.all(
|
||||||
files
|
files
|
||||||
.filter((file) => file.endsWith('.sql'))
|
.filter((file) => file.endsWith('.sql'))
|
||||||
.map<Promise<[string, Table[]]>>(async (file) => [
|
.map<Promise<[string, FileData]>>(async (file) => {
|
||||||
file,
|
const statements = (await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }))
|
||||||
(
|
|
||||||
await fs.readFile(path.join(dir, file), { encoding: 'utf-8' })
|
|
||||||
)
|
|
||||||
.split(';')
|
.split(';')
|
||||||
.map((value) => normalizeWhitespaces(value).toLowerCase())
|
.map((value) => normalizeWhitespaces(value));
|
||||||
|
const tables = statements
|
||||||
|
.map((value) => value.toLowerCase())
|
||||||
.filter((value) => value.startsWith('create table'))
|
.filter((value) => value.startsWith('create table'))
|
||||||
.map((value) => findFirstParentheses(value))
|
.map((value) => findFirstParentheses(value))
|
||||||
.filter((value): value is NonNullable<typeof value> => Boolean(value))
|
.filter((value): value is NonNullable<typeof value> => Boolean(value))
|
||||||
|
@ -56,17 +80,38 @@ const generate = async () => {
|
||||||
// CAUTION: Only works for single dimension arrays
|
// CAUTION: Only works for single dimension arrays
|
||||||
const isArray = Boolean(/\[.*]/.test(type)) || restJoined.includes('array');
|
const isArray = Boolean(/\[.*]/.test(type)) || restJoined.includes('array');
|
||||||
const required = restJoined.includes('not null');
|
const required = restJoined.includes('not null');
|
||||||
|
const primitiveType = getType(type);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
type: getType(type),
|
type: primitiveType,
|
||||||
|
customType: conditional(!primitiveType && type),
|
||||||
isArray,
|
isArray,
|
||||||
required,
|
required,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return { name, fields };
|
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';
|
const generatedDir = 'src/db-entries';
|
||||||
|
@ -75,11 +120,61 @@ const generate = async () => {
|
||||||
|
|
||||||
await fs.rmdir(generatedDir, { recursive: true });
|
await fs.rmdir(generatedDir, { recursive: true });
|
||||||
await fs.mkdir(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(
|
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 =
|
const content =
|
||||||
header +
|
header +
|
||||||
tables
|
importTypes +
|
||||||
|
tableWithTypes
|
||||||
.map(({ name, fields }) =>
|
.map(({ name, fields }) =>
|
||||||
[
|
[
|
||||||
`export type ${pluralize(camelcase(name, { pascalCase: true }), 1)}DBEntry = {`,
|
`export type ${pluralize(camelcase(name, { pascalCase: true }), 1)}DBEntry = {`,
|
||||||
|
|
|
@ -82,7 +82,7 @@ const getRawType = (value: string): string => {
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
export const getType = (
|
export const getType = (
|
||||||
value: string
|
value: string
|
||||||
): 'string' | 'number' | 'boolean' | 'Record<string, unknown>' | 'Date' => {
|
): 'string' | 'number' | 'boolean' | 'Record<string, unknown>' | 'Date' | undefined => {
|
||||||
switch (getRawType(value)) {
|
switch (getRawType(value)) {
|
||||||
case 'bpchar':
|
case 'bpchar':
|
||||||
case 'char':
|
case 'char':
|
||||||
|
@ -117,6 +117,5 @@ export const getType = (
|
||||||
case 'timestamptz':
|
case 'timestamptz':
|
||||||
return 'Date';
|
return 'Date';
|
||||||
default:
|
default:
|
||||||
throw new Error('Unable to parse type: ' + value);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
|
create type password_encryption_method as enum ('SaltAndPepper');
|
||||||
|
|
||||||
create table users (
|
create table users (
|
||||||
id varchar(24) not null,
|
id varchar(24) not null,
|
||||||
user_name varchar(128) unique,
|
user_name varchar(128) unique,
|
||||||
primary_email varchar(128) unique,
|
primary_email varchar(128) unique,
|
||||||
primary_phone varchar(128) unique,
|
primary_phone varchar(128) unique,
|
||||||
password_encrypted varchar(128),
|
password_encrypted varchar(128),
|
||||||
password_encryption_method varchar(32),
|
password_encryption_method password_encryption_method,
|
||||||
password_encryption_salt varchar(128),
|
password_encryption_salt varchar(128),
|
||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
|
@ -232,10 +232,10 @@
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
"@logto/essentials@^1.0.2":
|
"@logto/essentials@^1.0.5":
|
||||||
version "1.0.2"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@logto/essentials/-/essentials-1.0.2.tgz#02a8c878b4079a63709c35ce8c143370f259bb13"
|
resolved "https://registry.yarnpkg.com/@logto/essentials/-/essentials-1.0.5.tgz#edc1a376cf82e8829adcf26e8b98db1235cfbfcd"
|
||||||
integrity sha512-GN6m1c5ll7EhOgJbmFwRESkeWGjdoihst8O1aVIxvEOXhLe/79bph4hejsdm59piX3jgKWakEHimpqcBT1kO+Q==
|
integrity sha512-zft8VodNOtkhyyQTHiOlksk6UonQBY68dON4cQaUOlpN5JS9z5/zchi4I0j+XB6EeQ7l8ZtVXct2VZBp8m03kw==
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash.orderby "^4.6.0"
|
lodash.orderby "^4.6.0"
|
||||||
lodash.pick "^4.4.0"
|
lodash.pick "^4.4.0"
|
||||||
|
@ -337,6 +337,18 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
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@*":
|
"@types/minimatch@*":
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
|
||||||
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
|
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:
|
lodash@^4.13.1, lodash@^4.17.21:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
|
|
Loading…
Reference in a new issue