0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

feat: generate 2 types for db table schema (#154)

* feat(schema): generate Model and DBEntry

* feat: generate 2 types for db table
This commit is contained in:
Wang Sijie 2021-12-08 11:11:27 +08:00 committed by GitHub
parent b133b30b90
commit 7ce706ccbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 146 additions and 61 deletions

View file

@ -33,11 +33,11 @@ type InsertIntoConfig = {
};
interface BuildInsertInto {
<Schema extends SchemaLike>(
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
pool: DatabasePoolType,
{ fieldKeys, ...rest }: GeneratedSchema<Schema>,
config: InsertIntoConfigReturning
): (data: OmitAutoSetFields<Schema>) => Promise<Schema>;
): (data: OmitAutoSetFields<Schema>) => Promise<ReturnType>;
<Schema extends SchemaLike>(
pool: DatabasePoolType,
{ fieldKeys, ...rest }: GeneratedSchema<Schema>,
@ -45,7 +45,10 @@ interface BuildInsertInto {
): (data: OmitAutoSetFields<Schema>) => Promise<void>;
}
export const buildInsertInto: BuildInsertInto = <Schema extends SchemaLike>(
export const buildInsertInto: BuildInsertInto = <
Schema extends SchemaLike,
ReturnType extends SchemaLike
>(
pool: DatabasePoolType,
{ fieldKeys, ...rest }: GeneratedSchema<Schema>,
config?: InsertIntoConfig | InsertIntoConfigReturning
@ -55,10 +58,10 @@ export const buildInsertInto: BuildInsertInto = <Schema extends SchemaLike>(
const returning = Boolean(config?.returning);
const onConflict = config?.onConflict;
return async (data: OmitAutoSetFields<Schema>): Promise<Schema | void> => {
return async (data: OmitAutoSetFields<Schema>): Promise<ReturnType | void> => {
const {
rows: [entry],
} = await pool.query<Schema>(sql`
} = await pool.query<ReturnType>(sql`
insert into ${table} (${sql.join(
keys.map((key) => fields[key]),
sql`, `

View file

@ -14,11 +14,11 @@ export type UpdateWhereData<Schema extends SchemaLike> = {
};
interface BuildUpdateWhere {
<Schema extends SchemaLike>(
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
returning: true
): (data: UpdateWhereData<Schema>) => Promise<Schema>;
): (data: UpdateWhereData<Schema>) => Promise<ReturnType>;
<Schema extends SchemaLike>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
@ -26,7 +26,10 @@ interface BuildUpdateWhere {
): (data: UpdateWhereData<Schema>) => Promise<void>;
}
export const buildUpdateWhere: BuildUpdateWhere = <Schema extends SchemaLike>(
export const buildUpdateWhere: BuildUpdateWhere = <
Schema extends SchemaLike,
ReturnType extends SchemaLike
>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
returning = false
@ -43,8 +46,8 @@ export const buildUpdateWhere: BuildUpdateWhere = <Schema extends SchemaLike>(
return async ({ set, where }: UpdateWhereData<Schema>) => {
const {
rows: [entry],
} = await pool.query<Schema>(sql`
rows: [data],
} = await pool.query<ReturnType>(sql`
update ${table}
set ${sql.join(connectKeyValueWithEqualSign(set), sql`, `)}
where ${sql.join(connectKeyValueWithEqualSign(where), sql` and `)}
@ -52,7 +55,7 @@ export const buildUpdateWhere: BuildUpdateWhere = <Schema extends SchemaLike>(
`);
assertThat(
!returning || entry,
!returning || data,
new RequestError({
code: where.id ? 'entity.not_exists_with_id' : 'entity.not_exists',
name: schema.tableSingular,
@ -60,6 +63,6 @@ export const buildUpdateWhere: BuildUpdateWhere = <Schema extends SchemaLike>(
status: 404,
})
);
return entry;
return data;
};
};

View file

@ -1,4 +1,4 @@
import { ApplicationDBEntry, Applications } from '@logto/schemas';
import { Application, ApplicationDBEntry, Applications } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
@ -10,23 +10,31 @@ import RequestError from '@/errors/RequestError';
const { table, fields } = convertToIdentifiers(Applications);
export const findAllApplications = async () =>
pool.many<ApplicationDBEntry>(sql`
pool.many<Application>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
`);
export const findApplicationById = async (id: string) =>
pool.one<ApplicationDBEntry>(sql`
pool.one<Application>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=${id}
`);
export const insertApplication = buildInsertInto<ApplicationDBEntry>(pool, Applications, {
returning: true,
});
export const insertApplication = buildInsertInto<ApplicationDBEntry, Application>(
pool,
Applications,
{
returning: true,
}
);
const updateApplication = buildUpdateWhere<ApplicationDBEntry>(pool, Applications, true);
const updateApplication = buildUpdateWhere<ApplicationDBEntry, Application>(
pool,
Applications,
true
);
export const updateApplicationById = async (
id: string,

View file

@ -1,4 +1,5 @@
import {
OidcModelInstance,
OidcModelInstanceDBEntry,
OidcModelInstancePayload,
OidcModelInstances,
@ -11,11 +12,11 @@ import pool from '@/database/pool';
import { convertToIdentifiers, convertToTimestamp } from '@/database/utils';
export type WithConsumed<T> = T & { consumed?: boolean };
export type QueryResult = Pick<OidcModelInstanceDBEntry, 'payload' | 'consumedAt'>;
export type QueryResult = Pick<OidcModelInstance, 'payload' | 'consumedAt'>;
const { table, fields } = convertToIdentifiers(OidcModelInstances);
const withConsumed = <T>(data: T, consumedAt?: number): WithConsumed<T> => ({
const withConsumed = <T>(data: T, consumedAt?: number | null): WithConsumed<T> => ({
...data,
...(consumedAt ? { consumed: true } : undefined),
});

View file

@ -1,4 +1,4 @@
import { ResourceDBEntry, Resources } from '@logto/schemas';
import { Resource, ResourceDBEntry, Resources } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
@ -10,7 +10,7 @@ import RequestError from '@/errors/RequestError';
const { table, fields } = convertToIdentifiers(Resources);
export const findAllResources = async () =>
pool.many<ResourceDBEntry>(sql`
pool.many<Resource>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
`);
@ -30,24 +30,24 @@ export const hasResourceWithId = async (id: string) =>
`);
export const findResourceByIdentifier = async (indentifier: string) =>
pool.maybeOne<ResourceDBEntry>(sql`
pool.maybeOne<Resource>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.identifier}=${indentifier}
`);
export const findResourceById = async (id: string) =>
pool.one<ResourceDBEntry>(sql`
pool.one<Resource>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.id}=${id}
`);
export const insertResource = buildInsertInto<ResourceDBEntry>(pool, Resources, {
export const insertResource = buildInsertInto<ResourceDBEntry, Resource>(pool, Resources, {
returning: true,
});
const updateResource = buildUpdateWhere<ResourceDBEntry>(pool, Resources, true);
const updateResource = buildUpdateWhere<ResourceDBEntry, Resource>(pool, Resources, true);
export const updateResourceById = async (
id: string,

View file

@ -1,4 +1,4 @@
import { ResourceScopeDBEntry, ResourceScopes } from '@logto/schemas';
import { ResourceScope, ResourceScopeDBEntry, ResourceScopes } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
@ -9,15 +9,19 @@ import RequestError from '@/errors/RequestError';
const { table, fields } = convertToIdentifiers(ResourceScopes);
export const findAllScopesWithResourceId = async (resourceId: string) =>
pool.any<ResourceScopeDBEntry>(sql`
pool.any<ResourceScope>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.resourceId}=${resourceId}
`);
export const insertScope = buildInsertInto<ResourceScopeDBEntry>(pool, ResourceScopes, {
returning: true,
});
export const insertScope = buildInsertInto<ResourceScopeDBEntry, ResourceScope>(
pool,
ResourceScopes,
{
returning: true,
}
);
export const deleteScopeById = async (id: string) => {
const { rowCount } = await pool.query(sql`

View file

@ -1,4 +1,4 @@
import { UserDBEntry, Users } from '@logto/schemas';
import { User, UserDBEntry, Users } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
@ -10,14 +10,14 @@ import RequestError from '@/errors/RequestError';
const { table, fields } = convertToIdentifiers(Users);
export const findUserByUsername = async (username: string) =>
pool.one<UserDBEntry>(sql`
pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.username}=${username}
`);
export const findUserById = async (id: string) =>
pool.one<UserDBEntry>(sql`
pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.id}=${id}
@ -37,15 +37,15 @@ export const hasUserWithId = async (id: string) =>
where ${fields.id}=${id}
`);
export const insertUser = buildInsertInto<UserDBEntry>(pool, Users, { returning: true });
export const insertUser = buildInsertInto<UserDBEntry, User>(pool, Users, { returning: true });
export const findAllUsers = async () =>
pool.many<UserDBEntry>(sql`
pool.many<User>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
`);
const updateUser = buildUpdateWhere<UserDBEntry>(pool, Users, true);
const updateUser = buildUpdateWhere<UserDBEntry, User>(pool, Users, true);
export const updateUserById = async (id: string, set: Partial<OmitAutoSetFields<UserDBEntry>>) =>
updateUser({ set, where: { id } });

View file

@ -11,6 +11,14 @@ import {
import { ApplicationType } from './custom-types';
export type ApplicationDBEntry = {
id: string;
name: string;
type: ApplicationType;
oidcClientMetadata: OidcClientMetadata;
createdAt?: number;
};
export type Application = {
id: string;
name: string;
type: ApplicationType;
@ -23,7 +31,7 @@ const guard: Guard<ApplicationDBEntry> = z.object({
name: z.string(),
type: z.nativeEnum(ApplicationType),
oidcClientMetadata: oidcClientMetadataGuard,
createdAt: z.number(),
createdAt: z.number().optional(),
});
export const Applications: GeneratedSchema<ApplicationDBEntry> = Object.freeze({

View file

@ -14,7 +14,15 @@ export type OidcModelInstanceDBEntry = {
id: string;
payload: OidcModelInstancePayload;
expiresAt: number;
consumedAt?: number;
consumedAt?: number | null;
};
export type OidcModelInstance = {
modelName: string;
id: string;
payload: OidcModelInstancePayload;
expiresAt: number;
consumedAt: number | null;
};
const guard: Guard<OidcModelInstanceDBEntry> = z.object({

View file

@ -11,6 +11,13 @@ export type ResourceScopeDBEntry = {
resourceId: string;
};
export type ResourceScope = {
id: string;
name: string;
description: string;
resourceId: string;
};
const guard: Guard<ResourceScopeDBEntry> = z.object({
id: z.string(),
name: z.string(),

View file

@ -6,6 +6,15 @@ import { GeneratedSchema, Guard } from '../foundations';
import { AccessTokenFormatType, SignAlgorithmType } from './custom-types';
export type ResourceDBEntry = {
id: string;
name: string;
identifier: string;
accessTokenTtl?: number;
accessTokenFormat?: AccessTokenFormatType;
signAlgorithm?: SignAlgorithmType;
};
export type Resource = {
id: string;
name: string;
identifier: string;
@ -18,9 +27,9 @@ const guard: Guard<ResourceDBEntry> = z.object({
id: z.string(),
name: z.string(),
identifier: z.string(),
accessTokenTtl: z.number(),
accessTokenFormat: z.nativeEnum(AccessTokenFormatType),
signAlgorithm: z.nativeEnum(SignAlgorithmType),
accessTokenTtl: z.number().optional(),
accessTokenFormat: z.nativeEnum(AccessTokenFormatType).optional(),
signAlgorithm: z.nativeEnum(SignAlgorithmType).optional(),
});
export const Resources: GeneratedSchema<ResourceDBEntry> = Object.freeze({

View file

@ -6,6 +6,15 @@ import { UserLogPayload, userLogPayloadGuard, GeneratedSchema, Guard } from '../
import { UserLogType, UserLogResult } from './custom-types';
export type UserLogDBEntry = {
id: string;
userId: string;
type: UserLogType;
result: UserLogResult;
payload: UserLogPayload;
createdAt?: number;
};
export type UserLog = {
id: string;
userId: string;
type: UserLogType;
@ -20,7 +29,7 @@ const guard: Guard<UserLogDBEntry> = z.object({
type: z.nativeEnum(UserLogType),
result: z.nativeEnum(UserLogResult),
payload: userLogPayloadGuard,
createdAt: z.number(),
createdAt: z.number().optional(),
});
export const UserLogs: GeneratedSchema<UserLogDBEntry> = Object.freeze({

View file

@ -7,12 +7,22 @@ import { PasswordEncryptionMethod } from './custom-types';
export type UserDBEntry = {
id: string;
username?: string;
primaryEmail?: string;
primaryPhone?: string;
passwordEncrypted?: string;
passwordEncryptionMethod?: PasswordEncryptionMethod;
passwordEncryptionSalt?: string;
username?: string | null;
primaryEmail?: string | null;
primaryPhone?: string | null;
passwordEncrypted?: string | null;
passwordEncryptionMethod?: PasswordEncryptionMethod | null;
passwordEncryptionSalt?: string | null;
};
export type User = {
id: string;
username: string | null;
primaryEmail: string | null;
primaryPhone: string | null;
passwordEncrypted: string | null;
passwordEncryptionMethod: PasswordEncryptionMethod | null;
passwordEncryptionSalt: string | null;
};
const guard: Guard<UserDBEntry> = z.object({

View file

@ -9,7 +9,7 @@ export type Guard<T extends Record<string, unknown>> = ZodObject<
>;
export type SchemaValuePrimitive = string | number | boolean | undefined;
export type SchemaValue = SchemaValuePrimitive | Record<string, unknown>;
export type SchemaValue = SchemaValuePrimitive | Record<string, unknown> | null;
export type SchemaLike<Key extends string = string> = {
[key in Key]: SchemaValue;
};

View file

@ -65,7 +65,8 @@ const generate = async () => {
const restLowercased = restJoined.toLowerCase();
// CAUTION: Only works for single dimension arrays
const isArray = Boolean(/\[.*]/.test(type)) || restLowercased.includes('array');
const required = restLowercased.includes('not null');
const hasDefaultValue = restLowercased.includes('default');
const nullable = !restLowercased.includes('not null');
const primitiveType = getType(type);
const tsType = /\/\* @use (.*) \*\//.exec(restJoined)?.[1];
assert(
@ -81,7 +82,8 @@ const generate = async () => {
customType: conditional(!primitiveType && type),
tsType,
isArray,
required,
hasDefaultValue,
nullable,
};
});
return { name, fields };

View file

@ -5,29 +5,41 @@ import pluralize from 'pluralize';
import { TableWithType } from './types';
export const generateSchema = ({ name, fields }: TableWithType) => {
const databaseEntryType = `${pluralize(camelcase(name, { pascalCase: true }), 1)}DBEntry`;
const modelName = pluralize(camelcase(name, { pascalCase: true }), 1);
const databaseEntryType = `${modelName}DBEntry`;
return [
`export type ${databaseEntryType} = {`,
...fields.map(
({ name, type, isArray, required }) =>
` ${camelcase(name)}${conditionalString(!required && '?')}: ${type}${conditionalString(
isArray && '[]'
({ name, type, isArray, nullable, hasDefaultValue }) =>
` ${camelcase(name)}${conditionalString(
(nullable || hasDefaultValue) && '?'
)}: ${type}${conditionalString(isArray && '[]')}${conditionalString(
nullable && !hasDefaultValue && ' | null'
)};`
),
'};',
'',
`export type ${modelName} = {`,
...fields.map(
({ name, type, isArray, nullable, hasDefaultValue }) =>
` ${camelcase(name)}: ${type}${conditionalString(isArray && '[]')}${
nullable && !hasDefaultValue ? ' | null' : ''
};`
),
'};',
'',
`const guard: Guard<${databaseEntryType}> = z.object({`,
...fields.map(({ name, type, isArray, isEnum, required, tsType }) => {
...fields.map(({ name, type, isArray, isEnum, nullable, hasDefaultValue, tsType }) => {
if (tsType) {
return ` ${camelcase(name)}: ${camelcase(tsType)}Guard${conditionalString(
!required && '.optional()'
(nullable || hasDefaultValue) && '.optional()'
)},`;
}
return ` ${camelcase(name)}: z.${
isEnum ? `nativeEnum(${type})` : `${type}()`
}${conditionalString(isArray && '.array()')}${conditionalString(
!required && '.optional()'
(nullable || hasDefaultValue) && '.optional()'
)},`;
}),
' });',

View file

@ -3,7 +3,8 @@ export type Field = {
type?: string;
customType?: string;
tsType?: string;
required: boolean;
hasDefaultValue: boolean;
nullable: boolean;
isArray: boolean;
};