@anjianshi/utils 2.9.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +10 -0
  2. package/eslint.config.cjs +33 -0
  3. package/package.json +19 -10
  4. package/publish-prepare.cjs +16 -0
  5. package/src/env-browser/device.ts +62 -0
  6. package/src/env-browser/global.ts +21 -0
  7. package/src/env-browser/load-script.ts +13 -0
  8. package/src/env-browser/logging.ts +58 -0
  9. package/src/env-browser/manage-vconsole.ts +54 -0
  10. package/src/env-node/crypto-random.ts +30 -0
  11. package/src/env-node/fs.ts +50 -0
  12. package/src/env-node/index.ts +5 -0
  13. package/src/env-node/logging/handlers.ts +190 -0
  14. package/src/env-node/logging/index.ts +16 -0
  15. package/{env-react/emotion-register-globals.d.ts → src/env-react/emotion-register-globals.ts} +5 -2
  16. package/src/env-react/emotion.tsx +42 -0
  17. package/src/env-react/hooks.ts +59 -0
  18. package/src/env-react/index.ts +1 -0
  19. package/src/env-react/react-register-globals.ts +53 -0
  20. package/src/env-service/controllers.ts +93 -0
  21. package/src/env-service/env-reader.ts +141 -0
  22. package/src/env-service/index.ts +6 -0
  23. package/src/env-service/prisma/adapt-logging.ts +39 -0
  24. package/src/env-service/prisma/extensions/exist.ts +21 -0
  25. package/src/env-service/prisma/extensions/find-and-count.ts +24 -0
  26. package/src/env-service/prisma/extensions/soft-delete.ts +162 -0
  27. package/src/env-service/prisma/extensions/with-transaction.ts +65 -0
  28. package/src/env-service/prisma/index.ts +6 -0
  29. package/src/env-service/prisma/transaction-contexted.ts +80 -0
  30. package/src/env-service/redis-cache.ts +142 -0
  31. package/src/env-service/tasks.ts +45 -0
  32. package/src/index.ts +4 -0
  33. package/src/init-dayjs.ts +8 -0
  34. package/src/lang/async.ts +47 -0
  35. package/src/lang/color.ts +119 -0
  36. package/src/lang/index.ts +8 -0
  37. package/src/lang/object.ts +39 -0
  38. package/src/lang/random.ts +25 -0
  39. package/src/lang/result.ts +78 -0
  40. package/src/lang/string.ts +95 -0
  41. package/src/lang/time.ts +19 -0
  42. package/{lang/types.d.ts → src/lang/types.ts} +43 -23
  43. package/src/logging/adapt.ts +49 -0
  44. package/src/logging/formatters.ts +23 -0
  45. package/src/logging/index.ts +106 -0
  46. package/src/md5.ts +318 -0
  47. package/src/safe-request.ts +193 -0
  48. package/src/url.ts +185 -0
  49. package/src/validators/array.ts +97 -0
  50. package/src/validators/base.ts +145 -0
  51. package/src/validators/boolean.ts +21 -0
  52. package/src/validators/datetime.ts +39 -0
  53. package/src/validators/factory.ts +244 -0
  54. package/src/validators/index.ts +9 -0
  55. package/src/validators/number.ts +54 -0
  56. package/src/validators/object.ts +101 -0
  57. package/src/validators/one-of.ts +33 -0
  58. package/src/validators/string.ts +72 -0
  59. package/env-browser/device.d.ts +0 -24
  60. package/env-browser/device.js +0 -50
  61. package/env-browser/global.d.ts +0 -10
  62. package/env-browser/global.js +0 -15
  63. package/env-browser/load-script.d.ts +0 -5
  64. package/env-browser/load-script.js +0 -13
  65. package/env-browser/logging.d.ts +0 -18
  66. package/env-browser/logging.js +0 -49
  67. package/env-browser/manage-vconsole.d.ts +0 -16
  68. package/env-browser/manage-vconsole.js +0 -38
  69. package/env-node/crypto-random.d.ts +0 -13
  70. package/env-node/crypto-random.js +0 -28
  71. package/env-node/fs.d.ts +0 -19
  72. package/env-node/fs.js +0 -48
  73. package/env-node/index.d.ts +0 -6
  74. package/env-node/index.js +0 -6
  75. package/env-node/logging/handlers.d.ts +0 -58
  76. package/env-node/logging/handlers.js +0 -154
  77. package/env-node/logging/index.d.ts +0 -11
  78. package/env-node/logging/index.js +0 -14
  79. package/env-node/safe-request.d.ts +0 -26
  80. package/env-node/safe-request.js +0 -40
  81. package/env-react/emotion-register-globals.js +0 -5
  82. package/env-react/emotion.d.ts +0 -20
  83. package/env-react/emotion.jsx +0 -34
  84. package/env-react/hooks.d.ts +0 -4
  85. package/env-react/hooks.js +0 -16
  86. package/env-react/index.d.ts +0 -1
  87. package/env-react/index.js +0 -1
  88. package/env-react/react-register-globals.d.ts +0 -21
  89. package/env-react/react-register-globals.js +0 -19
  90. package/env-service/controllers.d.ts +0 -30
  91. package/env-service/controllers.js +0 -41
  92. package/env-service/env-reader.d.ts +0 -55
  93. package/env-service/env-reader.js +0 -79
  94. package/env-service/index.d.ts +0 -6
  95. package/env-service/index.js +0 -6
  96. package/env-service/prisma/adapt-logging.d.ts +0 -21
  97. package/env-service/prisma/adapt-logging.js +0 -30
  98. package/env-service/prisma/extensions/exist.d.ts +0 -10
  99. package/env-service/prisma/extensions/exist.js +0 -16
  100. package/env-service/prisma/extensions/find-and-count.d.ts +0 -7
  101. package/env-service/prisma/extensions/find-and-count.js +0 -19
  102. package/env-service/prisma/extensions/soft-delete.d.ts +0 -52
  103. package/env-service/prisma/extensions/soft-delete.js +0 -123
  104. package/env-service/prisma/extensions/with-transaction.d.ts +0 -9
  105. package/env-service/prisma/extensions/with-transaction.js +0 -54
  106. package/env-service/prisma/index.d.ts +0 -6
  107. package/env-service/prisma/index.js +0 -6
  108. package/env-service/prisma/transaction-contexted.d.ts +0 -11
  109. package/env-service/prisma/transaction-contexted.js +0 -52
  110. package/env-service/redis-cache.d.ts +0 -39
  111. package/env-service/redis-cache.js +0 -116
  112. package/env-service/tasks.d.ts +0 -12
  113. package/env-service/tasks.js +0 -37
  114. package/index.d.ts +0 -3
  115. package/index.js +0 -3
  116. package/init-dayjs.d.ts +0 -2
  117. package/init-dayjs.js +0 -7
  118. package/lang/async.d.ts +0 -19
  119. package/lang/async.js +0 -34
  120. package/lang/color.d.ts +0 -37
  121. package/lang/color.js +0 -111
  122. package/lang/index.d.ts +0 -8
  123. package/lang/index.js +0 -8
  124. package/lang/may-success.d.ts +0 -40
  125. package/lang/may-success.js +0 -27
  126. package/lang/object.d.ts +0 -12
  127. package/lang/object.js +0 -41
  128. package/lang/random.d.ts +0 -13
  129. package/lang/random.js +0 -24
  130. package/lang/string.d.ts +0 -29
  131. package/lang/string.js +0 -92
  132. package/lang/time.d.ts +0 -10
  133. package/lang/time.js +0 -18
  134. package/lang/types.js +0 -28
  135. package/logging/adapt.d.ts +0 -10
  136. package/logging/adapt.js +0 -43
  137. package/logging/formatters.d.ts +0 -10
  138. package/logging/formatters.js +0 -22
  139. package/logging/index.d.ts +0 -45
  140. package/logging/index.js +0 -90
  141. package/md5.d.ts +0 -30
  142. package/md5.js +0 -308
  143. package/url.d.ts +0 -77
  144. package/url.js +0 -149
  145. package/validators/array.d.ts +0 -30
  146. package/validators/array.js +0 -47
  147. package/validators/base.d.ts +0 -82
  148. package/validators/base.js +0 -42
  149. package/validators/boolean.d.ts +0 -3
  150. package/validators/boolean.js +0 -22
  151. package/validators/datetime.d.ts +0 -12
  152. package/validators/datetime.js +0 -30
  153. package/validators/factory.d.ts +0 -70
  154. package/validators/factory.js +0 -121
  155. package/validators/index.d.ts +0 -9
  156. package/validators/index.js +0 -9
  157. package/validators/number.d.ts +0 -19
  158. package/validators/number.js +0 -26
  159. package/validators/object.d.ts +0 -28
  160. package/validators/object.js +0 -49
  161. package/validators/one-of.d.ts +0 -10
  162. package/validators/one-of.js +0 -15
  163. package/validators/string.d.ts +0 -22
  164. package/validators/string.js +0 -35
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 通过 React Hook 把 emotion css 转换成 className
3
+ * (不再需要 <ClassName>)
4
+ *
5
+ * 使用前提:
6
+ * 1. 只支持浏览器渲染
7
+ * 2. 用 EmotionCacheProvider 包裹 App 根元素
8
+ *
9
+ * 来自:
10
+ * https://github.com/emotion-js/emotion/issues/1853#issuecomment-623349622
11
+ */
12
+ import { type EmotionCache, withEmotionCache } from '@emotion/react'
13
+ import { type CSSInterpolation, serializeStyles } from '@emotion/serialize'
14
+ import { insertStyles } from '@emotion/utils'
15
+ import { createContext, useContext, useCallback } from 'react'
16
+
17
+ const CacheContext = createContext<EmotionCache | undefined>(undefined)
18
+ export const useEmotionCache = () => useContext(CacheContext)
19
+
20
+ export const EmotionCacheProvider = withEmotionCache(
21
+ ({ children }: { children: React.ReactNode }, cache: EmotionCache) => {
22
+ return <CacheContext.Provider value={cache}>{children}</CacheContext.Provider>
23
+ },
24
+ )
25
+
26
+ export function useEmotionClassName(): (...args: CSSInterpolation[]) => string {
27
+ const cache = useEmotionCache()
28
+ return useCallback(
29
+ (...args) => {
30
+ if (!cache) {
31
+ if (process.env.NODE_ENV === 'production') {
32
+ return 'emotion-cache-missing'
33
+ }
34
+ throw new Error('No emotion cache found!')
35
+ }
36
+ const serialized = serializeStyles(args, cache.registered)
37
+ insertStyles(cache, serialized, false)
38
+ return cache.key + '-' + serialized.name
39
+ },
40
+ [cache],
41
+ )
42
+ }
@@ -0,0 +1,59 @@
1
+ import { useRef, useCallback } from 'react'
2
+
3
+ /**
4
+ * 生成一个 state 以及与其值同步的 ref
5
+ */
6
+ export function useStateWithRef<T>(initialValue: T | (() => T)) {
7
+ const [state, setState] = useState(initialValue)
8
+ const ref = useRef(state)
9
+
10
+ const setStateWithRef: typeof setState = useCallback((value: T | ((prevState: T) => T)) => {
11
+ setState(prevState => {
12
+ const newValue =
13
+ typeof value === 'function' ? (value as (prevState: T) => T)(prevState) : value
14
+ ref.current = newValue
15
+ return newValue
16
+ })
17
+ }, [])
18
+
19
+ return [state, setStateWithRef, ref] as const
20
+ }
21
+
22
+ /**
23
+ * 在 useEffect() 中执行异步操作。
24
+ * 因是异步运行,对清理机制的处理有所变化。
25
+ *
26
+ * 原本 useEffect() 中,通过返回一个函数来设置清理条件,在异步操作中不适用。
27
+ * 此外,同步内容是一定会在执行完只后才有可能触发清理的,但异步内容有可能在运行完之前,依赖就已经变化,触发了清理。
28
+ *
29
+ * 这里通过一个 context 对象来应对异步的情况。
30
+ * context.cancelled 值代表此次运行是否已被取消(清理),异步操作执行过程中如果发现此值变为 true,则可停止执行了。
31
+ * context.onCancelled(() => {}) 可注册一个回调,此次执行被清理时触发。多次调用仅保留最后一次的回调。
32
+ *
33
+ * 注意:需要配置 ESLint react-hooks/exhaustive-deps 规则以保证 deps 参与依赖检查
34
+ * 详见 https://react.dev/reference/eslint-plugin-react-hooks/lints/exhaustive-deps
35
+ */
36
+ export function useAsyncEffect(
37
+ callback: (context: AsyncEffectContext) => Promise<void>,
38
+ deps: unknown[] = [],
39
+ ) {
40
+ useEffect(() => {
41
+ let onCancel: (() => void) | null = null
42
+ const context = {
43
+ cancelled: false,
44
+ onCancel(callback: () => void) {
45
+ onCancel = callback
46
+ },
47
+ }
48
+
49
+ void callback(context)
50
+ return () => {
51
+ context.cancelled = true
52
+ if (onCancel) onCancel()
53
+ }
54
+ }, deps) // eslint-disable-line ts-react-hooks/exhaustive-deps
55
+ }
56
+ export interface AsyncEffectContext {
57
+ cancelled: boolean
58
+ onCancel: (callback: () => void) => void
59
+ }
@@ -0,0 +1 @@
1
+ export * from './hooks.js'
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 把 React Hooks 注册成全局变量,就不用每次使用都手动引入了
3
+ * Hooks 列表见:https://react.dev/reference/react/hooks
4
+ */
5
+
6
+ import {
7
+ useState as useStateValue,
8
+ useReducer as useReducerValue,
9
+ useContext as useContextValue,
10
+ useRef as useRefValue,
11
+ useImperativeHandle as useImperativeHandleValue,
12
+ useEffect as useEffectValue,
13
+ useMemo as useMemoValue,
14
+ useCallback as useCallbackValue,
15
+ useTransition as useTransitionValue,
16
+ useDeferredValue as useDeferredValueValue,
17
+ useDebugValue as useDebugValueValue,
18
+ useId as useIdValue,
19
+ useSyncExternalStore as useSyncExternalStoreValue,
20
+ useActionState as useActionStateValue,
21
+ } from 'react'
22
+
23
+ declare global {
24
+ var useState: typeof useStateValue
25
+ var useReducer: typeof useReducerValue
26
+ var useContext: typeof useContextValue
27
+ var useRef: typeof useRefValue
28
+ var useImperativeHandle: typeof useImperativeHandleValue
29
+ var useEffect: typeof useEffectValue
30
+ var useMemo: typeof useMemoValue
31
+ var useCallback: typeof useCallbackValue
32
+ var useTransition: typeof useTransitionValue
33
+ var useDeferredValue: typeof useDeferredValueValue
34
+ var useDebugValue: typeof useDebugValueValue
35
+ var useId: typeof useIdValue
36
+ var useSyncExternalStore: typeof useSyncExternalStoreValue
37
+ var useActionState: typeof useActionStateValue
38
+ }
39
+
40
+ globalThis.useState = useStateValue
41
+ globalThis.useReducer = useReducerValue
42
+ globalThis.useContext = useContextValue
43
+ globalThis.useRef = useRefValue
44
+ globalThis.useImperativeHandle = useImperativeHandleValue
45
+ globalThis.useEffect = useEffectValue
46
+ globalThis.useMemo = useMemoValue
47
+ globalThis.useCallback = useCallbackValue
48
+ globalThis.useTransition = useTransitionValue
49
+ globalThis.useDeferredValue = useDeferredValueValue
50
+ globalThis.useDebugValue = useDebugValueValue
51
+ globalThis.useId = useIdValue
52
+ globalThis.useSyncExternalStore = useSyncExternalStoreValue
53
+ globalThis.useActionState = useActionStateValue
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 把业务功能整理成各个 Controller,
3
+ * 并整合成一个 controllers 对象方便外部引用和 Controller 之间互相引用。
4
+ *
5
+ * 支持自定义 Controller 类,例如把 context 中的内容定义成属性。
6
+ */
7
+
8
+ /*
9
+ 【使用范例】
10
+
11
+ // 定制 Context 和 Controller
12
+ interface MyContext {
13
+ prop1: number
14
+ prop2: string
15
+ }
16
+ class MyController<
17
+ AllControllers extends Record<string, AnyController<MyContext>>,
18
+ > extends Controller<MyContext, AllControllers> {
19
+ get prop1() {
20
+ return this.context.prop1
21
+ }
22
+ get prop2() {
23
+ return this.context.prop2
24
+ }
25
+ }
26
+
27
+ // 实现 Controller 内容
28
+ class C1 extends MyController<Controllers> {
29
+ someMethod() {
30
+ console.log(this.prop1)
31
+ console.log(this.controllers.c2.prop2)
32
+ }
33
+ }
34
+ class C2 extends MyController<Controllers> {}
35
+ class C3 extends MyController<Controllers> {}
36
+
37
+ // 生成 controllers 类型和对象
38
+ export const controllerClasses = { c1: C1, c2: C2, c3: C3 }
39
+ export type Controllers = ControllersFrom<MyContext, typeof controllerClasses>
40
+ initializeControllers({ c1: C1, c2: C2, c3: C3 }, { prop1: 1, prop2: 2 })
41
+ */
42
+
43
+ export type AnyObject = Record<string, unknown>
44
+ export type AnyController<Context> = Controller<Context, any> // eslint-disable-line @typescript-eslint/no-explicit-any
45
+ type AnyControllerClass<Context> = typeof Controller<Context, any> // eslint-disable-line @typescript-eslint/no-explicit-any
46
+ type ControllerClassesFrom<Context, T extends AnyObject> = {
47
+ [K in keyof T]: T[K] extends AnyControllerClass<Context> ? T[K] : never
48
+ }
49
+ export type ControllersFrom<Context, T extends AnyObject> = {
50
+ [K in keyof T]: T[K] extends AnyControllerClass<Context> ? InstanceType<T[K]> : never
51
+ }
52
+
53
+ /**
54
+ * Controller 基类
55
+ */
56
+ export class Controller<Context, AllControllers extends Record<string, AnyController<Context>>> {
57
+ constructor(
58
+ /** 调用其他 controllers */
59
+ protected readonly controllers: AllControllers,
60
+
61
+ protected readonly context: Context,
62
+
63
+ protected readonly name: string = this.constructor.name,
64
+ ) {}
65
+ }
66
+
67
+ /**
68
+ * 传入 Controller 类列表,返回 controller 实例集合。
69
+ * 为优化性能,每个 controller 只有在被使用到时才会实例化。
70
+ */
71
+ export function initializeControllers<Context, T extends AnyObject>(
72
+ controllerClasses: T,
73
+ context: Context,
74
+ ) {
75
+ type Classes = ControllerClassesFrom<Context, T>
76
+ type Controllers = ControllersFrom<Context, T>
77
+ const proxy = new Proxy({} as Controllers, {
78
+ get(controllers, prop) {
79
+ if (typeof prop !== 'string') return
80
+ if (prop in controllers) return controllers[prop]
81
+ if (prop in controllerClasses) {
82
+ const Class = controllerClasses[prop]! as typeof Controller<Context, Controllers>
83
+ controllers[prop as keyof Classes] = new Class(
84
+ proxy,
85
+ context,
86
+ prop,
87
+ ) as Controllers[keyof Classes]
88
+ return controllers[prop]
89
+ }
90
+ },
91
+ })
92
+ return proxy
93
+ }
@@ -0,0 +1,141 @@
1
+ import * as dotenv from 'dotenv'
2
+ import { safeParseJSON } from '../lang/string.js'
3
+
4
+ type EnvValue = string | number | boolean | unknown[] | Record<string, unknown>
5
+
6
+ type TypeDef = 'string' | 'number' | 'boolean' | unknown[] | Record<string, unknown>
7
+ type TypeFrom<D extends TypeDef> = D extends 'string'
8
+ ? string
9
+ : D extends 'number'
10
+ ? number
11
+ : D extends 'boolean'
12
+ ? boolean
13
+ : D
14
+ type ResultFrom<Defs extends Record<string, TypeDef>> = {
15
+ [K in keyof Defs]: TypeFrom<Defs[K]>
16
+ }
17
+
18
+ /**
19
+ * 读取 .env 文件,并获取格式化后的数据
20
+ * 注意:依赖 dotenv 包
21
+ */
22
+ export class EnvReader {
23
+ protected loadedEnvs: Record<string, string> = {}
24
+
25
+ constructor(options: dotenv.DotenvConfigOptions = {}) {
26
+ dotenv.config({
27
+ ...options,
28
+
29
+ // 把从 .env 文件读到的内容写入此变量,而不是 process.env,以避免污染 process.env。
30
+ processEnv: this.loadedEnvs,
31
+ })
32
+ }
33
+
34
+ protected toNumber(raw: string) {
35
+ const num = parseInt(raw, 10)
36
+ return isFinite(num) ? num : undefined
37
+ }
38
+ protected toBoolean(raw: string) {
39
+ const formatted = raw.toLowerCase().trim()
40
+ if (['1', 'true', 'on'].includes(formatted)) return true
41
+ if (['0', 'false', 'off'].includes(formatted)) return false
42
+ return undefined
43
+ }
44
+
45
+ getRaw(key: string) {
46
+ return this.loadedEnvs[key] ?? process.env[key]
47
+ }
48
+
49
+ /**
50
+ * 获取指定 env 的值,并转换成与 defaults 匹配的类型。
51
+ * 若值不存在,返回 defaults。
52
+ */
53
+ get(key: string, defaults: string): string
54
+ get(key: string, defaults: number): number
55
+ get(key: string, defaults: boolean): boolean
56
+ get<T extends unknown[] | Record<string, unknown>>(key: string, defaults: T): T
57
+ get(key: string, defaults: EnvValue) {
58
+ const raw = this.getRaw(key)
59
+ if (raw === undefined) return defaults
60
+
61
+ if (typeof defaults === 'number') return this.toNumber(raw) ?? defaults
62
+ else if (typeof defaults === 'boolean') return this.toBoolean(raw) ?? defaults
63
+ else if (Array.isArray(defaults) || typeof defaults === 'object') {
64
+ return safeParseJSON(raw) ?? defaults
65
+ }
66
+
67
+ return raw
68
+ }
69
+
70
+ /**
71
+ * 获取指定 env 的值,并转换成指定类型,无需提供默认值。
72
+ * 值不存在或转换失败时,返回 undefined。
73
+ */
74
+ getByType(key: string, type?: 'string'): string | undefined
75
+ getByType(key: string, type: 'number'): number | undefined
76
+ getByType(key: string, type: 'boolean'): boolean | undefined
77
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
78
+ getByType<T extends unknown[] | Record<string, unknown>>(key: string, type: 'json'): T | undefined
79
+ getByType(key: string, type: 'string' | 'number' | 'boolean' | 'json' = 'string') {
80
+ const raw = this.getRaw(key)
81
+ if (raw === undefined) return raw
82
+ if (type === 'number') return this.toNumber(raw)
83
+ if (type === 'boolean') return this.toBoolean(raw)
84
+ if (type === 'json') return safeParseJSON(raw)
85
+ return raw
86
+ }
87
+
88
+ /**
89
+ * 同 envReader.get(),只不过是通过对象指定各 env 的默认值来批量获取
90
+ * envReader.batchGet({ port: 8000, debug: false, mobiles: ['123', '456'] }
91
+ */
92
+ batchGet<Defs extends Record<string, EnvValue>>(definitions: Defs) {
93
+ const result = {} as Record<string, unknown>
94
+ for (const [key, defaults] of Object.entries(definitions)) {
95
+ result[key] = this.get(key, defaults as string)
96
+ }
97
+
98
+ // 保证返回的值类型是“通用化”的,例如不是 `false` 而是 `boolean`
99
+ return result as {
100
+ [K in keyof Defs]: Defs[K] extends string
101
+ ? string
102
+ : Defs[K] extends number
103
+ ? number
104
+ : Defs[K] extends boolean
105
+ ? boolean
106
+ : Defs[K]
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 同 envReader.getByType(),只不过是通过对象指定各 env 的类型来批量获取。
112
+ *
113
+ * - required=false(默认)时,不存在或值为 undefined 的 env 不会出现在返回对象里,以保证 { ...defaults, ...envReader.batchGetByType(...) } 的用法能正常保留默认值。
114
+ * - required=true 时要求所有 env 都必须有值,否则会抛出异常
115
+ *
116
+ * envReader.batchGetByType({
117
+ * port: 'number',
118
+ * debug: 'boolean',
119
+ * mobiles: [] as string[], // 用此格式定义内容是数组的 JSON 值
120
+ * obj: {} as { a: number, b: string } // 用此格式定义内容是对象的 JSON 值
121
+ * })
122
+ */
123
+ batchGetByType<Defs extends Record<string, TypeDef>>(
124
+ definitions: Defs,
125
+ required?: false,
126
+ ): Partial<ResultFrom<Defs>>
127
+ batchGetByType<Defs extends Record<string, TypeDef>>(
128
+ definitions: Defs,
129
+ required: true,
130
+ ): ResultFrom<Defs>
131
+ batchGetByType<Defs extends Record<string, TypeDef>>(definitions: Defs, required = false) {
132
+ const result = {} as Record<string, unknown>
133
+ for (const [key, def] of Object.entries(definitions)) {
134
+ const value =
135
+ typeof def === 'string' ? this.getByType(key, def as 'string') : this.getByType(key, 'json')
136
+ if (value !== undefined) result[key] = value
137
+ else if (required) throw new Error(`env ${key} needs a value`)
138
+ }
139
+ return result
140
+ }
141
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 开发后端服务能用到的工具库
3
+ */
4
+ export * from './env-reader.js'
5
+ export * from './controllers.js'
6
+ export * from './tasks.js'
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 对接 Prisma 的日志记录
3
+ *
4
+ * 注意:Prisma 的 debugging 日志是直接输出到 console 的,没有提供处理渠道,所以无法记录进日志文件。
5
+ * 理论上可以重写 console.log/debug... 等方法来实现捕获,但这牵扯面太广,暂不这样做。
6
+ */
7
+ import nodeUtil from 'node:util'
8
+ import type { getPrismaClient, PrismaClientOptions } from '@prisma/client/runtime/library.js'
9
+ import chalk from 'chalk'
10
+ import { type Logger } from '../../logging/index.js'
11
+
12
+ type PrismalClient = ReturnType<typeof getPrismaClient> extends new () => infer T ? T : never
13
+
14
+ export function getPrismaLoggingOptions(debug: boolean) {
15
+ return {
16
+ errorFormat: 'pretty',
17
+ log: [
18
+ ...(debug ? [{ emit: 'event', level: 'query' } as const] : []),
19
+ { emit: 'event', level: 'info' },
20
+ { emit: 'event', level: 'warn' },
21
+ { emit: 'event', level: 'error' },
22
+ ],
23
+ } satisfies PrismaClientOptions
24
+ }
25
+
26
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
27
+ export function adaptPrismaLogging<T extends Pick<PrismalClient, '$on'>>(
28
+ prisma: T,
29
+ baseLogger: Logger,
30
+ ) {
31
+ // 记录 Prisma 相关日志
32
+ const queryLogger = baseLogger.getChild('query')
33
+ prisma.$on('query', e => {
34
+ queryLogger.debug(e.query, chalk.green(nodeUtil.format(e.params) + ` +${e.duration}ms`))
35
+ })
36
+ prisma.$on('info', e => baseLogger.info(e.message))
37
+ prisma.$on('warn', e => baseLogger.warn(e.message))
38
+ prisma.$on('error', e => baseLogger.error(e.message))
39
+ }
@@ -0,0 +1,21 @@
1
+ import { Prisma } from '@prisma/client/extension'
2
+
3
+ /**
4
+ * 快速检查指定条件的数据是否存在
5
+ * const exists = await prisma.xxx.exists({ id: '1' })
6
+ */
7
+ export const exists = Prisma.defineExtension({
8
+ name: 'exists',
9
+ model: {
10
+ $allModels: {
11
+ async exists<T>(
12
+ this: T,
13
+ where: Prisma.Args<T, 'count'>['where'],
14
+ withDeleted = false,
15
+ ): Promise<boolean> {
16
+ const context = Prisma.getExtensionContext(this)
17
+ return !!(await (context as any).count({ where, withDeleted }))
18
+ },
19
+ },
20
+ },
21
+ })
@@ -0,0 +1,24 @@
1
+ import { Prisma } from '@prisma/client/extension'
2
+ import { type SoftDeleteQueryArgs } from './soft-delete.js'
3
+
4
+ export const findAndCount = Prisma.defineExtension({
5
+ name: 'findAndCount',
6
+ model: {
7
+ $allModels: {
8
+ findAndCount<T, A>(
9
+ this: T,
10
+ rawArgs: Prisma.Exact<A, Prisma.Args<T, 'findMany'> & SoftDeleteQueryArgs>,
11
+ ) {
12
+ const context = Prisma.getExtensionContext(this)
13
+ const args = rawArgs as Prisma.Args<T, 'findMany'> & SoftDeleteQueryArgs
14
+ return Promise.all([
15
+ (context as any).findMany(args) as Promise<Prisma.Result<T, A, 'findMany'>>,
16
+ (context as any).count({
17
+ where: args.where,
18
+ withDeleted: args.withDeleted,
19
+ }) as Promise<Prisma.Result<T, A, 'count'>>,
20
+ ])
21
+ },
22
+ },
23
+ },
24
+ })
@@ -0,0 +1,162 @@
1
+ /**
2
+ * 扩展 Prisma 实现软删除
3
+ *
4
+ * 1. 有 deleteTime 字段的 model 支持软删除。
5
+ * 2. 执行 delete() 和 deleteMany() 时默认是进行软删除;可指定 soft 为 false 来彻底删除;执行软删除时可指定要额外更新的 data。
6
+ * 2. 查询时会忽略被软删除的记录;可指定 withDeleted 为 true 来包含它们。
7
+ * 4. 可通过 restore() 和 restoreMany() 恢复软删除的记录。
8
+ *
9
+ * 扩展实现方式参考:
10
+ * https://www.prisma.io/docs/orm/prisma-client/client-extensions/type-utilities#add-a-custom-property-to-a-method
11
+ * https://www.npmjs.com/package/@prisma/extension-accelerate?activeTab=code => @prisma/extension-accelerate/dist/esm/extension.js
12
+ *
13
+ * 此扩展修改了 Prisma 的原生方法。
14
+ * 为保证其他扩展也应用到修改过的这些方法,此扩展应尽可能放在最前面。
15
+ */
16
+ import { Prisma } from '@prisma/client/extension'
17
+ import type { Operation } from '@prisma/client/runtime/library.js'
18
+ import { type OptionalFields } from '../../../index.js'
19
+
20
+ type ExampleModel = any
21
+
22
+ type DeleteArgs<T> = Prisma.Args<T, 'delete'> & {
23
+ soft?: boolean
24
+ data?: Prisma.Args<T, 'update'>['data'] // 软删除时支持额外更新其他字段
25
+ }
26
+ type DeleteReturn<T, A> = Promise<Prisma.Result<T, A, 'delete'>>
27
+
28
+ type DeleteManyArgs<T> = Prisma.Args<T, 'deleteMany'> & {
29
+ soft?: boolean
30
+ data?: Prisma.Args<T, 'updateMany'>['data']
31
+ }
32
+ type DeleteManyReturn<T, A> = Promise<Prisma.Result<T, A, 'deleteMany'>>
33
+
34
+ type RestoreArgs<T> = OptionalFields<Prisma.Args<T, 'update'>, 'data'>
35
+ type RestoreManyArgs<T> = OptionalFields<Prisma.Args<T, 'updateMany'>, 'data'>
36
+
37
+ interface QueryExtraArgs {
38
+ withDeleted?: boolean
39
+ }
40
+ export type { QueryExtraArgs as SoftDeleteQueryArgs }
41
+ type QueryInputArgs<T, A, K extends Operation> = Prisma.Exact<A, Prisma.Args<T, K> & QueryExtraArgs>
42
+
43
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
44
+ function getModel<T>(that: T) {
45
+ const context = Prisma.getExtensionContext(that as ExampleModel)
46
+
47
+ // 1. 此扩展修改了 Prisma 原生的方法,所以要通过 context.$parent[context.$name] 获取上一层的 model,不然会自己调用自己导致死循环。
48
+ // 2. 如果此扩展后面还应用了其他扩展,那么仅仅一层 $parent 取得的 model 还是这个扩展修改过的版本而不是原生的。
49
+ // 此时需要递归向上,直到取得未经此扩展修改过的 model。不然此扩展的业务逻辑会被重复执行,
50
+ // 而因为第一次执行时已经把定制参数消解掉了,第二次执行时会误以为没有传入定制参数,最终导致定制参数失效。
51
+ let model = context
52
+ do {
53
+ model = (model as unknown as { $parent: Record<string, ExampleModel> }).$parent[context.$name!]!
54
+ } while ('withSoftDeleteExtension' in model)
55
+
56
+ const supportSoftDelete = 'deleteTime' in model.fields
57
+ return { model, supportSoftDelete }
58
+ }
59
+
60
+ function query<T, A, K extends Operation>(
61
+ that: T,
62
+ inputArgs: Prisma.Exact<A, Prisma.Args<T, K>>,
63
+ method: K,
64
+ ) {
65
+ const { model, supportSoftDelete } = getModel(that)
66
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
67
+ const { withDeleted = false, ...args } = inputArgs as Prisma.Args<ExampleModel, 'findFirst'> &
68
+ QueryExtraArgs
69
+
70
+ return model[method]({
71
+ ...args,
72
+ where: !supportSoftDelete || withDeleted ? args.where : { ...args.where, deleteTime: null },
73
+ }) as Promise<Prisma.Result<T, A, K>>
74
+ }
75
+
76
+ export const softDelete = Prisma.defineExtension({
77
+ name: 'softDeleted',
78
+ model: {
79
+ $allModels: {
80
+ withSoftDeleteExtension: true,
81
+
82
+ // -----------------------------
83
+ // 操作
84
+ // -----------------------------
85
+
86
+ delete<T, A>(this: T, rawArgs: Prisma.Exact<A, DeleteArgs<T>>) {
87
+ const { model, supportSoftDelete } = getModel(this)
88
+ const { soft = true, data, ...args } = rawArgs as DeleteArgs<ExampleModel>
89
+ if (supportSoftDelete && soft) {
90
+ return model.update({
91
+ ...args, // .delete() 的参数 .update() 也都支持
92
+ data: { ...(data ?? {}), deleteTime: new Date() },
93
+ }) as unknown as DeleteReturn<T, A> // .update() 的返回值和 .delete() 一样
94
+ } else {
95
+ return model.delete(args) as unknown as DeleteReturn<T, A>
96
+ }
97
+ },
98
+
99
+ deleteMany<T, A>(this: T, rawArgs: Prisma.Exact<A, DeleteManyArgs<T>>) {
100
+ const { model, supportSoftDelete } = getModel(this)
101
+ const { soft = true, data, ...args } = rawArgs as DeleteManyArgs<ExampleModel>
102
+ if (supportSoftDelete && soft) {
103
+ return model.updateMany({
104
+ ...args, // .deleteMany() 的参数 .updateMany() 也都支持
105
+ data: { ...(data ?? {}), deleteTime: new Date() },
106
+ }) as DeleteManyReturn<T, A> // .updateMany() 的返回值和 .deleteMany() 一样
107
+ } else {
108
+ return model.deleteMany(args) as DeleteManyReturn<T, A>
109
+ }
110
+ },
111
+
112
+ restore<T, A>(this: T, rawArgs: Prisma.Exact<A, RestoreArgs<T>>) {
113
+ const { data, ...args } = rawArgs as RestoreArgs<ExampleModel>
114
+ const { model, supportSoftDelete } = getModel(this)
115
+ if (!supportSoftDelete) throw new Error('当前模型不支持软删除,不能执行恢复')
116
+ return model.update({
117
+ ...(args as Prisma.Args<ExampleModel, 'update'>),
118
+ data: { ...(data ?? {}), deleteTime: null },
119
+ }) as unknown as Promise<Prisma.Result<T, A, 'update'>>
120
+ },
121
+
122
+ restoreMany<T, A>(this: T, rawArgs: Prisma.Exact<A, RestoreManyArgs<T>>) {
123
+ const { data, ...args } = rawArgs as RestoreArgs<ExampleModel>
124
+ const { model, supportSoftDelete } = getModel(this)
125
+ if (!supportSoftDelete) throw new Error('当前模型不支持软删除,不能执行恢复')
126
+ return model.updateMany({
127
+ ...(args as Prisma.Args<ExampleModel, 'updateMany'>),
128
+ data: { ...(data ?? {}), deleteTime: new Date() },
129
+ }) as Promise<Prisma.Result<T, A, 'updateMany'>>
130
+ },
131
+
132
+ // -----------------------------
133
+ // 查询
134
+ // -----------------------------
135
+
136
+ aggregate<T, A>(this: T, inputArgs: QueryInputArgs<T, A, 'aggregate'>) {
137
+ return query(this, inputArgs, 'aggregate')
138
+ },
139
+ count<T, A>(this: T, inputArgs: QueryInputArgs<T, A, 'count'>) {
140
+ return query(this, inputArgs, 'count')
141
+ },
142
+ findFirst<T, A>(this: T, inputArgs: QueryInputArgs<T, A, 'findFirst'>) {
143
+ return query(this, inputArgs, 'findFirst')
144
+ },
145
+ findFirstOrThrow<T, A>(this: T, inputArgs: QueryInputArgs<T, A, 'findFirstOrThrow'>) {
146
+ return query(this, inputArgs, 'findFirstOrThrow')
147
+ },
148
+ findMany<T, A>(this: T, inputArgs: QueryInputArgs<T, A, 'findMany'>) {
149
+ return query(this, inputArgs, 'findMany')
150
+ },
151
+ findUnique<T, A>(this: T, inputArgs: QueryInputArgs<T, A, 'findUnique'>) {
152
+ return query(this, inputArgs, 'findUnique')
153
+ },
154
+ findUniqueOrThrow<T, A>(this: T, inputArgs: QueryInputArgs<T, A, 'findUniqueOrThrow'>) {
155
+ return query(this, inputArgs, 'findUniqueOrThrow')
156
+ },
157
+ groupBy<T, A>(this: T, inputArgs: QueryInputArgs<T, A, 'groupBy'>) {
158
+ return query(this, inputArgs, 'groupBy')
159
+ },
160
+ },
161
+ },
162
+ })