0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

refactor(core): move database pool into env set

This commit is contained in:
Gao Sun 2022-04-19 21:49:20 +08:00
parent b527cb6a83
commit dff23b57db
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
35 changed files with 293 additions and 168 deletions

View file

@ -2,8 +2,8 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['./jest.setup.js', 'jest-matcher-specific-error'],
globalSetup: './jest.global-setup.js',
setupFilesAfterEnv: ['./jest.setup.ts', 'jest-matcher-specific-error'],
globalSetup: './jest.global-setup.ts',
globals: {
'ts-jest': {
tsconfig: 'tsconfig.test.json',

View file

@ -1,13 +1,12 @@
/* eslint-disable unicorn/prefer-module */
/**
* Generate private key for tests
*/
const { generateKeyPairSync } = require('crypto');
const { writeFileSync } = require('fs');
import { generateKeyPairSync } from 'crypto';
import { writeFileSync } from 'fs';
const privateKeyPath = 'oidc-private-key.test.pem';
export const privateKeyPath = 'oidc-private-key.test.pem';
module.exports = () => {
const globalSetup = () => {
const { privateKey } = generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
@ -23,7 +22,4 @@ module.exports = () => {
writeFileSync(privateKeyPath, privateKey);
};
exports = module.exports;
exports.privateKeyPath = privateKeyPath;
/* eslint-enable unicorn/prefer-module */
export default globalSetup;

View file

@ -1,12 +0,0 @@
/* eslint-disable unicorn/prefer-module */
/**
* Setup environment variables for unit test
*/
const { privateKeyPath } = require('./jest.global-setup.js');
process.env = {
...process.env,
OIDC_PRIVATE_KEY_PATH: privateKeyPath,
};
/* eslint-enable unicorn/prefer-module */

View file

@ -0,0 +1,15 @@
/**
* Setup environment variables for unit test
*/
import envSet from '@/env-set';
import { privateKeyPath } from './jest.global-setup';
(async () => {
process.env = {
...process.env,
OIDC_PRIVATE_KEY_PATH: privateKeyPath,
};
await envSet.load();
})();

View file

@ -1,6 +1,7 @@
import { CreateUser, Users } from '@logto/schemas';
import decamelize from 'decamelize';
import envSet from '@/env-set';
import { InsertionError } from '@/errors/SlonikError';
import { createTestPool } from '@/utils/test-utils';
@ -18,7 +19,9 @@ describe('buildInsertInto()', () => {
const user: CreateUser = { id: 'foo', username: '456' };
const expectInsertIntoSql = buildExpectedInsertIntoSql(Object.keys(user));
const pool = createTestPool(expectInsertIntoSql.join('\n'));
const insertInto = buildInsertInto(pool, Users);
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const insertInto = buildInsertInto(Users);
await expect(insertInto(user)).resolves.toBe(undefined);
});
@ -32,8 +35,10 @@ describe('buildInsertInto()', () => {
'set "primary_email"=excluded."primary_email"',
].join('\n')
);
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const { fields } = convertToIdentifiers(Users);
const insertInto = buildInsertInto(pool, Users, {
const insertInto = buildInsertInto(Users, {
onConflict: {
fields: [fields.id, fields.username],
setExcludedFields: [fields.primaryEmail],
@ -53,7 +58,9 @@ describe('buildInsertInto()', () => {
primaryEmail: String(primaryEmail),
})
);
const insertInto = buildInsertInto(pool, Users, { returning: true });
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const insertInto = buildInsertInto(Users, { returning: true });
await expect(
insertInto({ id: 'foo', username: '123', primaryEmail: 'foo@bar.com' })
).resolves.toStrictEqual(user);
@ -63,7 +70,9 @@ describe('buildInsertInto()', () => {
const user: CreateUser = { id: 'foo', username: '123', primaryEmail: 'foo@bar.com' };
const expectInsertIntoSql = buildExpectedInsertIntoSql(Object.keys(user));
const pool = createTestPool([...expectInsertIntoSql, 'returning *'].join('\n'));
const insertInto = buildInsertInto(pool, Users, { returning: true });
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const insertInto = buildInsertInto(Users, { returning: true });
const dataToInsert = { id: 'foo', username: '123', primaryEmail: 'foo@bar.com' };
await expect(insertInto(dataToInsert)).rejects.toMatchError(

View file

@ -1,7 +1,8 @@
import { SchemaLike, GeneratedSchema } from '@logto/schemas';
import { has } from '@silverhand/essentials';
import { DatabasePoolType, IdentifierSqlTokenType, sql } from 'slonik';
import { IdentifierSqlTokenType, sql } from 'slonik';
import envSet from '@/env-set';
import { InsertionError } from '@/errors/SlonikError';
import assertThat from '@/utils/assert-that';
@ -36,12 +37,10 @@ type InsertIntoConfig = {
interface BuildInsertInto {
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
pool: DatabasePoolType,
{ fieldKeys, ...rest }: GeneratedSchema<Schema>,
config: InsertIntoConfigReturning
): (data: OmitAutoSetFields<Schema>) => Promise<ReturnType>;
<Schema extends SchemaLike>(
pool: DatabasePoolType,
{ fieldKeys, ...rest }: GeneratedSchema<Schema>,
config?: InsertIntoConfig
): (data: OmitAutoSetFields<Schema>) => Promise<void>;
@ -51,7 +50,6 @@ export const buildInsertInto: BuildInsertInto = <
Schema extends SchemaLike,
ReturnType extends SchemaLike
>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
config?: InsertIntoConfig | InsertIntoConfigReturning
) => {
@ -65,7 +63,7 @@ export const buildInsertInto: BuildInsertInto = <
const insertingKeys = keys.filter((key) => has(data, key));
const {
rows: [entry],
} = await pool.query<ReturnType>(sql`
} = await envSet.pool.query<ReturnType>(sql`
insert into ${table} (${sql.join(
insertingKeys.map((key) => fields[key]),
sql`, `

View file

@ -1,10 +0,0 @@
import { createPool } from 'slonik';
import { createInterceptors } from 'slonik-interceptor-preset';
import envSet from '@/env-set';
const interceptors = [...createInterceptors()];
const pool = createPool(envSet.values.dbUrl, { interceptors });
export default pool;

View file

@ -1,5 +1,6 @@
import { CreateUser, Users, Applications } from '@logto/schemas';
import envSet from '@/env-set';
import { UpdateError } from '@/errors/SlonikError';
import { createTestPool } from '@/utils/test-utils';
@ -10,7 +11,9 @@ describe('buildUpdateWhere()', () => {
const pool = createTestPool(
'update "users"\nset "username"=$1\nwhere "id"=$2 and "username"=$3'
);
const updateWhere = buildUpdateWhere(pool, Users);
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const updateWhere = buildUpdateWhere(Users);
await expect(
updateWhere({
set: { username: '123' },
@ -29,7 +32,9 @@ describe('buildUpdateWhere()', () => {
primaryEmail: String(primaryEmail),
})
);
const updateWhere = buildUpdateWhere(pool, Users, true);
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const updateWhere = buildUpdateWhere(Users, true);
await expect(
updateWhere({ set: { username: '123', primaryEmail: 'foo@bar.com' }, where: { id: 'foo' } })
).resolves.toStrictEqual(user);
@ -43,8 +48,9 @@ describe('buildUpdateWhere()', () => {
customClientMetadata: String(customClientMetadata),
})
);
const updateWhere = buildUpdateWhere(pool, Applications, true);
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const updateWhere = buildUpdateWhere(Applications, true);
await expect(
updateWhere({
set: { customClientMetadata: { idTokenTtl: 3600 } },
@ -57,7 +63,9 @@ describe('buildUpdateWhere()', () => {
const pool = createTestPool(
'update "users"\nset "username"=$1\nwhere "id"=$2 and "username"=$3'
);
const updateWhere = buildUpdateWhere(pool, Users);
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const updateWhere = buildUpdateWhere(Users);
await expect(
updateWhere({
@ -69,7 +77,9 @@ describe('buildUpdateWhere()', () => {
it('throws `entity.not_exists_with_id` error with `undefined` when `returning` is true', async () => {
const pool = createTestPool('update "users"\nset "username"=$1\nwhere "id"=$2\nreturning *');
const updateWhere = buildUpdateWhere(pool, Users, true);
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const updateWhere = buildUpdateWhere(Users, true);
const updateWhereData = { set: { username: '123' }, where: { id: 'foo' } };
await expect(updateWhere(updateWhereData)).rejects.toMatchError(
@ -81,7 +91,9 @@ describe('buildUpdateWhere()', () => {
const pool = createTestPool(
'update "users"\nset "username"=$1\nwhere "username"=$2\nreturning *'
);
const updateWhere = buildUpdateWhere(pool, Users, true);
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(pool);
const updateWhere = buildUpdateWhere(Users, true);
const updateData = { set: { username: '123' }, where: { username: 'foo' } };
await expect(updateWhere(updateData)).rejects.toMatchError(new UpdateError(Users, updateData));

View file

@ -1,7 +1,8 @@
import { SchemaLike, GeneratedSchema } from '@logto/schemas';
import { notFalsy, Truthy } from '@silverhand/essentials';
import { DatabasePoolType, sql } from 'slonik';
import { sql } from 'slonik';
import envSet from '@/env-set';
import { UpdateError } from '@/errors/SlonikError';
import assertThat from '@/utils/assert-that';
import { isKeyOf } from '@/utils/schema';
@ -11,22 +12,18 @@ import { conditionalSql, convertToIdentifiers, convertToPrimitiveOrSql } from '.
interface BuildUpdateWhere {
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
returning: true
): (data: UpdateWhereData<Schema>) => Promise<ReturnType>;
<Schema extends SchemaLike>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
returning?: false
): (data: UpdateWhereData<Schema>) => Promise<void>;
<Schema extends SchemaLike>(schema: GeneratedSchema<Schema>, returning?: false): (
data: UpdateWhereData<Schema>
) => Promise<void>;
}
export const buildUpdateWhere: BuildUpdateWhere = <
Schema extends SchemaLike,
ReturnType extends SchemaLike
>(
pool: DatabasePoolType,
schema: GeneratedSchema<Schema>,
returning = false
) => {
@ -58,7 +55,7 @@ export const buildUpdateWhere: BuildUpdateWhere = <
return async ({ set, where }: UpdateWhereData<Schema>) => {
const {
rows: [data],
} = await pool.query<ReturnType>(sql`
} = await envSet.pool.query<ReturnType>(sql`
update ${table}
set ${sql.join(connectKeyValueWithEqualSign(set), sql`, `)}
where ${sql.join(connectKeyValueWithEqualSign(where), sql` and `)}

View file

@ -3,7 +3,8 @@ import { Falsy, notFalsy } from '@silverhand/essentials';
import dayjs from 'dayjs';
import { sql, SqlSqlTokenType, SqlTokenType, IdentifierSqlTokenType } from 'slonik';
import pool from './pool';
import envSet from '@/env-set';
import { FieldIdentifiers, Table } from './types';
export const conditionalSql = <T>(
@ -73,7 +74,7 @@ export const convertToIdentifiers = <T extends Table>(
export const convertToTimestamp = (time = dayjs()) => sql`to_timestamp(${time.valueOf() / 1000})`;
export const getTotalRowCount = async (table: IdentifierSqlTokenType) =>
pool.one<{ count: number }>(sql`
envSet.pool.one<{ count: number }>(sql`
select count(*)
from ${table}
`);

View file

@ -3,6 +3,8 @@ import { readFileSync } from 'fs';
import { assertEnv, getEnv, Optional } from '@silverhand/essentials';
import { nanoid } from 'nanoid';
import { createPool, DatabasePoolType } from 'slonik';
import { createInterceptors } from 'slonik-interceptor-preset';
import { string, number } from 'zod';
export enum MountedApps {
@ -33,15 +35,16 @@ const loadOidcValues = (port: number) => {
};
};
const loadEnvValues = () => {
const loadEnvValues = async () => {
const isProduction = getEnv('NODE_ENV') === 'production';
const isTest = getEnv('NODE_ENV') === 'test';
const port = Number(getEnv('PORT', '3001'));
const databaseUrl = isTest ? getEnv('DB_URL') : assertEnv('DB_URL');
return Object.freeze({
isTest,
isProduction,
dbUrl: isTest ? getEnv('DB_URL') : assertEnv('DB_URL'),
databaseUrl,
httpsCert: process.env.HTTPS_CERT,
httpsKey: process.env.HTTPS_KEY,
port,
@ -57,18 +60,44 @@ const loadEnvValues = () => {
});
};
const throwNotLoadedError = () => {
throw new Error(
'Env set is not loaded. Make sure to call `await envSet.load()` before using it.'
);
};
/* eslint-disable @silverhand/fp/no-let, @silverhand/fp/no-mutation */
function createEnvSet() {
// eslint-disable-next-line @silverhand/fp/no-let
let values = loadEnvValues();
let values: Optional<Awaited<ReturnType<typeof loadEnvValues>>>;
let pool: Optional<DatabasePoolType>;
return {
values,
reload: () => {
// eslint-disable-next-line @silverhand/fp/no-mutation
values = loadEnvValues();
get values() {
if (!values) {
return throwNotLoadedError();
}
return values;
},
get pool() {
if (!pool) {
return throwNotLoadedError();
}
return pool;
},
load: async () => {
values = await loadEnvValues();
if (!values.isTest) {
const interceptors = [...createInterceptors()];
pool = createPool(values.databaseUrl, { interceptors });
}
},
};
}
/* eslint-enable @silverhand/fp/no-let, @silverhand/fp/no-mutation */
const envSet = createEnvSet();

View file

@ -14,6 +14,7 @@ import initI18n from './i18n/init';
(async () => {
try {
await envSet.load();
const app = new Koa({
proxy: envSet.values.trustingTlsOffloadingProxies,
});

View file

@ -2,6 +2,7 @@ import { jwtVerify } from 'jose/jwt/verify';
import { Context } from 'koa';
import { IRouterParamContext } from 'koa-router';
import envSet from '@/env-set';
import RequestError from '@/errors/RequestError';
import { createContextWithRouteParameters } from '@/utils/test-utils';
@ -30,19 +31,14 @@ describe('koaAuth middleware', () => {
});
it('should read DEVELOPMENT_USER_ID from env variable first if not production', async () => {
// Mock the @/env/consts
process.env.DEVELOPMENT_USER_ID = 'foo';
const spy = jest
.spyOn(envSet, 'values', 'get')
.mockReturnValue({ ...envSet.values, developmentUserId: 'foo' });
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable unicorn/prefer-module */
const koaAuthModule = require('./koa-auth') as { default: typeof koaAuth };
/* eslint-enable @typescript-eslint/no-require-imports */
/* eslint-enable @typescript-eslint/no-var-requires */
/* eslint-enable unicorn/prefer-module */
await koaAuthModule.default()(ctx, next);
await koaAuth()(ctx, next);
expect(ctx.auth).toEqual('foo');
spy.mockRestore();
});
it('should set user auth with given sub returned from accessToken', async () => {

View file

@ -1,4 +1,4 @@
import { MountedApps } from '@/env-set';
import envSet, { MountedApps } from '@/env-set';
import { createContextWithRouteParameters } from '@/utils/test-utils';
import koaSpaProxy from './koa-spa-proxy';
@ -48,32 +48,38 @@ describe('koaSpaProxy middleware', () => {
});
it('production env should overwrite the request path to root if no target ui file are detected', async () => {
process.env.NODE_ENV = 'production';
process.env.PASSWORD_PEPPERS = JSON.stringify(['foo']);
process.env.DB_URL = 'some_db_url';
const spy = jest.spyOn(envSet, 'values', 'get').mockReturnValue({
...envSet.values,
isProduction: true,
passwordPeppers: ['foo'],
databaseUrl: 'some_db_url',
});
const ctx = createContextWithRouteParameters({
url: '/foo',
});
const { default: proxy } = await import('./koa-spa-proxy');
await proxy()(ctx, next);
await koaSpaProxy()(ctx, next);
expect(mockStaticMiddleware).toBeCalled();
expect(ctx.request.path).toEqual('/');
spy.mockRestore();
});
it('production env should call the static middleware if path hit the ui file directory', async () => {
process.env.NODE_ENV = 'production';
process.env.PASSWORD_PEPPERS = JSON.stringify(['foo']);
process.env.DB_URL = 'some_db_url';
const spy = jest.spyOn(envSet, 'values', 'get').mockReturnValue({
...envSet.values,
isProduction: true,
passwordPeppers: ['foo'],
databaseUrl: 'some_db_url',
});
const { default: proxy } = await import('./koa-spa-proxy');
const ctx = createContextWithRouteParameters({
url: '/sign-in',
});
await proxy()(ctx, next);
await koaSpaProxy()(ctx, next);
expect(mockStaticMiddleware).toBeCalled();
spy.mockRestore();
});
});

View file

@ -8,6 +8,7 @@ import {
convertToPrimitiveOrSql,
excludeAutoSetFields,
} from '@/database/utils';
import envSet from '@/env-set';
import { DeletionError } from '@/errors/SlonikError';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
@ -22,7 +23,7 @@ import {
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);

View file

@ -2,7 +2,6 @@ import { Application, CreateApplication, Applications } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
import pool from '@/database/pool';
import { buildUpdateWhere } from '@/database/update-where';
import {
convertToIdentifiers,
@ -10,6 +9,7 @@ import {
getTotalRowCount,
conditionalSql,
} from '@/database/utils';
import envSet from '@/env-set';
import { DeletionError } from '@/errors/SlonikError';
const { table, fields } = convertToIdentifiers(Applications);
@ -17,7 +17,7 @@ const { table, fields } = convertToIdentifiers(Applications);
export const findTotalNumberOfApplications = async () => getTotalRowCount(table);
export const findAllApplications = async (limit: number, offset: number) =>
pool.many<Application>(sql`
envSet.pool.many<Application>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
order by ${fields.createdAt} desc
@ -26,25 +26,17 @@ export const findAllApplications = async (limit: number, offset: number) =>
`);
export const findApplicationById = async (id: string) =>
pool.one<Application>(sql`
envSet.pool.one<Application>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=${id}
`);
export const insertApplication = buildInsertInto<CreateApplication, Application>(
pool,
Applications,
{
returning: true,
}
);
export const insertApplication = buildInsertInto<CreateApplication, Application>(Applications, {
returning: true,
});
const updateApplication = buildUpdateWhere<CreateApplication, Application>(
pool,
Applications,
true
);
const updateApplication = buildUpdateWhere<CreateApplication, Application>(Applications, true);
export const updateApplicationById = async (
id: string,
@ -52,7 +44,7 @@ export const updateApplicationById = async (
) => updateApplication({ set, where: { id } });
export const deleteApplicationById = async (id: string) => {
const { rowCount } = await pool.query(sql`
const { rowCount } = await envSet.pool.query(sql`
delete from ${table}
where ${fields.id}=${id}
`);

View file

@ -2,6 +2,7 @@ import { Connectors, ConnectorType, CreateConnector } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql, QueryResultRowType } from 'slonik';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import {
@ -13,7 +14,7 @@ import {
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);

View file

@ -2,28 +2,28 @@ import { Connector, CreateConnector, Connectors } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
import pool from '@/database/pool';
import { buildUpdateWhere } from '@/database/update-where';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
const { table, fields } = convertToIdentifiers(Connectors);
export const findAllConnectors = async () =>
pool.many<Connector>(sql`
envSet.pool.many<Connector>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
order by ${fields.enabled} desc, ${fields.id} asc
`);
export const findConnectorById = async (id: string) =>
pool.one<Connector>(sql`
envSet.pool.one<Connector>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=${id}
`);
export const insertConnector = buildInsertInto<CreateConnector, Connector>(pool, Connectors, {
export const insertConnector = buildInsertInto<CreateConnector, Connector>(Connectors, {
returning: true,
});
export const updateConnector = buildUpdateWhere<CreateConnector, Connector>(pool, Connectors, true);
export const updateConnector = buildUpdateWhere<CreateConnector, Connector>(Connectors, true);

View file

@ -2,6 +2,7 @@ import { OidcModelInstances, CreateOidcModelInstance } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import {
@ -15,7 +16,7 @@ import {
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);

View file

@ -8,8 +8,8 @@ import { conditional } from '@silverhand/essentials';
import { sql, ValueExpressionType } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
import pool from '@/database/pool';
import { convertToIdentifiers, convertToTimestamp } from '@/database/utils';
import envSet from '@/env-set';
export type WithConsumed<T> = T & { consumed?: boolean };
export type QueryResult = Pick<OidcModelInstance, 'payload' | 'consumedAt'>;
@ -26,7 +26,7 @@ const withConsumed = <T>(data: T, consumedAt?: number | null): WithConsumed<T> =
const convertResult = (result: QueryResult | null) =>
conditional(result && withConsumed(result.payload, result.consumedAt));
export const upsertInstance = buildInsertInto<CreateOidcModelInstance>(pool, OidcModelInstances, {
export const upsertInstance = buildInsertInto<CreateOidcModelInstance>(OidcModelInstances, {
onConflict: {
fields: [fields.modelName, fields.id],
setExcludedFields: [fields.payload, fields.expiresAt],
@ -40,7 +40,7 @@ const findByModel = (modelName: string) => sql`
`;
export const findPayloadById = async (modelName: string, id: string) => {
const result = await pool.maybeOne<QueryResult>(sql`
const result = await envSet.pool.maybeOne<QueryResult>(sql`
${findByModel(modelName)}
and ${fields.id}=${id}
`);
@ -56,7 +56,7 @@ export const findPayloadByPayloadField = async <
field: Field,
value: T
) => {
const result = await pool.maybeOne<QueryResult>(sql`
const result = await envSet.pool.maybeOne<QueryResult>(sql`
${findByModel(modelName)}
and ${fields.payload}->>${field}=${value}
`);
@ -65,7 +65,7 @@ export const findPayloadByPayloadField = async <
};
export const consumeInstanceById = async (modelName: string, id: string) => {
await pool.query(sql`
await envSet.pool.query(sql`
update ${table}
set ${fields.consumedAt}=${convertToTimestamp()}
where ${fields.modelName}=${modelName}
@ -74,7 +74,7 @@ export const consumeInstanceById = async (modelName: string, id: string) => {
};
export const destroyInstanceById = async (modelName: string, id: string) => {
await pool.query(sql`
await envSet.pool.query(sql`
delete from ${table}
where ${fields.modelName}=${modelName}
and ${fields.id}=${id}
@ -82,7 +82,7 @@ export const destroyInstanceById = async (modelName: string, id: string) => {
};
export const revokeInstanceByGrantId = async (modelName: string, grantId: string) => {
await pool.query(sql`
await envSet.pool.query(sql`
delete from ${table}
where ${fields.modelName}=${modelName}
and ${fields.payload}->>'grantId'=${grantId}

View file

@ -8,6 +8,7 @@ import {
convertToPrimitiveOrSql,
excludeAutoSetFields,
} from '@/database/utils';
import envSet from '@/env-set';
import { DeletionError } from '@/errors/SlonikError';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
@ -22,7 +23,7 @@ import {
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);

View file

@ -2,35 +2,35 @@ import { PasscodeType, Passcode, Passcodes, CreatePasscode } from '@logto/schema
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
import pool from '@/database/pool';
import { buildUpdateWhere } from '@/database/update-where';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
import { DeletionError } from '@/errors/SlonikError';
const { table, fields } = convertToIdentifiers(Passcodes);
export const findUnconsumedPasscodeByJtiAndType = async (jti: string, type: PasscodeType) =>
pool.maybeOne<Passcode>(sql`
envSet.pool.maybeOne<Passcode>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.interactionJti}=${jti} and ${fields.type}=${type} and ${fields.consumed} = false
`);
export const findUnconsumedPasscodesByJtiAndType = async (jti: string, type: PasscodeType) =>
pool.any<Passcode>(sql`
envSet.pool.any<Passcode>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.interactionJti}=${jti} and ${fields.type}=${type} and ${fields.consumed} = false
`);
export const insertPasscode = buildInsertInto<CreatePasscode, Passcode>(pool, Passcodes, {
export const insertPasscode = buildInsertInto<CreatePasscode, Passcode>(Passcodes, {
returning: true,
});
export const updatePasscode = buildUpdateWhere<CreatePasscode, Passcode>(pool, Passcodes, true);
export const updatePasscode = buildUpdateWhere<CreatePasscode, Passcode>(Passcodes, true);
export const deletePasscodeById = async (id: string) => {
const { rowCount } = await pool.query(sql`
const { rowCount } = await envSet.pool.query(sql`
delete from ${table}
where ${fields.id}=${id}
`);
@ -41,7 +41,7 @@ export const deletePasscodeById = async (id: string) => {
};
export const deletePasscodesByIds = async (ids: string[]) => {
const { rowCount } = await pool.query(sql`
const { rowCount } = await envSet.pool.query(sql`
delete from ${table}
where ${fields.id} in (${ids.join(',')})
`);

View file

@ -3,6 +3,7 @@ import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { mockResource } from '@/__mocks__';
import { convertToIdentifiers, convertToPrimitiveOrSql } from '@/database/utils';
import envSet from '@/env-set';
import { DeletionError } from '@/errors/SlonikError';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
@ -18,7 +19,7 @@ import {
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);

View file

@ -2,7 +2,6 @@ import { Resource, CreateResource, Resources } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
import pool from '@/database/pool';
import { buildUpdateWhere } from '@/database/update-where';
import {
convertToIdentifiers,
@ -10,6 +9,7 @@ import {
getTotalRowCount,
conditionalSql,
} from '@/database/utils';
import envSet from '@/env-set';
import { DeletionError } from '@/errors/SlonikError';
const { table, fields } = convertToIdentifiers(Resources);
@ -17,7 +17,7 @@ const { table, fields } = convertToIdentifiers(Resources);
export const findTotalNumberOfResources = async () => getTotalRowCount(table);
export const findAllResources = async (limit: number, offset: number) =>
pool.many<Resource>(sql`
envSet.pool.many<Resource>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
${conditionalSql(limit, (limit) => sql`limit ${limit}`)}
@ -25,24 +25,24 @@ export const findAllResources = async (limit: number, offset: number) =>
`);
export const findResourceByIndicator = async (indicator: string) =>
pool.maybeOne<Resource>(sql`
envSet.pool.maybeOne<Resource>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.indicator}=${indicator}
`);
export const findResourceById = async (id: string) =>
pool.one<Resource>(sql`
envSet.pool.one<Resource>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=${id}
`);
export const insertResource = buildInsertInto<CreateResource, Resource>(pool, Resources, {
export const insertResource = buildInsertInto<CreateResource, Resource>(Resources, {
returning: true,
});
const updateResource = buildUpdateWhere<CreateResource, Resource>(pool, Resources, true);
const updateResource = buildUpdateWhere<CreateResource, Resource>(Resources, true);
export const updateResourceById = async (
id: string,
@ -50,7 +50,7 @@ export const updateResourceById = async (
) => updateResource({ set, where: { id } });
export const deleteResourceById = async (id: string) => {
const { rowCount } = await pool.query(sql`
const { rowCount } = await envSet.pool.query(sql`
delete from ${table}
where ${fields.id}=${id}
`);

View file

@ -3,13 +3,14 @@ import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { mockRole } from '@/__mocks__';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import { findAllRoles, findRolesByRoleNames } from './roles';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);

View file

@ -1,19 +1,19 @@
import { Roles, Role } from '@logto/schemas';
import { sql } from 'slonik';
import pool from '@/database/pool';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
const { table, fields } = convertToIdentifiers(Roles);
export const findAllRoles = async () =>
pool.any<Role>(sql`
envSet.pool.any<Role>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
`);
export const findRolesByRoleNames = async (roleNames: string[]) =>
pool.any<Role>(sql`
envSet.pool.any<Role>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.name} in (${sql.join(roleNames, sql`, `)})

View file

@ -3,13 +3,14 @@ import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { mockSetting } from '@/__mocks__';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import { defaultSettingId, getSetting, updateSetting } from './setting';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);

View file

@ -1,16 +1,16 @@
import { Setting, CreateSetting, Settings } from '@logto/schemas';
import { sql } from 'slonik';
import pool from '@/database/pool';
import { buildUpdateWhere } from '@/database/update-where';
import { convertToIdentifiers, OmitAutoSetFields } from '@/database/utils';
import envSet from '@/env-set';
export const defaultSettingId = 'default';
const { table, fields } = convertToIdentifiers(Settings);
export const getSetting = async () =>
pool.one<Setting>(sql`
envSet.pool.one<Setting>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=${defaultSettingId}
@ -18,7 +18,6 @@ export const getSetting = async () =>
export const updateSetting = async (setting: Partial<OmitAutoSetFields<CreateSetting>>) => {
return buildUpdateWhere<CreateSetting, Setting>(
pool,
Settings,
true
)({ set: setting, where: { id: defaultSettingId } });

View file

@ -1,13 +1,14 @@
import { createMockPool, createMockQueryResult } from 'slonik';
import { mockSignInExperience } from '@/__mocks__';
import envSet from '@/env-set';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import { findDefaultSignInExperience, updateDefaultSignInExperience } from './sign-in-experience';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);

View file

@ -1,14 +1,13 @@
import { SignInExperience, CreateSignInExperience, SignInExperiences } from '@logto/schemas';
import { sql } from 'slonik';
import pool from '@/database/pool';
import { buildUpdateWhere } from '@/database/update-where';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
const { table, fields } = convertToIdentifiers(SignInExperiences);
const updateSignInExperience = buildUpdateWhere<CreateSignInExperience, SignInExperience>(
pool,
SignInExperiences,
true
);
@ -19,7 +18,7 @@ export const updateDefaultSignInExperience = async (set: Partial<CreateSignInExp
updateSignInExperience({ set, where: { id } });
export const findDefaultSignInExperience = async () =>
pool.one<SignInExperience>(sql`
envSet.pool.one<SignInExperience>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id} = ${id}

View file

@ -0,0 +1,67 @@
import { UserLogs } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { snakeCase } from 'snake-case';
import { mockUserLog } from '@/__mocks__';
import {
convertToIdentifiers,
excludeAutoSetFields,
convertToPrimitiveOrSql,
} from '@/database/utils';
import envSet from '@/env-set';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import { insertUserLog, findLogsByUserId } from './user-log';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('user-log query', () => {
const { table, fields } = convertToIdentifiers(UserLogs);
const dbvalue = { ...mockUserLog, payload: JSON.stringify(mockUserLog.payload) };
it('findLogsByUserId', async () => {
const userId = 'foo';
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.userId}=${userId}
order by created_at desc
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([userId]);
return createMockQueryResult([dbvalue]);
});
await expect(findLogsByUserId(userId)).resolves.toEqual([dbvalue]);
});
it('insertUserLog', async () => {
const keys = excludeAutoSetFields(UserLogs.fieldKeys);
// eslint-disable-next-line sql/no-unsafe-query
const expectSql = `
insert into "user_logs" (${keys.map((k) => `"${snakeCase(k)}"`).join(', ')})
values (${keys.map((_, index) => `$${index + 1}`).join(', ')})
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql);
expect(values).toEqual(keys.map((k) => convertToPrimitiveOrSql(k, mockUserLog[k])));
return createMockQueryResult([]);
});
await insertUserLog(mockUserLog);
});
});

View file

@ -0,0 +1,18 @@
import { CreateUserLog, UserLogs } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
const { table, fields } = convertToIdentifiers(UserLogs);
export const insertUserLog = buildInsertInto<CreateUserLog>(UserLogs);
export const findLogsByUserId = async (userId: string) =>
envSet.pool.many<CreateUserLog>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.userId}=${userId}
order by created_at desc
`);

View file

@ -3,6 +3,7 @@ import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { mockUser } from '@/__mocks__';
import { convertToIdentifiers, convertToPrimitiveOrSql } from '@/database/utils';
import envSet from '@/env-set';
import { DeletionError } from '@/errors/SlonikError';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
@ -28,7 +29,7 @@ import {
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
jest.spyOn(envSet, 'pool', 'get').mockReturnValue(
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);

View file

@ -2,43 +2,43 @@ import { User, CreateUser, Users } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
import pool from '@/database/pool';
import { buildUpdateWhere } from '@/database/update-where';
import { conditionalSql, convertToIdentifiers, OmitAutoSetFields } from '@/database/utils';
import envSet from '@/env-set';
import { DeletionError, UpdateError } from '@/errors/SlonikError';
const { table, fields } = convertToIdentifiers(Users);
export const findUserByUsername = async (username: string) =>
pool.one<User>(sql`
envSet.pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.username}=${username}
`);
export const findUserByEmail = async (email: string) =>
pool.one<User>(sql`
envSet.pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryEmail}=${email}
`);
export const findUserByPhone = async (phone: string) =>
pool.one<User>(sql`
envSet.pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryPhone}=${phone}
`);
export const findUserById = async (id: string) =>
pool.one<User>(sql`
envSet.pool.one<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.id}=${id}
`);
export const findUserByIdentity = async (connectorId: string, userId: string) =>
pool.one<User>(
envSet.pool.one<User>(
sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
@ -47,35 +47,35 @@ export const findUserByIdentity = async (connectorId: string, userId: string) =>
);
export const hasUser = async (username: string) =>
pool.exists(sql`
envSet.pool.exists(sql`
select ${fields.id}
from ${table}
where ${fields.username}=${username}
`);
export const hasUserWithId = async (id: string) =>
pool.exists(sql`
envSet.pool.exists(sql`
select ${fields.id}
from ${table}
where ${fields.id}=${id}
`);
export const hasUserWithEmail = async (email: string) =>
pool.exists(sql`
envSet.pool.exists(sql`
select ${fields.primaryEmail}
from ${table}
where ${fields.primaryEmail}=${email}
`);
export const hasUserWithPhone = async (phone: string) =>
pool.exists(sql`
envSet.pool.exists(sql`
select ${fields.primaryPhone}
from ${table}
where ${fields.primaryPhone}=${phone}
`);
export const hasUserWithIdentity = async (connectorId: string, userId: string) =>
pool.exists(
envSet.pool.exists(
sql`
select ${fields.id}
from ${table}
@ -83,7 +83,9 @@ export const hasUserWithIdentity = async (connectorId: string, userId: string) =
`
);
export const insertUser = buildInsertInto<CreateUser, User>(pool, Users, { returning: true });
export const insertUser = buildInsertInto<CreateUser, User>(Users, {
returning: true,
});
const buildUserSearchConditionSql = (search: string) => {
const searchFields = [fields.primaryEmail, fields.primaryPhone, fields.username, fields.name];
@ -93,14 +95,14 @@ const buildUserSearchConditionSql = (search: string) => {
};
export const countUsers = async (search?: string) =>
pool.one<{ count: number }>(sql`
envSet.pool.one<{ count: number }>(sql`
select count(*)
from ${table}
${conditionalSql(search, (search) => sql`where ${buildUserSearchConditionSql(search)}`)}
`);
export const findUsers = async (limit: number, offset: number, search?: string) =>
pool.any<User>(
envSet.pool.any<User>(
sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
@ -110,13 +112,13 @@ export const findUsers = async (limit: number, offset: number, search?: string)
`
);
const updateUser = buildUpdateWhere<CreateUser, User>(pool, Users, true);
const updateUser = buildUpdateWhere<CreateUser, User>(Users, true);
export const updateUserById = async (id: string, set: Partial<OmitAutoSetFields<CreateUser>>) =>
updateUser({ set, where: { id } });
export const deleteUserById = async (id: string) => {
const { rowCount } = await pool.query(sql`
const { rowCount } = await envSet.pool.query(sql`
delete from ${table}
where ${fields.id}=${id}
`);
@ -127,7 +129,7 @@ export const deleteUserById = async (id: string) => {
};
export const clearUserCustomDataById = async (id: string) => {
const { rowCount } = await pool.query<User>(sql`
const { rowCount } = await envSet.pool.query<User>(sql`
update ${table}
set ${fields.customData}='{}'::jsonb
where ${fields.id}=${id}
@ -139,7 +141,7 @@ export const clearUserCustomDataById = async (id: string) => {
};
export const deleteUserIdentity = async (userId: string, connectorId: string) =>
pool.one<User>(sql`
envSet.pool.one<User>(sql`
update ${table}
set ${fields.identities}=${fields.identities}::jsonb-${connectorId}
where ${fields.id}=${userId}

View file

@ -8,6 +8,7 @@
]
},
"include": [
"src"
"src",
"jest.*.ts"
]
}