create-pardx-scaffold 0.1.2 → 0.1.5

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 (27) hide show
  1. package/package.json +1 -1
  2. package/template/apps/api/libs/infra/clients/internal/ip-info/dto/ip-info.dto.ts +15 -0
  3. package/template/apps/api/libs/infra/clients/internal/ip-info/index.ts +3 -0
  4. package/template/apps/api/libs/infra/clients/internal/ip-info/ip-info.client.ts +57 -0
  5. package/template/apps/api/libs/infra/clients/internal/ip-info/ip-info.module.ts +17 -0
  6. package/template/apps/api/libs/infra/common/ts-rest/response.helper.ts +9 -1
  7. package/template/apps/api/libs/infra/shared-services/file-storage/README.md +4 -4
  8. package/template/apps/api/libs/infra/shared-services/file-storage/bucket-resolver.ts +4 -4
  9. package/template/apps/api/libs/infra/shared-services/file-storage/file-storage.module.ts +3 -3
  10. package/template/apps/api/libs/infra/shared-services/ip-geo/continent-mapping.ts +86 -0
  11. package/template/apps/api/libs/infra/shared-services/ip-geo/index.ts +37 -0
  12. package/template/apps/api/libs/infra/shared-services/ip-geo/ip-geo.module.ts +21 -0
  13. package/template/apps/api/libs/infra/shared-services/ip-geo/ip-geo.service.ts +135 -0
  14. package/template/apps/api/libs/infra/shared-services/uploader/index.ts +1 -1
  15. package/template/apps/api/libs/infra/shared-services/uploader/uploader.service.ts +14 -6
  16. package/template/apps/api/src/app.module.ts +3 -12
  17. package/template/apps/api/src/modules/uploader/uploader.controller.ts +290 -0
  18. package/template/apps/api/src/modules/uploader/uploader.module.ts +17 -0
  19. package/template/apps/web/components/index.ts +21 -0
  20. package/template/apps/web/components/skeletons.tsx +188 -0
  21. package/template/apps/web/components/suspense-utils.tsx +123 -0
  22. package/template/apps/web/lib/api/contracts/client.ts +4 -0
  23. package/template/apps/web/lib/deprecation-warning.ts +150 -0
  24. package/template/apps/web/lib/queries/optimistic-update.ts +204 -0
  25. package/template/packages/constants/src/index.ts +22 -0
  26. package/template/apps/api/src/modules/health/health.controller.ts +0 -13
  27. package/template/apps/api/src/modules/health/health.module.ts +0 -7
