Skip to content

Commit 438d4fe

Browse files
committed
feat: add money validation
1 parent c6d981e commit 438d4fe

File tree

7 files changed

+232
-203
lines changed

7 files changed

+232
-203
lines changed

.stacks/core/utils/src/currency.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ export {
2525
haveSameAmount,
2626
dinero as currency,
2727
} from 'dinero.js'
28+
export * from '@dinero.js/currencies'
29+
export type { Dinero, Currency } from 'dinero.js'

.stacks/core/validation/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './is'
22
export * from './validate'
3+
export * from './types'

.stacks/core/validation/src/is.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,7 @@
1-
import { currency, getTypeName, toString } from '@stacksjs/utils'
1+
import { getTypeName, toString } from '@stacksjs/utils'
22
import type { HashAlgorithm } from '@stacksjs/types'
33
import { validator } from '@stacksjs/validation'
44
import v from 'validator'
5-
import { USD } from '@dinero.js/currencies'
6-
import type { FieldContext } from '@vinejs/vine/build/src/types'
7-
8-
export const isMoney = validator.createRule((value: unknown, _, field: FieldContext) => {
9-
/**
10-
* Convert string representation of a number to a JavaScript
11-
* Number data type.
12-
*/
13-
const numericValue = validator.helpers.asNumber(value)
14-
15-
/**
16-
* Report error, if the value is NaN post-conversion
17-
*/
18-
if (Number.isNaN(numericValue)) {
19-
field.report(
20-
'The {{ field }} field value must be a number',
21-
'money',
22-
field,
23-
)
24-
return
25-
}
26-
27-
/**
28-
* Create amount type
29-
*/
30-
const amount = currency({ amount: numericValue, currency: USD })
31-
32-
/**
33-
* Mutate the field's value
34-
*/
35-
field.mutate(amount, field)
36-
})
375

