Skip to content

Commit d295174

Browse files
committed
refactor(guard-clause): extract
1 parent a31f551 commit d295174

File tree

6 files changed

+47
-107
lines changed

6 files changed

+47
-107
lines changed

examples/guard-clause/src/index.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Against, Guard, GuardErrors } from '@atls/guard-clause'
1+
import { Against } from '@atls/guard-clause'
2+
import { Guard } from '@atls/guard-clause'
3+
import { GuardErrors } from '@atls/guard-clause'
24

35
type SignUpPayload = {
46
email: string
@@ -10,13 +12,14 @@ type SignUpPayload = {
1012
export class UserOnboardingService {
1113
@Guard()
1214
register(
13-
@Against('email').Empty().NotStringLengthBetween(5, 128)
15+
@(Against('email').Empty())
16+
@(Against('email').NotStringLengthBetween(5, 128))
1417
email: string,
15-
@Against('age').NotNumberBetween(16, 120)
18+
@(Against('age').NotNumberBetween(16, 120))
1619
age: number,
17-
@Against('roles').Optional.Each.NotOneOf(['root', 'god-mode'])
20+
@(Against('roles').Optional.Each.NotOneOf(['root', 'god-mode']))
1821
roles: Array<string> = [],
19-
@Against('profileId').Optional.NotUUID('4')
22+
@(Against('profileId').Optional.NotUUID('4'))
2023
profileId?: string
2124
): SignUpPayload {
2225
return { email, age, roles, profileId }
@@ -28,9 +31,11 @@ const onboarding = new UserOnboardingService()
2831
try {
2932
onboarding.register('', 14, ['root'], 'not-a-uuid')
3033
} catch (error) {
31-
if (error instanceof GuardErrors) {
32-
const messages = error.errors.map((issue) => issue.message).join('\n')
33-
34-
process.stderr.write(`Не пускаем пользователя:\n${messages}\n`)
34+
if (!(error instanceof GuardErrors)) {
35+
throw error
3536
}
37+
38+
const messages = error.errors.map((issue) => issue.message).join('\n')
39+
40+
process.stderr.write(`Не пускаем пользователя:\n${messages}\n`)
3641
}

examples/service-entrypoint/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
Этот пример нужен как эталон запуска NestJS-сервиса внутри наших библиотек. Тут показываем, как собирать `@atls/logger` в боевом окружении, как правильно держать hot-reload через `import.meta.hot` и где врубать graceful shutdown. Никаких «магических» пакетов — только базовые зависимости из Nest + наш логгер.
44

5-
* Поднимаем один экземпляр логгера и прокидываем его в модуль, чтобы не плодить инстансы.
6-
* Прячем старт приложения за функцией `bootstrap`, чтобы было куда подвесить ошибки запуска и выключение.
7-
* Демонстрируем, как закрывать Nest-приложение, когда Vite/HMR даёт сигнал на перезагрузку.
5+
- Поднимаем один экземпляр логгера и прокидываем его в модуль, чтобы не плодить инстансы.
6+
- Прячем старт приложения за функцией `bootstrap`, чтобы было куда подвесить ошибки запуска и выключение.
7+
- Демонстрируем, как закрывать Nest-приложение, когда Vite/HMR даёт сигнал на перезагрузку.
88

99
Короче, это рельсовый шаблон старта сервиса: берёшь и копируешь, чтобы не наступать на грабли с ESM-импортами и хаотичными логгерами.

packages/core-errors/src/index.ts

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1 @@
1-
import { DomainError } from '@atls/core-errors'
2-
3-
type PaymentGateway = 'stripe' | 'cloud-payments'
4-
5-
type PaymentIntent = {
6-
id: string
7-
gateway: PaymentGateway
8-
amountMinor: number
9-
currency: string
10-
}
11-
12-
export class PaymentRejectedError extends DomainError {
13-
public readonly intent: PaymentIntent
14-
15-
private constructor(intent: PaymentIntent, reason: string) {
16-
super(`Платёж ${intent.id} улетел в трубу: ${reason}`)
17-
18-
this.intent = intent
19-
}
20-
21-
static becauseGatewayRejected(intent: PaymentIntent, reason: string): PaymentRejectedError {
22-
return new PaymentRejectedError(intent, reason)
23-
}
24-
}
25-
26-
export const ensureCaptured = (intent: PaymentIntent & { captured: boolean }): PaymentIntent => {
27-
if (!intent.captured) {
28-
throw PaymentRejectedError.becauseGatewayRejected(intent, 'гейт закрыл транзакцию')
29-
}
30-
31-
return intent
32-
}
33-
34-
try {
35-
ensureCaptured({
36-
id: 'pay_123',
37-
gateway: 'stripe',
38-
amountMinor: 1990,
39-
currency: 'RUB',
40-
captured: false,
41-
})
42-
} catch (error) {
43-
if (error instanceof PaymentRejectedError) {
44-
const { amountMinor, currency } = error.intent
45-
46-
process.stderr.write(`Надо раскатать рефанд на ${amountMinor} ${currency}\n`)
47-
}
48-
}
1+
export { DomainError } from './domain.error.js'

packages/guard-clause/src/decorators/against/index.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,28 @@ const factory = (name: string, options?: AbstractGuardExtensionFactoryOptions['o
2727
NotBigInt: NotBigIntDecoratorFactory(name, options),
2828
})
2929

30-
export const Against = (
31-
name: string
32-
): ReturnType<typeof factory> & {
33-
Optional: ReturnType<typeof factory>
34-
Each: ReturnType<typeof factory>
35-
} => ({
36-
...factory(name),
37-
Optional: factory(name, { optional: true }),
38-
Each: factory(name, { each: true }),
39-
})
30+
type GuardDecoratorBuilder = ReturnType<typeof factory> & {
31+
Optional: GuardDecoratorBuilder
32+
Each: GuardDecoratorBuilder
33+
}
34+
35+
const createBuilder = (
36+
name: string,
37+
options?: AbstractGuardExtensionFactoryOptions['options']
38+
): GuardDecoratorBuilder =>
39+
Object.defineProperties(factory(name, options), {
40+
Optional: {
41+
enumerable: true,
42+
get(): GuardDecoratorBuilder {
43+
return createBuilder(name, { ...options, optional: true })
44+
},
45+
},
46+
Each: {
47+
enumerable: true,
48+
get(): GuardDecoratorBuilder {
49+
return createBuilder(name, { ...options, each: true })
50+
},
51+
},
52+
}) as GuardDecoratorBuilder
53+
54+
export const Against = (name: string): GuardDecoratorBuilder => createBuilder(name)

packages/guard-clause/src/index.ts

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,4 @@
1-
import { Against, Guard, GuardErrors } from '@atls/guard-clause'
2-
3-
type SignUpPayload = {
4-
email: string
5-
age: number
6-
roles: Array<string>
7-
profileId?: string
8-
}
9-
10-
export class UserOnboardingService {
11-
@Guard()
12-
register(
13-
@Against('email').Empty().NotStringLengthBetween(5, 128)
14-
email: string,
15-
@Against('age').NotNumberBetween(16, 120)
16-
age: number,
17-
@Against('roles').Optional.Each.NotOneOf(['root', 'god-mode'])
18-
roles: Array<string> = [],
19-
@Against('profileId').Optional.NotUUID('4')
20-
profileId?: string
21-
): SignUpPayload {
22-
return { email, age, roles, profileId }
23-
}
24-
}
25-
26-
const onboarding = new UserOnboardingService()
27-
28-
try {
29-
onboarding.register('', 14, ['root'], 'not-a-uuid')
30-
} catch (error) {
31-
if (error instanceof GuardErrors) {
32-
const messages = error.errors.map((issue) => issue.message).join('\n')
33-
34-
process.stderr.write(`Не пускаем пользователя:\n${messages}\n`)
35-
}
36-
}
1+
export * from './decorators/index.js'
2+
export * from './errors/index.js'
3+
export * from './extensions/index.js'
4+
export * from './factory/index.js'

packages/logger/src/sonic-boom.utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ function autoEnd(stream: any, eventName: string) {
4141
}
4242

4343
export const build = () => {
44-
// @ts-expect-error -- sonic-boom typings miss the construct signature in nodenext mode
4544
const stream = new SonicBoom({ fd: process.stdout.fd || 1 })
4645

4746
stream.on('error', filterBrokenPipe)

0 commit comments

Comments
 (0)