@@ -0,0 +1,150 @@
1
+ /**
2
+ * API 废弃警告处理
3
+ *
4
+ * 当后端返回废弃响应头时,显示警告通知:
5
+ * - Deprecation: true
6
+ * - X-Deprecation-Message: 废弃原因
7
+ * - Sunset: 下线日期
8
+ */
9
+
10
+ import { toast } from 'sonner';
11
+ import {
12
+ DEPRECATION_HEADER,
13
+ DEPRECATION_MESSAGE_HEADER,
14
+ SUNSET_HEADER,
15
+ } from '@repo/constants';
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ export interface DeprecationInfo {
22
+ /** 废弃消息 */
23
+ message: string;
24
+ /** 下线日期 (ISO 8601) */
25
+ sunsetDate: string | null;
26
+ }
27
+
28
+ // ============================================================================
29
+ // Constants
30
+ // ============================================================================
31
+
32
+ const DEPRECATION_SHOWN_KEY = 'pardx:deprecation_warnings_shown';
33
+ const TOAST_DURATION = 10000; // 10 秒
34
+
35
+ // ============================================================================
36
+ // Helper Functions
37
+ // ============================================================================
38
+
39
+ /**
40
+ * 获取已显示警告的 API 路径集合
41
+ */
42
+ function getShownWarnings(): Set<string> {
43
+ if (typeof sessionStorage === 'undefined') {
44
+ return new Set();
45
+ }
46
+ try {
47
+ const stored = sessionStorage.getItem(DEPRECATION_SHOWN_KEY);
48
+ return stored ? new Set(JSON.parse(stored)) : new Set();
49
+ } catch {
50
+ return new Set();
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 记录已显示警告的 API 路径
56
+ */
57
+ function markWarningShown(path: string): void {
58
+ if (typeof sessionStorage === 'undefined') {
59
+ return;
60
+ }
61
+ try {
62
+ const shown = getShownWarnings();
63
+ shown.add(path);
64
+ sessionStorage.setItem(DEPRECATION_SHOWN_KEY, JSON.stringify(Array.from(shown)));
65
+ } catch {
66
+ // Ignore storage errors
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 格式化下线日期
72
+ */
73
+ function formatSunsetDate(sunsetDate: string): string {
74
+ try {
75
+ const date = new Date(sunsetDate);
76
+ return date.toLocaleDateString('zh-CN', {
77
+ year: 'numeric',
78
+ month: 'long',
79
+ day: 'numeric',
80
+ });
81
+ } catch {
82
+ return sunsetDate;
83
+ }
84
+ }
85
+
86
+ // ============================================================================
87
+ // Public Functions
88
+ // ============================================================================
89
+
90
+ /**
91
+ * 检查响应是否包含废弃标识
92
+ */
93
+ export function isDeprecated(headers: Headers): boolean {
94
+ const deprecation = headers.get(DEPRECATION_HEADER);
95
+ return deprecation === 'true';
96
+ }
97
+
98
+ /**
99
+ * 从响应头中提取废弃信息
100
+ */
101
+ export function getDeprecationInfo(headers: Headers): DeprecationInfo | null {
102
+ if (!isDeprecated(headers)) {
103
+ return null;
104
+ }
105
+
106
+ const message =
107
+ headers.get(DEPRECATION_MESSAGE_HEADER) || '此 API 即将废弃,请尽快迁移';
108
+ const sunsetDate = headers.get(SUNSET_HEADER);
109
+
110
+ return { message, sunsetDate };
111
+ }
112
+
113
+ /**
114
+ * 检查并显示废弃警告
115
+ *
116
+ * @param headers 响应头
117
+ * @param path API 路径
118
+ */
119
+ export function checkDeprecationWarning(headers: Headers, path: string): void {
120
+ if (typeof window === 'undefined') {
121
+ return;
122
+ }
123
+
124
+ const info = getDeprecationInfo(headers);
125
+ if (!info) {
126
+ return;
127
+ }
128
+
129
+ // 检查是否已显示过该路径的警告
130
+ const shown = getShownWarnings();
131
+ if (shown.has(path)) {
132
+ return;
133
+ }
134
+
135
+ // 标记为已显示
136
+ markWarningShown(path);
137
+
138
+ // 构建警告消息
139
+ let description = info.message;
140
+ if (info.sunsetDate) {
141
+ description += `\n下线日期: ${formatSunsetDate(info.sunsetDate)}`;
142
+ }
143
+
144
+ // 显示警告 Toast
145
+ toast.warning('API 即将废弃', {
146
+ description,
147
+ duration: TOAST_DURATION,
148
+ dismissible: true,
149
+ });
150
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * 乐观更新工具
3
+ *
4
+ * 提供 React Query 乐观更新的工具函数和示例
5
+ */
6
+
7
+ import type { QueryClient } from '@tanstack/react-query';
8
+
9
+ /**
10
+ * 乐观更新配置
11
+ */
12
+ interface OptimisticUpdateConfig<TData, TVariables> {
13
+ /** Query Key */
14
+ queryKey: readonly unknown[];
15
+ /** 乐观更新函数 - 返回更新后的数据 */
16
+ updater: (oldData: TData | undefined, variables: TVariables) => TData;
17
+ /** 回滚函数(可选) */
18
+ onRollback?: (context: { previousData: TData | undefined }) => void;
19
+ }
20
+
21
+ /**
22
+ * 创建乐观更新的 mutation 配置
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * const updateTodo = useMutation({
27
+ * mutationFn: api.todo.update,
28
+ * ...createOptimisticUpdate(queryClient, {
29
+ * queryKey: ['todos'],
30
+ * updater: (oldData, variables) => {
31
+ * return oldData?.map(todo =>
32
+ * todo.id === variables.id ? { ...todo, ...variables } : todo
33
+ * );
34
+ * },
35
+ * }),
36
+ * });
37
+ * ```
38
+ */
39
+ export function createOptimisticUpdate<TData, TVariables, TError = unknown>(
40
+ queryClient: QueryClient,
41
+ config: OptimisticUpdateConfig<TData, TVariables>,
42
+ ) {
43
+ return {
44
+ onMutate: async (variables: TVariables) => {
45
+ // 取消正在进行的查询
46
+ await queryClient.cancelQueries({ queryKey: config.queryKey });
47
+
48
+ // 保存之前的数据
49
+ const previousData = queryClient.getQueryData<TData>(config.queryKey);
50
+
51
+ // 乐观更新
52
+ queryClient.setQueryData<TData>(config.queryKey, (oldData) =>
53
+ config.updater(oldData, variables),
54
+ );
55
+
56
+ // 返回上下文用于回滚
57
+ return { previousData };
58
+ },
59
+
60
+ onError: (
61
+ _error: TError,
62
+ _variables: TVariables,
63
+ context: { previousData: TData | undefined } | undefined,
64
+ ) => {
65
+ // 回滚到之前的数据
66
+ if (context?.previousData !== undefined) {
67
+ queryClient.setQueryData(config.queryKey, context.previousData);
68
+ }
69
+ config.onRollback?.(context || { previousData: undefined });
70
+ },
71
+
72
+ onSettled: () => {
73
+ // 重新获取数据以确保同步
74
+ queryClient.invalidateQueries({ queryKey: config.queryKey });
75
+ },
76
+ };
77
+ }
78
+
79
+ /**
80
+ * 列表项乐观更新 - 更新单个项
81
+ */
82
+ export function createListItemOptimisticUpdate<
83
+ TItem extends { id: string | number },
84
+ TVariables extends { id: string | number },
85
+ >(
86
+ queryClient: QueryClient,
87
+ queryKey: readonly unknown[],
88
+ options?: {
89
+ onRollback?: () => void;
90
+ },
91
+ ) {
92
+ return createOptimisticUpdate<TItem[], TVariables>(queryClient, {
93
+ queryKey,
94
+ updater: (oldData, variables) => {
95
+ if (!oldData) return [];
96
+ return oldData.map((item) =>
97
+ item.id === variables.id ? { ...item, ...variables } : item,
98
+ );
99
+ },
100
+ onRollback: options?.onRollback,
101
+ });
102
+ }
103
+
104
+ /**
105
+ * 列表项乐观删除
106
+ */
107
+ export function createListItemOptimisticDelete<
108
+ TItem extends { id: string | number },
109
+ >(
110
+ queryClient: QueryClient,
111
+ queryKey: readonly unknown[],
112
+ options?: {
113
+ onRollback?: () => void;
114
+ },
115
+ ) {
116
+ return createOptimisticUpdate<TItem[], { id: string | number }>(queryClient, {
117
+ queryKey,
118
+ updater: (oldData, variables) => {
119
+ if (!oldData) return [];
120
+ return oldData.filter((item) => item.id !== variables.id);
121
+ },
122
+ onRollback: options?.onRollback,
123
+ });
124
+ }
125
+
126
+ /**
127
+ * 列表项乐观添加
128
+ */
129
+ export function createListItemOptimisticAdd<TItem>(
130
+ queryClient: QueryClient,
131
+ queryKey: readonly unknown[],
132
+ options?: {
133
+ prepend?: boolean;
134
+ onRollback?: () => void;
135
+ },
136
+ ) {
137
+ return createOptimisticUpdate<TItem[], TItem>(queryClient, {
138
+ queryKey,
139
+ updater: (oldData, newItem) => {
140
+ if (!oldData) return [newItem];
141
+ return options?.prepend
142
+ ? [newItem, ...oldData]
143
+ : [...oldData, newItem];
144
+ },
145
+ onRollback: options?.onRollback,
146
+ });
147
+ }
148
+
149
+ /**
150
+ * 使用示例
151
+ *
152
+ * @example
153
+ * ```tsx
154
+ * // 在组件中使用乐观更新
155
+ * function TodoList() {
156
+ * const queryClient = useQueryClient();
157
+ *
158
+ * // 更新 Todo
159
+ * const updateTodo = useMutation({
160
+ * mutationFn: (data: { id: string; completed: boolean }) =>
161
+ * api.todo.update(data),
162
+ * ...createListItemOptimisticUpdate<Todo, { id: string; completed: boolean }>(
163
+ * queryClient,
164
+ * ['todos'],
165
+ * ),
166
+ * });
167
+ *
168
+ * // 删除 Todo
169
+ * const deleteTodo = useMutation({
170
+ * mutationFn: (id: string) => api.todo.delete(id),
171
+ * ...createListItemOptimisticDelete<Todo>(queryClient, ['todos']),
172
+ * });
173
+ *
174
+ * // 添加 Todo
175
+ * const addTodo = useMutation({
176
+ * mutationFn: (data: Omit<Todo, 'id'>) => api.todo.create(data),
177
+ * ...createListItemOptimisticAdd<Todo>(queryClient, ['todos'], {
178
+ * prepend: true,
179
+ * }),
180
+ * });
181
+ *
182
+ * return (
183
+ * <ul>
184
+ * {todos.map(todo => (
185
+ * <li key={todo.id}>
186
+ * <input
187
+ * type="checkbox"
188
+ * checked={todo.completed}
189
+ * onChange={() => updateTodo.mutate({
190
+ * id: todo.id,
191
+ * completed: !todo.completed,
192
+ * })}
193
+ * />
194
+ * {todo.title}
195
+ * <button onClick={() => deleteTodo.mutate({ id: todo.id })}>
196
+ * 删除
197
+ * </button>
198
+ * </li>
199
+ * ))}
200
+ * </ul>
201
+ * );
202
+ * }
203
+ * ```
204
+ */
@@ -54,6 +54,28 @@ export const DATE_FORMAT = {
54
54
  ISO: 'YYYY-MM-DDTHH:mm:ss.SSSZ',
55
55
  } as const;
56
56
 
57
+ // ============================================================================
58
+ // Deprecation Headers (废弃警告响应头)
59
+ // ============================================================================
60
+
61
+ /**
62
+ * 废弃标识 Header (响应)
63
+ * 值为 "true" 表示该 API 已废弃
64
+ */
65
+ export const DEPRECATION_HEADER = 'deprecation' as const;
66
+
67
+ /**
68
+ * 废弃消息 Header (响应)
69
+ * 包含废弃原因和迁移建议
70
+ */
71
+ export const DEPRECATION_MESSAGE_HEADER = 'x-deprecation-message' as const;
72
+
73
+ /**
74
+ * 下线日期 Header (响应)
75
+ * 格式: ISO 8601 日期字符串
76
+ */
77
+ export const SUNSET_HEADER = 'sunset' as const;
78
+
57
79
  // ============================================================================
58
80
  // API Versioning Constants
59
81
  // ============================================================================
@@ -1,13 +0,0 @@
1
- import { Controller, Get } from '@nestjs/common';
2
-
3
- @Controller('health')
4
- export class HealthController {
5
- @Get()
6
- check() {
7
- return {
8
- status: 'ok',
9
- timestamp: new Date().toISOString(),
10
- uptime: process.uptime(),
11
- };
12
- }
13
- }
@@ -1,7 +0,0 @@
1
- import { Module } from '@nestjs/common';
2
- import { HealthController } from './health.controller';
3
-
4
- @Module({
5
- controllers: [HealthController],
6
- })
7
- export class HealthModule {}