0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

refactor: fix schema types (#3379)

by separating CreateSchema and Schema. also no need for explicitly
set generic type for database builders like `buildInsertIntoWithPool()`.
This commit is contained in:
Gao Sun 2023-03-14 12:45:13 +08:00 committed by GitHub
parent 68ca4ae7a8
commit 0b45c8f487
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 109 additions and 117 deletions

View file

@ -9,8 +9,8 @@ import { isKeyOf } from '#src/utils/schema.js';
export const buildFindEntityByIdWithPool =
(pool: CommonQueryMethods) =>
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
schema: GeneratedSchema<Schema & { id: string }>
<CreateSchema extends SchemaLike, Schema extends CreateSchema>(
schema: GeneratedSchema<CreateSchema, Schema & { id: string }>
) => {
const { table, fields } = convertToIdentifiers(schema);
const isKeyOfSchema = isKeyOf(schema);
@ -20,7 +20,7 @@ export const buildFindEntityByIdWithPool =
return async (id: string) => {
try {
return await pool.one<ReturnType>(sql`
return await pool.one<Schema>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=${id}

View file

@ -35,20 +35,20 @@ type InsertIntoConfig = {
};
type BuildInsertInto = {
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
{ fieldKeys, ...rest }: GeneratedSchema<Schema>,
<CreateSchema extends SchemaLike, Schema extends CreateSchema>(
{ fieldKeys, ...rest }: GeneratedSchema<CreateSchema, Schema>,
config: InsertIntoConfigReturning
): (data: OmitAutoSetFields<Schema>) => Promise<ReturnType>;
<Schema extends SchemaLike>(
{ fieldKeys, ...rest }: GeneratedSchema<Schema>,
): (data: OmitAutoSetFields<CreateSchema>) => Promise<Schema>;
<CreateSchema extends SchemaLike, Schema extends CreateSchema>(
{ fieldKeys, ...rest }: GeneratedSchema<CreateSchema, Schema>,
config?: InsertIntoConfig
): (data: OmitAutoSetFields<Schema>) => Promise<void>;
): (data: OmitAutoSetFields<CreateSchema>) => Promise<void>;
};
export const buildInsertIntoWithPool =
(pool: CommonQueryMethods): BuildInsertInto =>
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
schema: GeneratedSchema<Schema>,
<CreateSchema extends SchemaLike, Schema extends CreateSchema>(
schema: GeneratedSchema<CreateSchema, Schema>,
config?: InsertIntoConfig | InsertIntoConfigReturning
) => {
const { fieldKeys, ...rest } = schema;
@ -57,11 +57,11 @@ export const buildInsertIntoWithPool =
const returning = Boolean(config?.returning);
const onConflict = config?.onConflict;
return async (data: OmitAutoSetFields<Schema>): Promise<ReturnType | void> => {
return async (data: OmitAutoSetFields<CreateSchema>): Promise<Schema | void> => {
const insertingKeys = keys.filter((key) => has(data, key));
const {
rows: [entry],
} = await pool.query<ReturnType>(sql`
} = await pool.query<Schema>(sql`
insert into ${table} (${sql.join(
insertingKeys.map((key) => fields[key]),
sql`, `
@ -80,7 +80,7 @@ export const buildInsertIntoWithPool =
${conditionalSql(returning, () => sql`returning *`)}
`);
assertThat(!returning || entry, new InsertionError(schema, data));
assertThat(!returning || entry, new InsertionError<CreateSchema, Schema>(schema, data));
return entry;
};

View file

@ -11,19 +11,20 @@ import assertThat from '#src/utils/assert-that.js';
import { isKeyOf } from '#src/utils/schema.js';
type BuildUpdateWhere = {
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
schema: GeneratedSchema<Schema>,
<CreateSchema extends SchemaLike, Schema extends CreateSchema>(
schema: GeneratedSchema<CreateSchema, Schema>,
returning: true
): (data: UpdateWhereData<Schema>) => Promise<ReturnType>;
<Schema extends SchemaLike>(schema: GeneratedSchema<Schema>, returning?: false): (
data: UpdateWhereData<Schema>
) => Promise<void>;
): (data: UpdateWhereData<Schema>) => Promise<Schema>;
<CreateSchema extends SchemaLike, Schema extends CreateSchema>(
schema: GeneratedSchema<CreateSchema, Schema>,
returning?: false
): (data: UpdateWhereData<Schema>) => Promise<void>;
};
export const buildUpdateWhereWithPool =
(pool: CommonQueryMethods): BuildUpdateWhere =>
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
schema: GeneratedSchema<Schema>,
<CreateSchema extends SchemaLike, Schema extends CreateSchema>(
schema: GeneratedSchema<CreateSchema, Schema>,
returning = false
) => {
const { table, fields } = convertToIdentifiers(schema);
@ -59,7 +60,7 @@ export const buildUpdateWhereWithPool =
return async ({ set, where, jsonbMode }: UpdateWhereData<Schema>) => {
const {
rows: [data],
} = await pool.query<ReturnType>(sql`
} = await pool.query<Schema>(sql`
update ${table}
set ${sql.join(connectKeyValueWithEqualSign(set, jsonbMode), sql`, `)}
where ${sql.join(connectKeyValueWithEqualSign(where, jsonbMode), sql` and `)}

View file

@ -14,11 +14,17 @@ export class DeletionError extends SlonikError {
}
}
export class UpdateError<Schema extends SchemaLike> extends SlonikError {
schema: GeneratedSchema<Schema>;
export class UpdateError<
CreateSchema extends SchemaLike,
Schema extends CreateSchema
> extends SlonikError {
schema: GeneratedSchema<CreateSchema, Schema>;
detail: UpdateWhereData<Schema>;
public constructor(schema: GeneratedSchema<Schema>, detail: UpdateWhereData<Schema>) {
public constructor(
schema: GeneratedSchema<CreateSchema, Schema>,
detail: UpdateWhereData<Schema>
) {
super('Resource not found.');
this.schema = schema;
@ -26,11 +32,17 @@ export class UpdateError<Schema extends SchemaLike> extends SlonikError {
}
}
export class InsertionError<Schema extends SchemaLike> extends SlonikError {
schema: GeneratedSchema<Schema>;
detail?: OmitAutoSetFields<Schema>;
export class InsertionError<
CreateSchema extends SchemaLike,
Schema extends CreateSchema
> extends SlonikError {
schema: GeneratedSchema<CreateSchema, Schema>;
detail?: OmitAutoSetFields<CreateSchema>;
public constructor(schema: GeneratedSchema<Schema>, detail?: OmitAutoSetFields<Schema>) {
public constructor(
schema: GeneratedSchema<CreateSchema, Schema>,
detail?: OmitAutoSetFields<CreateSchema>
) {
super('Create Error.');
this.schema = schema;

View file

@ -74,7 +74,7 @@ export const createUserLibrary = (queries: Queries) => {
{ retries, factor: 0 } // No need for exponential backoff
);
const insertUserQuery = buildInsertIntoWithPool(pool)<CreateUser, User>(Users, {
const insertUserQuery = buildInsertIntoWithPool(pool)(Users, {
returning: true,
});

View file

@ -40,7 +40,7 @@ export default function koaSlonikErrorHandler<StateT, ContextT>(): Middleware<St
code: 'entity.create_failed',
// Assert generic type of the Class instance
// eslint-disable-next-line no-restricted-syntax
name: (error as InsertionError<SchemaLike>).schema.tableSingular,
name: (error as InsertionError<SchemaLike, SchemaLike>).schema.tableSingular,
});
}
@ -49,7 +49,7 @@ export default function koaSlonikErrorHandler<StateT, ContextT>(): Middleware<St
code: 'entity.not_exists',
// Assert generic type of the Class instance
// eslint-disable-next-line no-restricted-syntax
name: (error as UpdateError<SchemaLike>).schema.tableSingular,
name: (error as UpdateError<SchemaLike, SchemaLike>).schema.tableSingular,
});
}

View file

@ -25,19 +25,11 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
${conditionalSql(offset, (offset) => sql`offset ${offset}`)}
`)
);
const findApplicationById = buildFindEntityByIdWithPool(pool)<CreateApplication, Application>(
Applications
);
const insertApplication = buildInsertIntoWithPool(pool)<CreateApplication, Application>(
Applications,
{
returning: true,
}
);
const updateApplication = buildUpdateWhereWithPool(pool)<CreateApplication, Application>(
Applications,
true
);
const findApplicationById = buildFindEntityByIdWithPool(pool)(Applications);
const insertApplication = buildInsertIntoWithPool(pool)(Applications, {
returning: true,
});
const updateApplication = buildUpdateWhereWithPool(pool)(Applications, true);
const updateApplicationById = async (
id: string,
set: Partial<OmitAutoSetFields<CreateApplication>>

View file

@ -1,4 +1,4 @@
import type { Connector, CreateConnector } from '@logto/schemas';
import type { Connector } from '@logto/schemas';
import { Connectors } from '@logto/schemas';
import { manyRows, convertToIdentifiers } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik';
@ -71,13 +71,10 @@ export const createConnectorQueries = (pool: CommonQueryMethods) => {
throw new DeletionError(Connectors.table, JSON.stringify({ ids }));
}
};
const insertConnector = buildInsertIntoWithPool(pool)<CreateConnector, Connector>(Connectors, {
const insertConnector = buildInsertIntoWithPool(pool)(Connectors, {
returning: true,
});
const updateConnector = buildUpdateWhereWithPool(pool)<CreateConnector, Connector>(
Connectors,
true
);
const updateConnector = buildUpdateWhereWithPool(pool)(Connectors, true);
return {
findAllConnectors,

View file

@ -1,4 +1,4 @@
import type { CreateCustomPhrase, CustomPhrase } from '@logto/schemas';
import type { CustomPhrase } from '@logto/schemas';
import { CustomPhrases } from '@logto/schemas';
import { convertToIdentifiers, manyRows } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik';
@ -38,16 +38,13 @@ export const createCustomPhraseQueries = (pool: CommonQueryMethods) => {
where ${fields.languageTag} = ${languageTag}
`);
const upsertCustomPhrase = buildInsertIntoWithPool(pool)<CreateCustomPhrase, CustomPhrase>(
CustomPhrases,
{
returning: true,
onConflict: {
fields: [fields.tenantId, fields.languageTag],
setExcludedFields: [fields.translation],
},
}
);
const upsertCustomPhrase = buildInsertIntoWithPool(pool)(CustomPhrases, {
returning: true,
onConflict: {
fields: [fields.tenantId, fields.languageTag],
setExcludedFields: [fields.translation],
},
});
const deleteCustomPhraseByLanguageTag = async (languageTag: string) => {
const { rowCount } = await pool.query(sql`

View file

@ -21,13 +21,13 @@ export const createHooksQueries = (pool: CommonQueryMethods) => {
`)
);
const findHookById = buildFindEntityByIdWithPool(pool)<CreateHook, Hook>(Hooks);
const findHookById = buildFindEntityByIdWithPool(pool)(Hooks);
const insertHook = buildInsertIntoWithPool(pool)<CreateHook, Hook>(Hooks, {
const insertHook = buildInsertIntoWithPool(pool)(Hooks, {
returning: true,
});
const updateHook = buildUpdateWhereWithPool(pool)<CreateHook, Hook>(Hooks, true);
const updateHook = buildUpdateWhereWithPool(pool)(Hooks, true);
const updateHookById = async (
id: string,

View file

@ -1,4 +1,4 @@
import type { CreateLog, Log } from '@logto/schemas';
import type { Log } from '@logto/schemas';
import { token, Logs } from '@logto/schemas';
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik';
@ -30,7 +30,7 @@ const buildLogConditionSql = (logCondition: LogCondition) =>
});
export const createLogQueries = (pool: CommonQueryMethods) => {
const insertLog = buildInsertIntoWithPool(pool)<CreateLog>(Logs);
const insertLog = buildInsertIntoWithPool(pool)(Logs);
const countLogs = async (condition: LogCondition) =>
pool.one<{ count: number }>(sql`
@ -49,7 +49,7 @@ export const createLogQueries = (pool: CommonQueryMethods) => {
offset ${offset}
`);
const findLogById = buildFindEntityByIdWithPool(pool)<CreateLog, Log>(Logs);
const findLogById = buildFindEntityByIdWithPool(pool)(Logs);
const getDailyActiveUserCountsByTimeInterval = async (
startTimeExclusive: number,

View file

@ -1,8 +1,4 @@
import type {
OidcModelInstance,
CreateOidcModelInstance,
OidcModelInstancePayload,
} from '@logto/schemas';
import type { OidcModelInstance, OidcModelInstancePayload } from '@logto/schemas';
import { OidcModelInstances } from '@logto/schemas';
import { convertToIdentifiers, convertToTimestamp } from '@logto/shared';
import type { Nullable } from '@silverhand/essentials';
@ -59,15 +55,12 @@ const findByModel = (modelName: string) => sql`
`;
export const createOidcModelInstanceQueries = (pool: CommonQueryMethods) => {
const upsertInstance = buildInsertIntoWithPool(pool)<CreateOidcModelInstance>(
OidcModelInstances,
{
onConflict: {
fields: [fields.tenantId, fields.modelName, fields.id],
setExcludedFields: [fields.payload, fields.expiresAt],
},
}
);
const upsertInstance = buildInsertIntoWithPool(pool)(OidcModelInstances, {
onConflict: {
fields: [fields.tenantId, fields.modelName, fields.id],
setExcludedFields: [fields.payload, fields.expiresAt],
},
});
const findPayloadById = async (modelName: string, id: string) => {
const result = await pool.maybeOne<QueryResult>(sql`

View file

@ -1,5 +1,5 @@
import type { VerificationCodeType } from '@logto/connector-kit';
import type { Passcode, CreatePasscode, RequestVerificationCodePayload } from '@logto/schemas';
import type { Passcode, RequestVerificationCodePayload } from '@logto/schemas';
import { Passcodes } from '@logto/schemas';
import { conditionalSql, convertToIdentifiers } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik';
@ -54,7 +54,7 @@ export const createPasscodeQueries = (pool: CommonQueryMethods) => {
properties: FindByIdentifierAndTypeProperties
) => pool.any<Passcode>(buildSqlForFindByIdentifierAndType(properties));
const insertPasscode = buildInsertIntoWithPool(pool)<CreatePasscode, Passcode>(Passcodes, {
const insertPasscode = buildInsertIntoWithPool(pool)(Passcodes, {
returning: true,
});

View file

@ -111,10 +111,11 @@ describe('resource query', () => {
});
it('insertResource', async () => {
const insertFields = Object.values(fields).filter((field) => field.names[0] !== 'tenant_id');
const expectSql = sql`
insert into ${table} (${sql.join(Object.values(fields), sql`, `)})
insert into ${table} (${sql.join(insertFields, sql`, `)})
values (${sql.join(
Object.values(fields).map((_, index) => `$${index + 1}`),
insertFields.map((_, index) => `$${index + 1}`),
sql`, `
)})
returning *
@ -124,7 +125,9 @@ describe('resource query', () => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual(
Resources.fieldKeys.map((k) => convertToPrimitiveOrSql(k, mockResource[k]))
Resources.fieldKeys
.filter((key) => key !== 'tenantId')
.map((k) => convertToPrimitiveOrSql(k, mockResource[k]))
);
return createMockQueryResult([mockResource]);

View file

@ -33,7 +33,7 @@ export const createResourceQueries = (pool: CommonQueryMethods) => {
where ${fields.indicator}=${indicator}
`);
const findResourceById = buildFindEntityByIdWithPool(pool)<CreateResource, Resource>(Resources);
const findResourceById = buildFindEntityByIdWithPool(pool)(Resources);
const findResourcesByIds = async (resourceIds: string[]) =>
resourceIds.length > 0
@ -44,11 +44,11 @@ export const createResourceQueries = (pool: CommonQueryMethods) => {
`)
: [];
const insertResource = buildInsertIntoWithPool(pool)<CreateResource, Resource>(Resources, {
const insertResource = buildInsertIntoWithPool(pool)(Resources, {
returning: true,
});
const updateResource = buildUpdateWhereWithPool(pool)<CreateResource, Resource>(Resources, true);
const updateResource = buildUpdateWhereWithPool(pool)(Resources, true);
const updateResourceById = async (
id: string,

View file

@ -125,7 +125,7 @@ describe('roles query', () => {
const keys = excludeAutoSetFields(Roles.fieldKeys);
const expectSql = `
insert into "roles" ("tenant_id", "id", "name", "description")
insert into "roles" ("id", "name", "description")
values (${keys.map((_, index) => `$${index + 1}`).join(', ')})
returning *
`;

View file

@ -108,13 +108,13 @@ export const createRolesQueries = (pool: CommonQueryMethods) => {
)}
`);
const insertRole = buildInsertIntoWithPool(pool)<CreateRole, Role>(Roles, {
const insertRole = buildInsertIntoWithPool(pool)(Roles, {
returning: true,
});
const findRoleById = buildFindEntityByIdWithPool(pool)<CreateRole, Role>(Roles);
const findRoleById = buildFindEntityByIdWithPool(pool)(Roles);
const updateRole = buildUpdateWhereWithPool(pool)<CreateRole, Role>(Roles, true);
const updateRole = buildUpdateWhereWithPool(pool)(Roles, true);
const updateRoleById = async (id: string, set: Partial<OmitAutoSetFields<CreateRole>>) =>
updateRole({ set, where: { id }, jsonbMode: 'merge' });

View file

@ -124,13 +124,13 @@ export const createScopeQueries = (pool: CommonQueryMethods) => {
`)
: [];
const insertScope = buildInsertIntoWithPool(pool)<CreateScope, Scope>(Scopes, {
const insertScope = buildInsertIntoWithPool(pool)(Scopes, {
returning: true,
});
const findScopeById = buildFindEntityByIdWithPool(pool)<CreateScope, Scope>(Scopes);
const findScopeById = buildFindEntityByIdWithPool(pool)(Scopes);
const updateScope = buildUpdateWhereWithPool(pool)<CreateScope, Scope>(Scopes, true);
const updateScope = buildUpdateWhereWithPool(pool)(Scopes, true);
const updateScopeById = async (id: string, set: Partial<OmitAutoSetFields<CreateScope>>) =>
updateScope({ set, where: { id }, jsonbMode: 'merge' });

View file

@ -1,4 +1,4 @@
import type { SignInExperience, CreateSignInExperience } from '@logto/schemas';
import type { CreateSignInExperience } from '@logto/schemas';
import { SignInExperiences } from '@logto/schemas';
import type { CommonQueryMethods } from 'slonik';
@ -8,18 +8,13 @@ import { buildUpdateWhereWithPool } from '#src/database/update-where.js';
const id = 'default';
export const createSignInExperienceQueries = (pool: CommonQueryMethods) => {
const updateSignInExperience = buildUpdateWhereWithPool(pool)<
CreateSignInExperience,
SignInExperience
>(SignInExperiences, true);
const updateSignInExperience = buildUpdateWhereWithPool(pool)(SignInExperiences, true);
const updateDefaultSignInExperience = async (set: Partial<CreateSignInExperience>) =>
updateSignInExperience({ set, where: { id }, jsonbMode: 'replace' });
const findDefaultSignInExperience = async () =>
buildFindEntityByIdWithPool(pool)<CreateSignInExperience, SignInExperience>(SignInExperiences)(
id
);
buildFindEntityByIdWithPool(pool)(SignInExperiences)(id);
return { updateDefaultSignInExperience, findDefaultSignInExperience };
};

View file

@ -172,7 +172,7 @@ export const createUserQueries = (pool: CommonQueryMethods) => {
`)
: [];
const updateUser = buildUpdateWhereWithPool(pool)<CreateUser, User>(Users, true);
const updateUser = buildUpdateWhereWithPool(pool)(Users, true);
const updateUserById = async (
id: string,

View file

@ -1,4 +1,4 @@
import type { CreateVerificationStatus, VerificationStatus } from '@logto/schemas';
import type { VerificationStatus } from '@logto/schemas';
import { VerificationStatuses } from '@logto/schemas';
import { convertToIdentifiers } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik';
@ -16,10 +16,7 @@ export const createVerificationStatusQueries = (pool: CommonQueryMethods) => {
where ${fields.userId}=${userId}
`);
const insertVerificationStatus = buildInsertIntoWithPool(pool)<
CreateVerificationStatus,
VerificationStatus
>(VerificationStatuses, {
const insertVerificationStatus = buildInsertIntoWithPool(pool)(VerificationStatuses, {
returning: true,
});

View file

@ -1,6 +1,8 @@
import type { GeneratedSchema, SchemaLike } from '@logto/schemas';
export const isKeyOf =
<Schema extends SchemaLike>({ fieldKeys }: GeneratedSchema<Schema>) =>
<CreateSchema extends SchemaLike, Schema extends CreateSchema>({
fieldKeys,
}: GeneratedSchema<CreateSchema, Schema>) =>
(key: string): key is keyof Schema extends string ? keyof Schema : never =>
fieldKeys.includes(key);

View file

@ -18,7 +18,10 @@ export type SchemaLike<Key extends string = string> = {
[key in Key]: SchemaValue;
};
export type GeneratedSchema<Schema extends SchemaLike> = keyof Schema extends string
export type GeneratedSchema<
CreateSchema extends SchemaLike,
Schema extends CreateSchema
> = keyof Schema extends string
? Readonly<{
table: string;
tableSingular: string;
@ -26,7 +29,7 @@ export type GeneratedSchema<Schema extends SchemaLike> = keyof Schema extends st
[key in keyof Required<Schema>]: string;
};
fieldKeys: ReadonlyArray<keyof Schema>;
createGuard: CreateGuard<Schema>;
createGuard: CreateGuard<CreateSchema>;
guard: Guard<Schema>;
}>
: never;

View file

@ -75,7 +75,7 @@ export const generateSchema = ({ name, fields }: TableWithType) => {
'',
`export const ${camelcase(name, {
pascalCase: true,
})}: GeneratedSchema<${databaseEntryType}> = Object.freeze({`,
})}: GeneratedSchema<${databaseEntryType}, ${modelName}> = Object.freeze({`,
` table: '${name}',`,
` tableSingular: '${pluralize(name, 1)}',`,
' fields: {',

View file

@ -13,7 +13,7 @@ export const conditionalArraySql = <T>(
buildSql: (value: Exclude<T[], Falsy>) => SqlSqlToken
) => (value.length > 0 ? buildSql(value) : sql``);
export const autoSetFields = Object.freeze(['createdAt', 'updatedAt'] as const);
export const autoSetFields = Object.freeze(['tenantId', 'createdAt', 'updatedAt'] as const);
export type OmitAutoSetFields<T> = Omit<T, (typeof autoSetFields)[number]>;
export type ExcludeAutoSetFields<T> = Exclude<T, (typeof autoSetFields)[number]>;
export const excludeAutoSetFields = <T extends string>(fields: readonly T[]) =>