386
export function isEmail(email: string) {
397
return validator.helpers.isEmail(email)
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import process from 'node:process'
2+
import validator from '@vinejs/vine'
3+
import type { Infer } from '@vinejs/vine/build/src/types'
4+
import { loadEnv } from 'vite'
5+
import { projectPath } from '@stacksjs/path'
6+
import { handleError } from '@stacksjs/error-handling'
7+
import { log } from '@stacksjs/logging'
8+
9+
export const envPrefix: string | string[] = ['FRONTEND_', 'APP_', 'DB_', 'REDIS_', 'AWS_', 'MAIL_', 'SEARCH_ENGINE_', 'MEILISEARCH_']
10+
11+
// TODO: envSchema needs to be auto generated from the .env file
12+
// envSchema could also be named "backendEnvSchema"
13+
export const envSchema = validator.object({
14+
APP_NAME: validator.string().optional(),
15+
APP_ENV: validator.enum(['local', 'development', 'staging', 'production']).optional(),
16+
APP_KEY: validator.string().optional(),
17+
APP_URL: validator.string(),
18+
APP_DEBUG: validator.enum(['true', 'false']).transform(Boolean).optional(),
19+
APP_SUBDOMAIN_API: validator.string().optional(),
20+
APP_SUBDOMAIN_DOCS: validator.string().optional(),
21+
APP_SUBDOMAIN_LIBRARY: validator.string().optional(),
22+
APP_BUCKET: validator.string().optional(),
23+
24+
DB_CONNECTION: validator.string().optional(),
25+
DB_HOST: validator.string().optional(),
26+
DB_PORT: validator.string().regex(/^\d*$/).transform(Number).optional(),
27+
DB_DATABASE: validator.string().optional(),
28+
DB_USERNAME: validator.string().optional(),
29+
DB_PASSWORD: validator.string().optional(),
30+
31+
SEARCH_ENGINE_DRIVER: validator.string().optional(),
32+
MEILISEARCH_HOST: validator.string().optional(),
33+
MEILISEARCH_KEY: validator.string().optional(),
34+
35+
CACHE_DRIVER: validator.enum(['dynamodb', 'memcached', 'redis']).optional(),
36+
CACHE_PREFIX: validator.string().optional(),
37+
CACHE_TTL: validator.string().regex(/^\d*$/).transform(Number).optional(),
38+
39+
AWS_ACCESS_KEY_ID: validator.string().optional(),
40+
AWS_SECRET_ACCESS_KEY: validator.string().optional(),
41+
AWS_DEFAULT_REGION: validator.string().optional(),
42+
DYNAMODB_CACHE_TABLE: validator.string().optional(),
43+
DYNAMODB_ENDPOINT: validator.string().optional(),
44+
45+
MEMCACHED_PERSISTENT_ID: validator.string().optional(),
46+
MEMCACHED_USERNAME: validator.string().optional(),
47+
MEMCACHED_PASSWORD: validator.string().optional(),
48+
MEMCACHED_HOST: validator.string().optional(),
49+
MEMCACHED_PORT: validator.string().regex(/^\d*$/).transform(Number).optional(),
50+
51+
REDIS_HOST: validator.string().optional(),
52+
REDIS_PORT: validator.string().regex(/^\d*$/).transform(Number).optional(),
53+
54+
MAIL_FROM_NAME: validator.string().optional(),
55+
MAIL_FROM_ADDRESS: validator.string().optional(),
56+
57+
EMAILJS_HOST: validator.string().optional(),
58+
EMAILJS_USERNAME: validator.string().optional(),
59+
EMAILJS_PASSWORD: validator.string().optional(),
60+
EMAILJS_PORT: validator.string().regex(/^\d*$/).transform(Number).optional(),
61+
EMAILJS_SECURE: validator.enum(['true', 'false']).transform(Boolean).optional(),
62+
63+
MAILGUN_API_KEY: validator.string().optional(),
64+
MAILGUN_DOMAIN: validator.string().optional(),
65+
MAILGUN_USERNAME: validator.string().optional(),
66+
67+
MAILJET_API_KEY: validator.string().optional(),
68+
MAILJET_API_SECRET: validator.string().optional(),
69+
70+
MANDRILL_API_KEY: validator.string().optional(),
71+
72+
NETCORE_API_KEY: validator.string().optional(),
73+
74+
NODEMAILER_HOST: validator.string().optional(),
75+
NODEMAILER_USERNAME: validator.string().optional(),
76+
NODEMAILER_PASSWORD: validator.string().optional(),
77+
NODEMAILER_PORT: validator.string().regex(/^\d*$/).transform(Number).optional(),
78+
NODEMAILER_SECURE: validator.enum(['true', 'false']).transform(Boolean).optional(),
79+
80+
POSTMARK_API_TOKEN: validator.string().optional(),
81+
POSTMARK_API_KEY: validator.string().optional(),
82+
83+
SENDGRID_API_KEY: validator.string().optional(),
84+
SENDGRID_SENDER_NAME: validator.string().optional(),
85+
86+
SES_API_VERSION: validator.string().optional(),
87+
SES_ACCESS_KEY_ID: validator.string().optional(),
88+
SES_SECRET_ACCESS_KEY: validator.string().optional(),
89+
SES_REGION: validator.string().optional(),
90+
91+
FROM_PHONE_NUMBER: validator.string().optional(),
92+
TWILIO_ACCOUNT_SID: validator.string().optional(),
93+
TWILIO_AUTH_TOKEN: validator.string().optional(),
94+
95+
VONAGE_API_KEY: validator.string().optional(),
96+
VONAGE_API_SECRET: validator.string().optional(),
97+
98+
GUPSHUP_USER_ID: validator.string().optional(),
99+
GUPSHUP_PASSWORD: validator.string().optional(),
100+
101+
PLIVO_ACCOUNT_ID: validator.string().optional(),
102+
PLIVO_AUTH_TOKEN: validator.string().optional(),
103+
104+
SMS77_API_KEY: validator.string().optional(),
105+
106+
SNS_REGION: validator.string().optional(),
107+
SNS_ACCESS_KEY_ID: validator.string().optional(),
108+
SNS_SECRET_ACCESS_KEY: validator.string().optional(),
109+
110+
TELNYX_API_KEY: validator.string().optional(),
111+
TELNYX_MESSAGE_PROFILE_ID: validator.string().optional(),
112+
113+
TERMII_API_KEY: validator.string().optional(),
114+
115+
SLACK_FROM: validator.string().optional(),
116+
SLACK_APPLICATION_ID: validator.string().optional(),
117+
SLACK_CLIENT_ID: validator.string().optional(),
118+
SLACK_SECRET_KEY: validator.string().optional(),
119+
120+
MICROSOFT_TEAMS_APPLICATION_ID: validator.string().optional(),
121+
MICROSOFT_TEAMS_CLIENT_ID: validator.string().optional(),
122+
MICROSOFT_TEAMS_SECRET: validator.string().optional(),
123+
})
124+
125+
export const frontendEnvSchema = validator.object({
126+
FRONTEND_APP_ENV: validator.enum(['local', 'development', 'staging', 'production']).optional(),
127+
FRONTEND_APP_URL: validator.string().optional(),
128+
})
129+
130+
export type BackendEnv = Infer<typeof backendEnvSchema>
131+
export type BackendEnvKeys = keyof BackendEnv
132+
133+
export type FrontendEnv = Infer<typeof frontendEnvSchema>
134+
export type FrontendEnvKeys = keyof FrontendEnv
135+
136+
export type Env = Infer<typeof envSchema>
137+
export type EnvKeys = keyof Env
138+
139+
export async function env() {
140+
try {
141+
const mode = process.env.NODE_ENV ?? 'development'
142+
const data = loadEnv(mode, projectPath(), envPrefix)
143+
const v = validator.compile(envSchema)
144+
145+
return await v.validate(data)
146+
}
147+
catch (error: any) {
148+
// if (error instanceof errors.E_VALIDATION_ERROR)
149+
// console.log(error.messages)
150+
handleError(error)
151+
return { success: false, error }
152+
}
153+
}
154+
155+
export function getEnvIssues(env?: any): void {
156+
const result = env()
157+
158+
if (!result.success) {
159+
const message = result.error.message
160+
log.error(message)
161+
return
162+
}
163+
164+
return result
165+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './env'
2+
export * from './money'
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { FieldContext, FieldOptions, Validation } from '@vinejs/vine/build/src/types'
2+
import type { Dinero } from '@stacksjs/utils'
3+
import { USD, currency } from '@stacksjs/utils'
4+
import { BaseLiteralType } from '@vinejs/vine'
5+
import { validator } from '../'
6+
7+
export const isMoney = validator.createRule((value: unknown, _, field: FieldContext) => {
8+
/**
9+
* Convert string representation of a number to a JavaScript
10+
* Number data type.
11+
*/
12+
const numericValue = validator.helpers.asNumber(value)
13+
14+
/**
15+
* Report error, if the value is NaN post-conversion
16+
*/
17+
if (Number.isNaN(numericValue)) {
18+
field.report(
19+
'The {{ field }} field value must be a number',
20+
'money',
21+
field,
22+
)
23+
return
24+
}
25+
26+
/**
27+
* Create amount type
28+
*/
29+
const amount = currency({ amount: numericValue, currency: USD })
30+
31+
/**
32+
* Mutate the field's value
33+
*/
34+
field.mutate(amount, field)
35+
})
36+
37+
export type Money = Dinero<number>
38+
39+
export class ValidatorMoney extends BaseLiteralType<Money, Money> {
40+
constructor(options?: FieldOptions, validations?: Validation<any>[]) {
41+
super(options, validations || [isMoney()])
42+
}
43+
44+
clone() {
45+
return new ValidatorMoney(
46+
this.cloneOptions(),
47+
this.cloneValidations(),
48+
) as this
49+
}
50+
}

0 commit comments

Comments
 (0)