0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

Complete table types generation

This commit is contained in:
Gao Sun 2021-06-23 21:23:17 +08:00
parent 17b6f98833
commit 7709f3cb8a
7 changed files with 117 additions and 43 deletions

View file

@ -5,10 +5,12 @@
"repository": "https://github.com/logto-io/schemas",
"author": "Logto",
"license": "UNLICENSED",
"files": ["lib"],
"scripts": {
"generate": "ts-node src/gen/index.ts",
"build": "rm -rf lib/ && tsc --p tsconfig.build.json",
"lint": "xo src/"
"build": "yarn generate && rm -rf lib/ && tsc --p tsconfig.build.json",
"lint": "xo src/",
"prepare": "yarn build"
},
"engines": {
"node": "14",
@ -16,6 +18,9 @@
},
"devDependencies": {
"@types/node": "14",
"@types/pluralize": "^0.0.29",
"camelcase": "^6.2.0",
"pluralize": "^8.0.0",
"ts-node": "^10.0.0",
"typescript": "^4.3.4",
"xo": "0.39.1"

View file

@ -0,0 +1,3 @@
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
export * from './oidc-model-instance';

View file

@ -0,0 +1,11 @@
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
export type OidcModelInstanceDBEntry = {
modelName: string;
id: string;
payload: Record<string, unknown>;
expiresAt: number;
userCode?: string;
uid?: string;
grantId?: string;
};

View file

@ -1,8 +1,16 @@
import assert from 'assert';
import camelcase from 'camelcase';
import fs from 'fs/promises';
import path from 'path';
import pluralize from 'pluralize';
import { findFirstParentheses, getType, normalizeWhitespaces, removeParentheses } from './utils';
import {
conditionalString,
findFirstParentheses,
getType,
normalizeWhitespaces,
removeParentheses,
} from './utils';
type Field = {
name: string;
@ -21,47 +29,85 @@ const dir = 'tables';
const generate = async () => {
const files = await fs.readdir(dir);
const generated = await Promise.all(
files.map(async (file) => {
return (await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }))
.split(';')
.map((value) => normalizeWhitespaces(value).toLowerCase())
.filter((value) => value.startsWith('create table'))
.map((value) => findFirstParentheses(value))
.filter((value): value is NonNullable<typeof value> => Boolean(value))
.map<Table>(({ prefix, body }) => {
const name = normalizeWhitespaces(prefix).split(' ')[2];
assert(name, 'Missing table name: ' + prefix);
files
.filter((file) => file.endsWith('.sql'))
.map<Promise<[string, Table[]]>>(async (file) => [
file,
(
await fs.readFile(path.join(dir, file), { encoding: 'utf-8' })
)
.split(';')
.map((value) => normalizeWhitespaces(value).toLowerCase())
.filter((value) => value.startsWith('create table'))
.map((value) => findFirstParentheses(value))
.filter((value): value is NonNullable<typeof value> => Boolean(value))
.map<Table>(({ prefix, body }) => {
const name = normalizeWhitespaces(prefix).split(' ')[2];
assert(name, 'Missing table name: ' + prefix);
const fields = removeParentheses(body)
.split(',')
.map((value) => normalizeWhitespaces(value))
.filter((value) =>
['primary', 'foreign', 'unique', 'exclude', 'check'].every(
(constraint) => !value.startsWith(constraint)
const fields = removeParentheses(body)
.split(',')
.map((value) => normalizeWhitespaces(value))
.filter((value) =>
['primary', 'foreign', 'unique', 'exclude', 'check'].every(
(constraint) => !value.startsWith(constraint)
)
)
)
.map<Field>((value) => {
const [name, type, ...rest] = value.split(' ');
assert(name && type, 'Missing column name or type: ' + value);
.map<Field>((value) => {
const [name, type, ...rest] = value.split(' ');
assert(name && type, 'Missing column name or type: ' + value);
const restJoined = rest.join(' ');
// CAUTION: Only works for single dimension arrays
const isArray = Boolean(/\[.*]/.test(type)) || restJoined.includes('array');
const required = restJoined.includes('not null');
const restJoined = rest.join(' ');
// CAUTION: Only works for single dimension arrays
const isArray = Boolean(/\[.*]/.test(type)) || restJoined.includes('array');
const required = restJoined.includes('not null');
return {
name,
type: getType(type),
isArray,
required,
};
});
return { name, fields };
});
return {
name,
type: getType(type),
isArray,
required,
};
});
return { name, fields };
}),
])
);
const generatedDir = 'src/db-entries';
const header = '// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n\n';
const getOutputFileName = (file: string) => pluralize(file.slice(0, -4).replace(/_/g, '-'), 1);
await fs.rmdir(generatedDir, { recursive: true });
await fs.mkdir(generatedDir, { recursive: true });
await Promise.all(
generated.map(async ([file, tables]) => {
const content =
header +
tables
.map(({ name, fields }) =>
[
`export type ${pluralize(camelcase(name, { pascalCase: true }), 1)}DBEntry = {`,
...fields.map(
({ name, type, isArray, required }) =>
` ${camelcase(name)}${conditionalString(
!required && '?'
)}: ${type}${conditionalString(isArray && '[]')};`
),
'};',
].join('\n')
)
.join('\n') +
'\n';
await fs.writeFile(path.join(generatedDir, getOutputFileName(file) + '.ts'), content);
})
);
const tables = generated.flat();
console.log(tables);
await fs.writeFile(
path.join(generatedDir, 'index.ts'),
header +
generated.map(([file]) => `export * from './${getOutputFileName(file)}';`).join('\n') +
'\n'
);
};
void generate();

View file

@ -1,5 +1,9 @@
import { Optional } from '../global';
export type Falsy = 0 | undefined | null | false | '';
export const conditionalString = (value: string | Falsy): string => (value ? value : '');
export const normalizeWhitespaces = (string: string): string => string.replace(/\s+/g, ' ').trim();
const getCountDelta = (value: string): number => {
@ -80,7 +84,9 @@ const getRawType = (value: string): string => {
// Reference: https://github.com/SweetIQ/schemats/blob/7c3d3e16b5d507b4d9bd246794e7463b05d20e75/src/schemaPostgres.ts
// eslint-disable-next-line complexity
export const getType = (value: string): 'string' | 'number' | 'boolean' | 'Object' | 'Date' => {
export const getType = (
value: string
): 'string' | 'number' | 'boolean' | 'Record<string, unknown>' | 'Date' => {
switch (getRawType(value)) {
case 'bpchar':
case 'char':
@ -109,7 +115,7 @@ export const getType = (value: string): 'string' | 'number' | 'boolean' | 'Objec
return 'boolean';
case 'json':
case 'jsonb':
return 'Object';
return 'Record<string, unknown>';
case 'date':
case 'timestamp':
case 'timestamptz':

View file

@ -1,3 +1 @@
export type A = {
foo: string;
};
export * from './db-entries';

View file

@ -359,6 +359,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/pluralize@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/pluralize/-/pluralize-0.0.29.tgz#6ffa33ed1fc8813c469b859681d09707eb40d03c"
integrity sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==
"@typescript-eslint/eslint-plugin@^4.22.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz#1a66f03b264844387beb7dc85e1f1d403bd1803f"