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:
parent
b133b30b90
commit
7ce706ccbe
17 changed files with 146 additions and 61 deletions
|
@ -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`, `
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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 } });
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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()'
|
||||
)},`;
|
||||
}),
|
||||
' });',
|
||||
|
|
|
@ -3,7 +3,8 @@ export type Field = {
|
|||
type?: string;
|
||||
customType?: string;
|
||||
tsType?: string;
|
||||
required: boolean;
|
||||
hasDefaultValue: boolean;
|
||||
nullable: boolean;
|
||||
isArray: boolean;
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue