@fui-org/fui-cli 0.1.1 → 0.3.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.
- package/README.md +18 -3
- package/dist/fui.js +160 -43
- package/package.json +4 -2
- package/skills/fui/SKILL.md +131 -66
- package/skills/fui-skill/README.md +112 -0
- package/skills/fui-skill/SKILL.md +269 -0
- package/skills/fui-skill/assets/projectdefaultstyle.css +518 -0
- package/skills/fui-skill/design-md/airbnb/DESIGN.md +545 -0
- package/skills/fui-skill/design-md/airbnb/README.md +5 -0
- package/skills/fui-skill/design-md/airtable/DESIGN.md +554 -0
- package/skills/fui-skill/design-md/airtable/README.md +5 -0
- package/skills/fui-skill/design-md/apple/DESIGN.md +562 -0
- package/skills/fui-skill/design-md/apple/README.md +5 -0
- package/skills/fui-skill/design-md/asu/DESIGN.md +179 -0
- package/skills/fui-skill/design-md/asu/README.md +107 -0
- package/skills/fui-skill/design-md/binance/DESIGN.md +634 -0
- package/skills/fui-skill/design-md/binance/README.md +5 -0
- package/skills/fui-skill/design-md/bmw/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/bmw/README.md +5 -0
- package/skills/fui-skill/design-md/bmw-m/DESIGN.md +503 -0
- package/skills/fui-skill/design-md/bmw-m/README.md +5 -0
- package/skills/fui-skill/design-md/bugatti/DESIGN.md +454 -0
- package/skills/fui-skill/design-md/bugatti/README.md +5 -0
- package/skills/fui-skill/design-md/cal/DESIGN.md +542 -0
- package/skills/fui-skill/design-md/cal/README.md +5 -0
- package/skills/fui-skill/design-md/claude/DESIGN.md +589 -0
- package/skills/fui-skill/design-md/claude/README.md +5 -0
- package/skills/fui-skill/design-md/clay/DESIGN.md +541 -0
- package/skills/fui-skill/design-md/clay/README.md +5 -0
- package/skills/fui-skill/design-md/clickhouse/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/clickhouse/README.md +5 -0
- package/skills/fui-skill/design-md/cohere/DESIGN.md +451 -0
- package/skills/fui-skill/design-md/cohere/README.md +5 -0
- package/skills/fui-skill/design-md/coinbase/DESIGN.md +570 -0
- package/skills/fui-skill/design-md/coinbase/README.md +5 -0
- package/skills/fui-skill/design-md/composio/DESIGN.md +506 -0
- package/skills/fui-skill/design-md/composio/README.md +5 -0
- package/skills/fui-skill/design-md/cursor/DESIGN.md +537 -0
- package/skills/fui-skill/design-md/cursor/README.md +5 -0
- package/skills/fui-skill/design-md/elevenlabs/DESIGN.md +504 -0
- package/skills/fui-skill/design-md/elevenlabs/README.md +5 -0
- package/skills/fui-skill/design-md/expo/DESIGN.md +526 -0
- package/skills/fui-skill/design-md/expo/README.md +5 -0
- package/skills/fui-skill/design-md/ferrari/DESIGN.md +531 -0
- package/skills/fui-skill/design-md/ferrari/README.md +5 -0
- package/skills/fui-skill/design-md/figma/DESIGN.md +578 -0
- package/skills/fui-skill/design-md/figma/README.md +5 -0
- package/skills/fui-skill/design-md/framer/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/framer/README.md +5 -0
- package/skills/fui-skill/design-md/fui/DESIGN.md +532 -0
- package/skills/fui-skill/design-md/hashicorp/DESIGN.md +575 -0
- package/skills/fui-skill/design-md/hashicorp/README.md +5 -0
- package/skills/fui-skill/design-md/ibm/DESIGN.md +550 -0
- package/skills/fui-skill/design-md/ibm/README.md +5 -0
- package/skills/fui-skill/design-md/intercom/DESIGN.md +546 -0
- package/skills/fui-skill/design-md/intercom/README.md +5 -0
- package/skills/fui-skill/design-md/kraken/DESIGN.md +125 -0
- package/skills/fui-skill/design-md/kraken/README.md +5 -0
- package/skills/fui-skill/design-md/lamborghini/DESIGN.md +288 -0
- package/skills/fui-skill/design-md/lamborghini/README.md +5 -0
- package/skills/fui-skill/design-md/linear.app/DESIGN.md +548 -0
- package/skills/fui-skill/design-md/linear.app/README.md +5 -0
- package/skills/fui-skill/design-md/lovable/DESIGN.md +298 -0
- package/skills/fui-skill/design-md/lovable/README.md +5 -0
- package/skills/fui-skill/design-md/mastercard/DESIGN.md +365 -0
- package/skills/fui-skill/design-md/mastercard/README.md +5 -0
- package/skills/fui-skill/design-md/meta/DESIGN.md +683 -0
- package/skills/fui-skill/design-md/meta/README.md +5 -0
- package/skills/fui-skill/design-md/minimax/DESIGN.md +746 -0
- package/skills/fui-skill/design-md/minimax/README.md +5 -0
- package/skills/fui-skill/design-md/mintlify/DESIGN.md +852 -0
- package/skills/fui-skill/design-md/mintlify/README.md +5 -0
- package/skills/fui-skill/design-md/miro/DESIGN.md +825 -0
- package/skills/fui-skill/design-md/miro/README.md +5 -0
- package/skills/fui-skill/design-md/mistral.ai/DESIGN.md +773 -0
- package/skills/fui-skill/design-md/mistral.ai/README.md +5 -0
- package/skills/fui-skill/design-md/mongodb/DESIGN.md +767 -0
- package/skills/fui-skill/design-md/mongodb/README.md +5 -0
- package/skills/fui-skill/design-md/nike/DESIGN.md +575 -0
- package/skills/fui-skill/design-md/nike/README.md +5 -0
- package/skills/fui-skill/design-md/notion/DESIGN.md +821 -0
- package/skills/fui-skill/design-md/notion/README.md +5 -0
- package/skills/fui-skill/design-md/nvidia/DESIGN.md +640 -0
- package/skills/fui-skill/design-md/nvidia/README.md +5 -0
- package/skills/fui-skill/design-md/ollama/DESIGN.md +539 -0
- package/skills/fui-skill/design-md/ollama/README.md +5 -0
- package/skills/fui-skill/design-md/opencode.ai/DESIGN.md +521 -0
- package/skills/fui-skill/design-md/opencode.ai/README.md +5 -0
- package/skills/fui-skill/design-md/pinterest/DESIGN.md +597 -0
- package/skills/fui-skill/design-md/pinterest/README.md +5 -0
- package/skills/fui-skill/design-md/playstation/DESIGN.md +661 -0
- package/skills/fui-skill/design-md/playstation/README.md +5 -0
- package/skills/fui-skill/design-md/posthog/DESIGN.md +690 -0
- package/skills/fui-skill/design-md/posthog/README.md +5 -0
- package/skills/fui-skill/design-md/raycast/DESIGN.md +669 -0
- package/skills/fui-skill/design-md/raycast/README.md +5 -0
- package/skills/fui-skill/design-md/renault/DESIGN.md +589 -0
- package/skills/fui-skill/design-md/renault/README.md +5 -0
- package/skills/fui-skill/design-md/replicate/DESIGN.md +616 -0
- package/skills/fui-skill/design-md/replicate/README.md +5 -0
- package/skills/fui-skill/design-md/resend/DESIGN.md +585 -0
- package/skills/fui-skill/design-md/resend/README.md +5 -0
- package/skills/fui-skill/design-md/revolut/DESIGN.md +636 -0
- package/skills/fui-skill/design-md/revolut/README.md +5 -0
- package/skills/fui-skill/design-md/runwayml/DESIGN.md +244 -0
- package/skills/fui-skill/design-md/runwayml/README.md +5 -0
- package/skills/fui-skill/design-md/sanity/DESIGN.md +357 -0
- package/skills/fui-skill/design-md/sanity/README.md +5 -0
- package/skills/fui-skill/design-md/sentry/DESIGN.md +551 -0
- package/skills/fui-skill/design-md/sentry/README.md +5 -0
- package/skills/fui-skill/design-md/shopify/DESIGN.md +516 -0
- package/skills/fui-skill/design-md/shopify/README.md +5 -0
- package/skills/fui-skill/design-md/slack/DESIGN.md +482 -0
- package/skills/fui-skill/design-md/spacex/DESIGN.md +363 -0
- package/skills/fui-skill/design-md/spacex/README.md +5 -0
- package/skills/fui-skill/design-md/spotify/DESIGN.md +246 -0
- package/skills/fui-skill/design-md/spotify/README.md +5 -0
- package/skills/fui-skill/design-md/starbucks/DESIGN.md +580 -0
- package/skills/fui-skill/design-md/starbucks/README.md +5 -0
- package/skills/fui-skill/design-md/stripe/DESIGN.md +487 -0
- package/skills/fui-skill/design-md/stripe/README.md +5 -0
- package/skills/fui-skill/design-md/supabase/DESIGN.md +462 -0
- package/skills/fui-skill/design-md/supabase/README.md +5 -0
- package/skills/fui-skill/design-md/superhuman/DESIGN.md +448 -0
- package/skills/fui-skill/design-md/superhuman/README.md +5 -0
- package/skills/fui-skill/design-md/tesla/DESIGN.md +286 -0
- package/skills/fui-skill/design-md/tesla/README.md +5 -0
- package/skills/fui-skill/design-md/theverge/DESIGN.md +339 -0
- package/skills/fui-skill/design-md/theverge/README.md +5 -0
- package/skills/fui-skill/design-md/together.ai/DESIGN.md +633 -0
- package/skills/fui-skill/design-md/together.ai/README.md +5 -0
- package/skills/fui-skill/design-md/uber/DESIGN.md +636 -0
- package/skills/fui-skill/design-md/uber/README.md +5 -0
- package/skills/fui-skill/design-md/vercel/DESIGN.md +736 -0
- package/skills/fui-skill/design-md/vercel/README.md +5 -0
- package/skills/fui-skill/design-md/vodafone/DESIGN.md +538 -0
- package/skills/fui-skill/design-md/vodafone/README.md +5 -0
- package/skills/fui-skill/design-md/voltagent/DESIGN.md +521 -0
- package/skills/fui-skill/design-md/voltagent/README.md +5 -0
- package/skills/fui-skill/design-md/warp/DESIGN.md +526 -0
- package/skills/fui-skill/design-md/warp/README.md +5 -0
- package/skills/fui-skill/design-md/webflow/DESIGN.md +588 -0
- package/skills/fui-skill/design-md/webflow/README.md +5 -0
- package/skills/fui-skill/design-md/wired/DESIGN.md +497 -0
- package/skills/fui-skill/design-md/wired/README.md +5 -0
- package/skills/fui-skill/design-md/wise/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/wise/README.md +5 -0
- package/skills/fui-skill/design-md/x.ai/DESIGN.md +465 -0
- package/skills/fui-skill/design-md/x.ai/README.md +5 -0
- package/skills/fui-skill/design-md/zapier/DESIGN.md +537 -0
- package/skills/fui-skill/design-md/zapier/README.md +5 -0
- package/skills/fui-skill/examples/component.vue +162 -0
- package/skills/fui-skill/examples/f-table-patterns.json +331 -0
- package/skills/fui-skill/examples/module-patterns.json +973 -0
- package/skills/fui-skill/examples/project-patterns.json +222 -0
- package/skills/fui-skill/metadata.json +75 -0
- package/skills/fui-skill/references/INDEX.md +144 -0
- package/skills/fui-skill/references/advanced-techniques.md +160 -0
- package/skills/fui-skill/references/coding-standards.md +112 -0
- package/skills/fui-skill/references/component-design.md +455 -0
- package/skills/fui-skill/references/component-quickref.md +77 -0
- package/skills/fui-skill/references/component-table.md +276 -0
- package/skills/fui-skill/references/components-dialog.md +192 -0
- package/skills/fui-skill/references/components-display.md +147 -0
- package/skills/fui-skill/references/components-echart.md +391 -0
- package/skills/fui-skill/references/components-input.md +359 -0
- package/skills/fui-skill/references/controls-patterns.md +847 -0
- package/skills/fui-skill/references/controls-styling-vocabulary.md +140 -0
- package/skills/fui-skill/references/db-table-design.md +77 -0
- package/skills/fui-skill/references/db-workflow.md +504 -0
- package/skills/fui-skill/references/default-function.md +415 -0
- package/skills/fui-skill/references/design-modes.md +85 -0
- package/skills/fui-skill/references/echart-templates.md +481 -0
- package/skills/fui-skill/references/fastproject.md +97 -0
- package/skills/fui-skill/references/fsheet.md +218 -0
- package/skills/fui-skill/references/fullstack-workflow.md +351 -0
- package/skills/fui-skill/references/module-data-patterns.md +126 -0
- package/skills/fui-skill/references/module-json-anatomy.md +137 -0
- package/skills/fui-skill/references/module-structure.md +260 -0
- package/skills/fui-skill/references/new-session.md +108 -0
- package/skills/fui-skill/references/pdfmake.md +60 -0
- package/skills/fui-skill/references/permission-system.md +169 -0
- package/skills/fui-skill/references/platform-architecture.md +294 -0
- package/skills/fui-skill/references/project-config.md +335 -0
- package/skills/fui-skill/references/project-provisioning.md +383 -0
- package/skills/fui-skill/references/script-map.md +296 -0
- package/skills/fui-skill/references/sql-clr-functions.md +224 -0
- package/skills/fui-skill/references/system-design.md +116 -0
- package/skills/fui-skill/references/tapi-file-api.md +191 -0
- package/skills/fui-skill/references/tapi-permission-patterns.md +158 -0
- package/skills/fui-skill/references/tapi-reference.md +549 -0
- package/skills/fui-skill/references/tools-registry.md +460 -0
- package/skills/fui-skill/references/ui-crosswindow-patterns.md +317 -0
- package/skills/fui-skill/references/ui-dialog-patterns.md +229 -0
- package/skills/fui-skill/references/ui-layout-patterns.md +176 -0
- package/skills/fui-skill/references/ui-patterns.md +315 -0
- package/skills/fui-skill/references/ui-screenshot-review.md +94 -0
- package/skills/fui-skill/references/ui-table-cell-patterns.md +316 -0
- package/skills/fui-skill/references/ui-templates.md +29 -0
- package/skills/fui-skill/references/verification.md +246 -0
- package/skills/fui-skill/references/watcher-patterns.md +196 -0
- package/skills/fui-skill/references/websocket-realtime.md +254 -0
- package/skills/fui-skill/scripts/component-3.0.js +2549 -0
- package/skills/fui-skill/scripts/component.js +3142 -0
- package/skills/fui-skill/scripts/componentTable-3.0.js +909 -0
- package/skills/fui-skill/scripts/componentTable.js +769 -0
- package/skills/fui-skill/scripts/defaultfunction-3.0.js +781 -0
- package/skills/fui-skill/scripts/defaultfunction.js +966 -0
- package/skills/fui-skill/scripts/fastproject-3.0.js +870 -0
- package/skills/fui-skill/scripts/fastproject.js +828 -0
- package/skills/fui-skill/scripts/fechart.js +890 -0
- package/skills/fui-skill/scripts/fsheet.js +1330 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# tAPI Reference — Transparent Query API (Core)
|
|
2
|
+
|
|
3
|
+
> File này sở hữu: **quy ước tAPI: đặt tên SP (`spAPI_*`/`spAPI_AUTH_*`/`spAPIFILE_*`), params, URL, response contract, route `/help`**. Quy trình deploy xem [db-workflow.md](db-workflow.md).
|
|
4
|
+
|
|
5
|
+
tAPI tự sinh API từ SQL Stored Procedures. Tên SP quyết định endpoint URL, kiểu xác thực, và quyền truy cập. Không cần viết controller hay route — chỉ cần đặt tên SP đúng quy tắc.
|
|
6
|
+
|
|
7
|
+
tAPI hỗ trợ cả **GET và POST** — không cần chỉ định HTTP method khi viết SP. FUI module.json tự quyết định method dựa trên cách gọi.
|
|
8
|
+
|
|
9
|
+
File này là **core**: naming, params, URL, response format, error handling, SP design rules, checklist. Hai chủ đề chuyên sâu đã tách ra — load thêm khi cần:
|
|
10
|
+
|
|
11
|
+
| Chủ đề | File |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Kiểm tra quyền trong thân SP (5 pattern, `@sys_SystemRight`, `getSystemRight`) | [tapi-permission-patterns.md](tapi-permission-patterns.md) |
|
|
14
|
+
| File API — GET/UPLOAD, `spAPIFILE_`, `fileContent`, `tblFileData` | [tapi-file-api.md](tapi-file-api.md) |
|
|
15
|
+
|
|
16
|
+
> Nếu SP cần xử lý ảnh, gọi HTTP ra ngoài, gửi email, đọc/ghi file trên server, hoặc regex — kiểm tra xem DB đã có sẵn **SQL CLR function** (`httpCall`, `sendMail`, `ImageResize`, `FileWriter`, `RegexMatch`...) chưa trước khi viết logic thủ công. Xem [sql-clr-functions.md](sql-clr-functions.md).
|
|
17
|
+
|
|
18
|
+
## 1. Quy tắc đặt tên SP
|
|
19
|
+
|
|
20
|
+
### Cấu trúc tên
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
spAPI_[AUTH_]FunctionName
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Phần | Bắt buộc | Ý nghĩa |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `spAPI_` | ✅ | Đánh dấu SP được phép gọi qua API. Thiếu prefix này → API không gọi được |
|
|
29
|
+
| `AUTH_` | ❌ | Không yêu cầu xác thực. Bỏ qua → bắt buộc phải có token hợp lệ |
|
|
30
|
+
| `FunctionName` | ✅ | Tên hàm API, dùng trong URL. Ví dụ: `MessageHome`, `UserSetting` |
|
|
31
|
+
|
|
32
|
+
**Ví dụ tên SP:**
|
|
33
|
+
|
|
34
|
+
```sql
|
|
35
|
+
spAPI_MessageHome -- có auth
|
|
36
|
+
spAPI_AUTH_SelectChat -- không cần auth
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### File API
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
spAPIFILE_Document -- SP để GET file
|
|
43
|
+
spAPIFILE_UPLOAD_Document -- SP để nhận file upload
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> Chi tiết File API: xem [tapi-file-api.md](tapi-file-api.md).
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 2. Tham số SP
|
|
51
|
+
|
|
52
|
+
Ba loại tham số:
|
|
53
|
+
|
|
54
|
+
### `@url1_`, `@url2_`, `@url3_`... — Route params
|
|
55
|
+
|
|
56
|
+
Giá trị lấy trực tiếp từ URL theo thứ tự:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
https://api.domain.vn/app/FunctionName/value1/value2
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```sql
|
|
63
|
+
@url1_GroupID varchar(50), -- nhận "value1"
|
|
64
|
+
@url2_Mode varchar(50), -- nhận "value2"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `@sys_` — System params (tAPI tự inject, không cần truyền từ client)
|
|
68
|
+
|
|
69
|
+
| Tham số | Kiểu | Mô tả |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `@sys_UserID` | varchar(9) | Mã người dùng đã xác thực |
|
|
72
|
+
| `@sys_UserName` | varchar(50) | Tên đăng nhập |
|
|
73
|
+
| `@sys_GroupID` | int | Nhóm của user |
|
|
74
|
+
| `@sys_DepartmentID` | varchar(9) | Mã đơn vị |
|
|
75
|
+
| `@sys_SystemRight` | int | Quyền phân tầng (1=thấp, 9=cao) |
|
|
76
|
+
| `@sys_FunctionRight` | varchar(2000) | Quyền chức năng dạng `[AD][RPT][MGR]` |
|
|
77
|
+
| `@sys_SessionID` | varchar(50) | Mã phiên xác thực |
|
|
78
|
+
| `@sys_RawData` | nvarchar(max) | Toàn bộ JSON body từ client |
|
|
79
|
+
| `@sys_header_{TênHeader}` | varchar(n) | **Một HỌ tham số, không phải một tên cố định** — đọc bất kỳ HTTP header nào; `NULL` khi client không gửi header đó. Quy tắc đặt tên ở mục ngay dưới |
|
|
80
|
+
|
|
81
|
+
> **CRITICAL — Không truyền `@sys_` vào `IN` của module.json:**
|
|
82
|
+
> tAPI backend tự gán toàn bộ tham số `@sys_*` từ session đã xác thực. Nếu client truyền vào `IN`, giá trị sẽ bị **bỏ qua hoặc ghi đè** bởi server — truyền vào là thừa và có thể gây nhầm lẫn.
|
|
83
|
+
>
|
|
84
|
+
> ```json
|
|
85
|
+
> // ❌ SAI — không đưa sys_ vào IN
|
|
86
|
+
> "IN": { "ModuleID": "sModuleID", "sys_UserID": "vueData.user.UserID" }
|
|
87
|
+
>
|
|
88
|
+
> // ✅ ĐÚNG — chỉ truyền custom params
|
|
89
|
+
> "IN": { "ModuleID": "sModuleID" }
|
|
90
|
+
> ```
|
|
91
|
+
>
|
|
92
|
+
> `@sys_UserID`, `@sys_SystemRight`, `@sys_FunctionRight`... đều được tAPI tự điền — SP nhận đúng giá trị mà không cần client gửi.
|
|
93
|
+
|
|
94
|
+
#### `@sys_header_*` — đọc HTTP header: thay mỗi dấu `-` bằng `_`
|
|
95
|
+
|
|
96
|
+
Muốn đọc một HTTP header thì khai một tham số `@sys_header_{TênHeader}` — **bất kỳ header nào**, không
|
|
97
|
+
giới hạn trong một danh sách cho sẵn. Nhưng tên header HTTP chứa dấu `-` (`User-Agent`,
|
|
98
|
+
`Sec-Ch-Ua-Mobile`), mà **`-` không hợp lệ trong tên biến/tham số của SQL Server**. Viết thẳng tên
|
|
99
|
+
header vào là script không biên dịch được:
|
|
100
|
+
|
|
101
|
+
```sql
|
|
102
|
+
-- ❌ SAI — lỗi cú pháp ngay lúc CREATE PROCEDURE, không phải lỗi runtime
|
|
103
|
+
@sys_header_User-Agent varchar(500)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Không có cách bọc nào cứu được.** T-SQL không cho phép tên tham số dạng delimited identifier: cả
|
|
107
|
+
`@[sys_header_User-Agent]` lẫn `@"sys_header_User-Agent"` đều không hợp lệ. Đừng mất thời gian thử —
|
|
108
|
+
chuyển tên là con đường **duy nhất**.
|
|
109
|
+
|
|
110
|
+
**Quy tắc chuyển: mỗi dấu `-` thành một dấu `_`.** Không bỏ bớt ký tự, không đổi hoa-thường, không ghép
|
|
111
|
+
các đoạn lại với nhau — đây là phép thay thế 1-đổi-1, nên nhìn tên tham số là đọc ngược ra tên header
|
|
112
|
+
ngay, không phải đoán ranh giới giữa các đoạn.
|
|
113
|
+
|
|
114
|
+
| HTTP header | Tham số trong SP |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `User-Agent` | `@sys_header_User_Agent` |
|
|
117
|
+
| `Sec-Ch-Ua-Mobile` | `@sys_header_Sec_Ch_Ua_Mobile` |
|
|
118
|
+
| `X-Forwarded-For` | `@sys_header_X_Forwarded_For` |
|
|
119
|
+
| `Accept-Language` | `@sys_header_Accept_Language` |
|
|
120
|
+
| `Cookie` | `@sys_header_Cookie` — không có `-` thì giữ nguyên |
|
|
121
|
+
|
|
122
|
+
Chiều ngược lại (đọc SP có sẵn để biết nó lấy header nào): bỏ tiền tố `@sys_header_`, rồi đổi mọi `_`
|
|
123
|
+
còn lại thành `-` — `@sys_header_Accept_Language` → `Accept-Language`.
|
|
124
|
+
|
|
125
|
+
**Hoa-thường không quan trọng** — tAPI so khớp không phân biệt chữ hoa chữ thường, ở cả tiền tố lẫn tên
|
|
126
|
+
header: `@sys_header_User_Agent`, `@sys_Header_User_Agent`, `@sys_header_USER_AGENT` đều tra đúng một
|
|
127
|
+
header. (Đó là lý do `@sys_Header_RequestID` viết hoa chữ `H` trong bảng trên vẫn chạy.) Dù vậy **hãy
|
|
128
|
+
viết đúng hoa-thường như tên header chuẩn** (`User-Agent`, không `USER_AGENT`): người đọc SP nhận ra
|
|
129
|
+
ngay header nào, khỏi phải dịch. Thứ **bắt buộc** đúng vẫn chỉ là dấu `_` thay cho `-`.
|
|
130
|
+
|
|
131
|
+
```sql
|
|
132
|
+
CREATE PROCEDURE spAPI_AuditLog_Insert
|
|
133
|
+
@Action nvarchar(100),
|
|
134
|
+
@sys_UserID varchar(9),
|
|
135
|
+
@sys_header_User_Agent nvarchar(500),
|
|
136
|
+
@sys_header_X_Forwarded_For varchar(200)
|
|
137
|
+
AS
|
|
138
|
+
BEGIN
|
|
139
|
+
-- ISNULL vì client không gửi header thì tham số là NULL, không phải chuỗi rỗng
|
|
140
|
+
INSERT INTO tblAuditLog (Action, UserID, UserAgent, ClientIP, CreateTime)
|
|
141
|
+
VALUES (@Action, @sys_UserID,
|
|
142
|
+
ISNULL(@sys_header_User_Agent, N'(không có)'),
|
|
143
|
+
ISNULL(@sys_header_X_Forwarded_For, ''),
|
|
144
|
+
GETDATE());
|
|
145
|
+
END
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Ba điều đi kèm, đừng bỏ qua:
|
|
149
|
+
|
|
150
|
+
- **Vẫn là `@sys_*`, nên vẫn theo mọi luật của họ đó:** tAPI tự điền, **không** đưa vào `IN` của
|
|
151
|
+
module.json, và **không được gán default value** (`= null` là lỗi cứng — `db_sp_deploy` chặn, xem mục
|
|
152
|
+
dưới).
|
|
153
|
+
- **Client không gửi header thì tham số là `NULL`** (không phải chuỗi rỗng) — và header vắng mặt là
|
|
154
|
+
chuyện bình thường, không phải ca hiếm: lời gọi từ script/server-to-server thường không có
|
|
155
|
+
`User-Agent`. `NULL` kéo theo hai hệ quả im lặng của T-SQL, bọc bằng `ISNULL(...)` ngay tại chỗ dùng:
|
|
156
|
+
- **So sánh ra UNKNOWN, không ra TRUE/FALSE.** `WHERE @sys_header_User_Agent <> 'bot'` **loại luôn**
|
|
157
|
+
dòng khi header vắng — ngược hẳn điều người viết mong đợi.
|
|
158
|
+
- **Nối chuỗi bằng `+` biến cả chuỗi thành `NULL`.** Một header vắng đủ để xoá trắng nguyên câu log.
|
|
159
|
+
- **Dữ liệu header do client tự khai, KHÔNG tin được.** `User-Agent`, `X-Forwarded-For` giả được hết —
|
|
160
|
+
dùng để ghi log/thống kê thì hợp lý, lấy làm căn cứ phân quyền thì không. Quyền lấy từ
|
|
161
|
+
`@sys_UserID`/`@sys_SystemRight` (tAPI dựng từ token đã xác thực), không lấy từ header.
|
|
162
|
+
|
|
163
|
+
Không chắc tAPI có nhận đúng tên mình vừa đặt không → `db_sp_help({ name })` in ra danh sách tham số
|
|
164
|
+
đầu vào mà tAPI thực sự thấy (xem §3.1).
|
|
165
|
+
|
|
166
|
+
### Custom params — Tham số nghiệp vụ do lập trình viên định nghĩa
|
|
167
|
+
|
|
168
|
+
Tên tham số = tên field trong JSON body gửi lên:
|
|
169
|
+
|
|
170
|
+
```sql
|
|
171
|
+
@StudentID varchar(9),
|
|
172
|
+
@SemesterID int,
|
|
173
|
+
@Keyword nvarchar(200)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Ghép tham số là KHOAN DUNG — thiếu hay thừa đều không lỗi
|
|
177
|
+
|
|
178
|
+
Đây là hành vi nền tảng, quyết định cách đọc mọi mục còn lại của §2:
|
|
179
|
+
|
|
180
|
+
| Tình huống | tAPI làm gì |
|
|
181
|
+
|---|---|
|
|
182
|
+
| Body **thiếu** một khoá mà SP có tham số | truyền **`NULL`** cho tham số đó |
|
|
183
|
+
| Body **thừa** một khoá mà SP không có tham số | **bỏ qua im lặng** |
|
|
184
|
+
|
|
185
|
+
**Không có ca nào báo lỗi.** Gõ sai tên khoá trong `IN` của module.json (`StudentId` thay vì
|
|
186
|
+
`StudentID`) không làm request hỏng — SP chỉ nhận `NULL` và trả về kết quả rỗng hoặc sai, còn HTTP vẫn
|
|
187
|
+
200. Đây là lớp lỗi hỏng-âm-thầm hay gặp nhất khi nối API.
|
|
188
|
+
|
|
189
|
+
Hai hệ quả phải nhớ:
|
|
190
|
+
|
|
191
|
+
- **Đổi/xoá tham số của SP là thay đổi contract KHÔNG phát tín hiệu.** Module cũ vẫn gửi khoá cũ, vẫn
|
|
192
|
+
chạy, chỉ là giá trị rơi vào hư không. Đừng trông chờ một lỗi để biết mình quên sửa nơi gọi — tự
|
|
193
|
+
soát, hoặc đối chiếu bằng `db_sp_help`.
|
|
194
|
+
- **`NULL` lan theo luật T-SQL**: `WHERE @x <> 'y'` thành UNKNOWN nên loại luôn dòng, và `+` nối chuỗi
|
|
195
|
+
ra `NULL`. Tham số có thể vắng thì bọc `ISNULL(...)` ngay tại chỗ dùng.
|
|
196
|
+
|
|
197
|
+
Kiểm tên khoá thật sự tới được SP: `db_sp_help({ name })` in danh sách tham số mà tAPI đang thấy
|
|
198
|
+
(xem §3.1).
|
|
199
|
+
|
|
200
|
+
> **CRITICAL — Không được gán giá trị mặc định cho bất kỳ tham số nào:**
|
|
201
|
+
> tAPI **luôn truyền đủ mọi tham số của SP** — khoá nào client không gửi thì nó truyền `NULL`. Nên
|
|
202
|
+
> `DEFAULT` trong khai báo SP **không bao giờ được dùng tới**: viết `@HeID int = 99` rồi không gửi
|
|
203
|
+
> `HeID` thì nhận `NULL`, không phải `99`. Default ở đây là một lời hứa luôn bị phá, và nó im lặng.
|
|
204
|
+
> Không gán `= value` hay `= null` cho bất kỳ tham số nào — kể cả `@sys_*`. **Đây không chỉ là quy ước**: `db_sp_deploy` chặn cứng (code-level, `detectDefaultApiParams()` trong `src/tools/db.ts`) mọi SP `spAPI_*`/`spAPIFILE_*` có tham số dạng này, và `db_sp_verify` cảnh báo sớm cùng lỗi trước cả khi deploy.
|
|
205
|
+
> Cần một giá trị mặc định thì đặt **trong thân SP**: `SET @HeID = ISNULL(@HeID, 99)`.
|
|
206
|
+
>
|
|
207
|
+
> ```sql
|
|
208
|
+
> -- ❌ SAI
|
|
209
|
+
> CREATE PROCEDURE spAPI_TS_Report
|
|
210
|
+
> @HeID int = 99,
|
|
211
|
+
> @NamHoc int = 2024,
|
|
212
|
+
> @sys_UserID varchar(9) = null,
|
|
213
|
+
> @sys_SystemRight int
|
|
214
|
+
>
|
|
215
|
+
> -- ✅ ĐÚNG
|
|
216
|
+
> CREATE PROCEDURE spAPI_TS_Report
|
|
217
|
+
> @HeID int,
|
|
218
|
+
> @NamHoc int,
|
|
219
|
+
> @sys_UserID varchar(9),
|
|
220
|
+
> @sys_SystemRight int
|
|
221
|
+
> ```
|
|
222
|
+
|
|
223
|
+
Client gửi: `POST /app/FunctionName` với body `{ "StudentID": "SV001", "SemesterID": 1 }`
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 3. URL gọi API
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
{domain}/{apiName}/{FunctionName}/{url1}/{url2}...
|
|
231
|
+
{domain}/{apiName}/auth/{FunctionName}/{url1}/{url2}...
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| Phần | Ý nghĩa |
|
|
235
|
+
|---|---|
|
|
236
|
+
| `domain` | Domain API, ví dụ `https://api.example.vn` |
|
|
237
|
+
| `apiName` | Tên apiName của ứng dụng: `me`, `congvan`, `calen`... — cũng chính là **alias kết nối DB** trong workspace (`_db/{apiName}/`) |
|
|
238
|
+
| `auth` | Thêm segment này khi SP có `AUTH_` (không cần token) |
|
|
239
|
+
| `FunctionName` | Phần tên sau prefix của SP |
|
|
240
|
+
| `url1`, `url2`... | Route params tương ứng `@url1_`, `@url2_` |
|
|
241
|
+
|
|
242
|
+
**Một project có thể có NHIỀU `apiName`** — tức nhiều database, mỗi cái một alias. `"API": "/{alias}/Ten"`
|
|
243
|
+
trong module.json là thứ nói SP đó nằm ở database nào; MCP dùng đúng alias ấy làm tên thư mục
|
|
244
|
+
`_db/{alias}/` và làm tham số `db` của các DB tool (xem [db-workflow.md](db-workflow.md) §1, §4).
|
|
245
|
+
|
|
246
|
+
**Ví dụ:**
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
SP: spAPI_AUTH_Select_Chat
|
|
250
|
+
URL: https://api.example.vn/me/auth/Select_Chat
|
|
251
|
+
|
|
252
|
+
SP: spAPI_GetStudentList
|
|
253
|
+
URL: https://api.example.vn/me/GetStudentList
|
|
254
|
+
|
|
255
|
+
SP: spAPI_GetStudentDetail (@url1_StudentID)
|
|
256
|
+
URL: https://api.example.vn/me/GetStudentDetail/SV001
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
> **CRITICAL — lỗi ghép URL hay gặp: thiếu dấu `/` giữa `domain` và `apiName`.**
|
|
260
|
+
> `domain` KHÔNG BAO GIỜ tự có dấu `/` ở cuối — phải tự chèn đúng MỘT dấu `/` trước `apiName` khi ghép. Gõ tắt bằng tay rất dễ dính lỗi này vì mắt đọc lướt không phân biệt được đâu là phần domain, đâu là phần apiName trong một chuỗi liền.
|
|
261
|
+
>
|
|
262
|
+
> ```
|
|
263
|
+
> domain=tapi.lhu.edu.vn, apiName=ts, function=TS_Report_HeXetTuyenSelectAll
|
|
264
|
+
>
|
|
265
|
+
> ❌ SAI (thiếu /, "ts" bị nuốt vào domain): https://tapi.lhu.edu.vnts/TS_Report_HeXetTuyenSelectAll
|
|
266
|
+
> ✅ ĐÚNG: https://tapi.lhu.edu.vn/ts/TS_Report_HeXetTuyenSelectAll
|
|
267
|
+
> ```
|
|
268
|
+
>
|
|
269
|
+
> **Đừng tự ghép chuỗi bằng tay/trí nhớ.** `db_sp_verify` (dòng `Wiring URL: ...`) và `db_sp_help` đã tự tính URL đúng cho từng SP — copy nguyên văn dòng đó thay vì gõ lại.
|
|
270
|
+
|
|
271
|
+
### 3.1 Cache tham số & route `/help`
|
|
272
|
+
|
|
273
|
+
tAPI **cache chữ ký tham số** (danh sách tên/kiểu param) của mỗi hàm sau lần gọi đầu tiên. Sau khi ALTER một SP `spAPI_*`/`spAPIFILE_*` để **thêm/bớt/đổi tên/đổi kiểu tham số**, endpoint thật vẫn phục vụ theo chữ ký **cũ** cho tới khi cache được xoá.
|
|
274
|
+
|
|
275
|
+
Route xoá cache = URL gọi API thật, chèn thêm `/help` ngay sau domain (trước `apiName`):
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
{domain}/help/{apiName}/{FunctionName} ← xoá cache + trả về tham số của hàm
|
|
279
|
+
{domain}/help/{apiName}/auth/{FunctionName}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Gọi route này có hai tác dụng đồng thời: **(a) xoá cache tham số** của hàm đó và **(b) trả về danh sách tham số đầu vào** — hữu ích để tra cứu trước khi wire `IN` vào module.json mà không chắc SP đang nhận params nào.
|
|
283
|
+
|
|
284
|
+
**Quy tắc bắt buộc — xoá cache sau mỗi lần ALTER/UPDATE SP:**
|
|
285
|
+
- Dùng `db_sp_deploy`: tool tự gọi `/help` xoá cache. **Gọi đúng một lần theo URL chuẩn; hỏng thì bỏ qua** — SP đã deploy xong, đây chỉ là dọn cache.
|
|
286
|
+
- Dùng `db_sql_execute_nonquery` để chạy `ALTER PROCEDURE` trực tiếp: **không có auto-clear** → **bắt buộc gọi `db_sp_help` thủ công** ngay sau khi ALTER thành công. Đây là ca duy nhất bắt buộc gọi tay.
|
|
287
|
+
- Không xoá cache → endpoint thật tiếp tục phục vụ theo chữ ký **cũ**, có thể gây lỗi tham số ngay cả khi SP đã đúng.
|
|
288
|
+
- **`/help` báo lỗi thì DỪNG, không thử URL biến thể.** Dạng URL (`/auth/` hay không) suy ra từ tên SP, không phải thứ để đoán; thử qua lại chỉ tốn lượt mà không đổi kết quả. Chỉ quay lại `db_sp_help` khi endpoint thật thực sự còn nhận tham số cũ.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 4. Format dữ liệu trả về
|
|
293
|
+
|
|
294
|
+
### 4.1 Một SELECT — trả về array
|
|
295
|
+
|
|
296
|
+
```sql
|
|
297
|
+
SELECT StudentID, FullName, GPA FROM tblStudent WHERE ClassID = @ClassID
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"data": [
|
|
303
|
+
{ "StudentID": "SV001", "FullName": "Nguyen Van A", "GPA": 3.5 },
|
|
304
|
+
{ "StudentID": "SV002", "FullName": "Tran Thi B", "GPA": 3.2 }
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### 4.2 Nhiều SELECT — trả về array of arrays
|
|
310
|
+
|
|
311
|
+
Mỗi `SELECT` trong SP tạo thành một mảng con trong `data`:
|
|
312
|
+
|
|
313
|
+
```sql
|
|
314
|
+
SELECT TotalCount = COUNT(*) FROM tblStudent WHERE ClassID = @ClassID
|
|
315
|
+
SELECT StudentID, FullName FROM tblStudent WHERE ClassID = @ClassID
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```json
|
|
319
|
+
{
|
|
320
|
+
"data": [
|
|
321
|
+
[{ "TotalCount": 42 }],
|
|
322
|
+
[
|
|
323
|
+
{ "StudentID": "SV001", "FullName": "Nguyen Van A" },
|
|
324
|
+
{ "StudentID": "SV002", "FullName": "Tran Thi B" }
|
|
325
|
+
]
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Trong FUI module.json**, đọc từng mảng con qua index:
|
|
331
|
+
```json
|
|
332
|
+
"OUT": "studentList",
|
|
333
|
+
"CALLBACK": { "EXE": "vueData.total = vueData.studentList[0][0].TotalCount; vueData.students = vueData.studentList[1]" }
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 4.3 `convert_to_object` — convert một dòng thành object
|
|
337
|
+
|
|
338
|
+
Dùng khi muốn trả về record dạng object thay vì array. Một SP có thể có **nhiều SELECT** dùng `convert_to_object` — tất cả được merge vào cùng một object response.
|
|
339
|
+
|
|
340
|
+
> ⚠️ **Quy tắc quan trọng**: Khi SELECT có `convert_to_object` **và** cột `json_data` chứa JSON string, SELECT đó chỉ được trả về **đúng 1 dòng**. Nếu nhiều hơn 1 dòng → tAPI trả về `{}` (object rỗng).
|
|
341
|
+
|
|
342
|
+
**Hai dạng giá trị:**
|
|
343
|
+
- `convert_to_object = 'key'` → tạo nested object với tên key đó
|
|
344
|
+
- `convert_to_object = ''` → merge thẳng vào root object
|
|
345
|
+
|
|
346
|
+
**Ví dụ 1 — Kết hợp named key và root merge:**
|
|
347
|
+
|
|
348
|
+
```sql
|
|
349
|
+
SELECT convert_to_object = 'sinhvien',
|
|
350
|
+
sys_SystemRight = @sys_SystemRight,
|
|
351
|
+
sys_FunctionRight = @sys_FunctionRight
|
|
352
|
+
|
|
353
|
+
SELECT convert_to_object = '',
|
|
354
|
+
ngaythang = GETDATE(), count = 10
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"sinhvien": {
|
|
360
|
+
"sys_SystemRight": 9,
|
|
361
|
+
"sys_FunctionRight": "[11][3]"
|
|
362
|
+
},
|
|
363
|
+
"ngaythang": "2018-09-11T11:20:47.397",
|
|
364
|
+
"count": 10
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Ví dụ 2 — `json_data` override field cùng tên:**
|
|
369
|
+
|
|
370
|
+
```sql
|
|
371
|
+
SELECT convert_to_object = '',
|
|
372
|
+
ProjectName = N'abc',
|
|
373
|
+
stt = 123,
|
|
374
|
+
json_data = '{"ProjectName":"T-A-P-I","menu":[{"name":"Croatia","link":"vn"},{"name":"England","link":"/tuyensinh"}]}'
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```json
|
|
378
|
+
{
|
|
379
|
+
"ProjectName": "T-A-P-I",
|
|
380
|
+
"stt": 123,
|
|
381
|
+
"menu": [
|
|
382
|
+
{ "name": "Croatia", "link": "vn" },
|
|
383
|
+
{ "name": "England", "link": "/tuyensinh" }
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
> `json_data` được parse và merge sau — nếu có field trùng tên với cột SQL (`ProjectName`), **giá trị từ `json_data` sẽ override**.
|
|
389
|
+
|
|
390
|
+
**Ví dụ 3 — Nhiều SELECT cùng `convert_to_object = ''` đều merge vào root:**
|
|
391
|
+
|
|
392
|
+
```sql
|
|
393
|
+
SELECT convert_to_object = '',
|
|
394
|
+
ProjectName = N'abc', stt = 123,
|
|
395
|
+
json_data = '{"ProjectName":"T-A-P-I","menu":[{"name":"Croatia","link":"vn"},{"name":"England","link":"/tuyensinh"}]}'
|
|
396
|
+
|
|
397
|
+
SELECT convert_to_object = '',
|
|
398
|
+
TruyVan2 = N'Dữ liệu của truy vấn 2',
|
|
399
|
+
json_data = '{"TruyVan2_json_string": 99}'
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{
|
|
404
|
+
"ProjectName": "T-A-P-I",
|
|
405
|
+
"stt": 123,
|
|
406
|
+
"menu": [
|
|
407
|
+
{ "name": "Croatia", "link": "vn" },
|
|
408
|
+
{ "name": "England", "link": "/tuyensinh" }
|
|
409
|
+
],
|
|
410
|
+
"TruyVan2": "Dữ liệu của truy vấn 2",
|
|
411
|
+
"TruyVan2_json_string": 99
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Tất cả SELECT trong SP đều merge vào **cùng 1 object** — không tạo array như SELECT thông thường.
|
|
416
|
+
|
|
417
|
+
### 4.4 `[json_data:FieldName]` — Convert chuỗi JSON thành object
|
|
418
|
+
|
|
419
|
+
Khi cột trong bảng lưu chuỗi JSON và cần trả về dạng object (không phải string), dùng alias `[json_data:FieldName]`:
|
|
420
|
+
|
|
421
|
+
```sql
|
|
422
|
+
SELECT PartID, PartName, [json_data:FileInfo] = FileInfo
|
|
423
|
+
FROM tblPart WHERE PartID = @url1_PartID
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
tAPI tự động parse cột có alias `json_data:FieldName` thành nested JSON object trong response:
|
|
427
|
+
|
|
428
|
+
```json
|
|
429
|
+
{
|
|
430
|
+
"PartID": "P001",
|
|
431
|
+
"PartName": "Tài liệu",
|
|
432
|
+
"FileInfo": { "fileName": "doc.pdf", "size": 1024 }
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Không cần parse thủ công ở client. Cột DB vẫn lưu dạng string — chỉ response được convert.
|
|
437
|
+
|
|
438
|
+
> Tên sau dấu `:` (`FileInfo`) là tên field trong response. Tên cột SQL (`FileInfo` trong `= FileInfo`) là tên cột thực trên bảng.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## 5. Xử lý lỗi trong SP
|
|
443
|
+
|
|
444
|
+
Tất cả lỗi dùng `RAISERROR` với severity **16** — không dùng severity khác.
|
|
445
|
+
|
|
446
|
+
### Lỗi thông thường (validation, ràng buộc dữ liệu)
|
|
447
|
+
|
|
448
|
+
```sql
|
|
449
|
+
RAISERROR(N'Dữ liệu không hợp lệ', 16, 1)
|
|
450
|
+
RETURN
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
```json
|
|
454
|
+
{ "Message": "Dữ liệu không hợp lệ" }
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Lỗi Unauthorized (thiếu quyền) — prefix `[Unauthorized]` → HTTP 401
|
|
458
|
+
|
|
459
|
+
```sql
|
|
460
|
+
IF @sys_SystemRight < 2
|
|
461
|
+
BEGIN
|
|
462
|
+
RAISERROR(N'[Unauthorized]Bạn không có quyền trên chức năng này.', 16, 1)
|
|
463
|
+
RETURN
|
|
464
|
+
END
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
{ "Message": "Bạn không có quyền trên chức năng này." }
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Quy tắc bắt buộc:**
|
|
472
|
+
- Luôn dùng severity `16` — không dùng `11`, `14`, hay giá trị khác
|
|
473
|
+
- Luôn có `RETURN` ngay sau `RAISERROR` để dừng SP
|
|
474
|
+
- Prefix `N` bắt buộc cho Unicode tiếng Việt
|
|
475
|
+
- **Mọi lỗi liên quan đến quyền (thiếu `SystemRight`/`FunctionRight`) phải mở message bằng `[Unauthorized]`** — tAPI đọc chuỗi này để trả HTTP status `401` thay vì `200`/`500` mặc định; client (`errorMess()` trong `fastproject.js`) rẽ nhánh xử lý riêng theo status `401` (redirect login nếu message chứa "token", ngược lại hiện lỗi quyền). tAPI tự cắt `[Unauthorized]` khỏi `Message` trả về client — text người dùng thấy không có tiền tố này. Lỗi validation/ràng buộc dữ liệu thông thường (ví dụ trên) **không** dùng prefix này.
|
|
476
|
+
|
|
477
|
+
> **Kiểm tra quyền chi tiết (5 pattern, `getSystemRight`, thứ tự kiểm tra, quy ước message):** xem [tapi-permission-patterns.md](tapi-permission-patterns.md).
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## 6. Quy tắc thiết kế SP tốt
|
|
482
|
+
|
|
483
|
+
### Đặt tên SP
|
|
484
|
+
|
|
485
|
+
```
|
|
486
|
+
spAPI_{Entity}{Action}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
| Action | Mô tả |
|
|
490
|
+
|---|---|
|
|
491
|
+
| `Select` / `List` | Truy vấn danh sách |
|
|
492
|
+
| `Get` | Lấy chi tiết một bản ghi |
|
|
493
|
+
| `Insert` / `Add` | Thêm mới |
|
|
494
|
+
| `Update` / `Edit` | Cập nhật |
|
|
495
|
+
| `Delete` / `Remove` | Xoá |
|
|
496
|
+
| `AUTH_Select` | Truy vấn công khai (không cần token) |
|
|
497
|
+
|
|
498
|
+
**Ví dụ chuẩn:**
|
|
499
|
+
|
|
500
|
+
```sql
|
|
501
|
+
spAPI_StudentList -- GET danh sách sinh viên
|
|
502
|
+
spAPI_StudentGet -- GET chi tiết (url1 = StudentID)
|
|
503
|
+
spAPI_StudentInsert -- POST thêm mới
|
|
504
|
+
spAPI_StudentUpdate -- POST cập nhật
|
|
505
|
+
spAPI_StudentDelete -- POST xoá (url1 = StudentID)
|
|
506
|
+
spAPI_AUTH_CourseList -- Danh sách môn học công khai
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Wiring trong FUI module.json
|
|
510
|
+
|
|
511
|
+
```json
|
|
512
|
+
"fetchStudents": {
|
|
513
|
+
"API": "/me/StudentList",
|
|
514
|
+
"IN": { "Keyword": "vueData.keyword", "ClassID": "vueData.selectedClass" },
|
|
515
|
+
"OUT": "studentList"
|
|
516
|
+
},
|
|
517
|
+
"addStudent": {
|
|
518
|
+
"API": "/me/StudentInsert",
|
|
519
|
+
"IN": {
|
|
520
|
+
"StudentID": "vueData.form.StudentID",
|
|
521
|
+
"FullName": "vueData.form.FullName",
|
|
522
|
+
"ClassID": "vueData.form.ClassID"
|
|
523
|
+
},
|
|
524
|
+
"CALLBACK": [ { "CALL": "vueData.fetchStudents" }, { "MESS": "Đã thêm thành công" } ]
|
|
525
|
+
},
|
|
526
|
+
"deleteStudent": {
|
|
527
|
+
"API": "/me/StudentDelete/`{{vueData.selectedID}}",
|
|
528
|
+
"CONFIRM": "Bạn có chắc muốn xoá?",
|
|
529
|
+
"CALLBACK": { "CALL": "vueData.fetchStudents" }
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## 7. Checklist khi thiết kế SP
|
|
536
|
+
|
|
537
|
+
1. Prefix `spAPI_` bắt buộc — thiếu là API không gọi được
|
|
538
|
+
2. Thêm `AUTH_` nếu endpoint công khai (login page, lookup data)
|
|
539
|
+
3. `@sys_UserID` luôn có trong SP có ghi dữ liệu (audit trail)
|
|
540
|
+
4. Không dùng `SELECT *` — liệt kê cột cụ thể
|
|
541
|
+
5. Kiểm tra quyền ở đầu SP nếu cần, dùng `RAISERROR` + `RETURN` — xem [tapi-permission-patterns.md](tapi-permission-patterns.md)
|
|
542
|
+
6. Nhiều SELECT → client đọc theo index `data[0]`, `data[1]`
|
|
543
|
+
7. Một record duy nhất → dùng `convert_to_object` thay vì `data[0][0]`
|
|
544
|
+
8. **Không viết `GRANT EXECUTE` vào file .sql** — thân `CREATE PROCEDURE` kéo dài tới hết batch và tAPI không hỗ trợ `GO`, nên câu GRANT đặt sau `END` bị **nuốt vào thân SP**: nó chạy mỗi lần gọi SP chứ không cấp quyền lúc deploy, và không lỗi nào nổi lên. `db_sp_deploy` tự cấp quyền cho procedure mới rồi xác nhận lại bằng `sys.database_permissions` — đọc dòng `✓`/`❗` trong kết quả (chi tiết: [verification.md](verification.md#security))
|
|
545
|
+
9. **Không đưa tham số `sys_*` vào `IN` của module.json** — tAPI tự inject, truyền vào là thừa
|
|
546
|
+
9b. **Đọc HTTP header** → `@sys_header_{TênHeader}` với **mỗi dấu `-` đổi thành `_`** (`User-Agent` → `@sys_header_User_Agent`). Giữ nguyên dấu `-` là **lỗi cú pháp lúc CREATE PROCEDURE**, và không bọc ngoặc vuông để né được — xem §2. Header do client tự khai nên chỉ dùng để log, không dùng để gác quyền
|
|
547
|
+
9c. **Ghép tham số không bao giờ báo lỗi**: khoá thiếu → `NULL`, khoá thừa → bỏ qua. Gõ sai tên khoá trong `IN` cho ra HTTP 200 + dữ liệu rỗng/sai, không cho ra lỗi — đối chiếu tên bằng `db_sp_help` thay vì chờ hệ thống báo. Cũng vì vậy `DEFAULT` trong khai báo SP vô tác dụng (tAPI luôn truyền `NULL`); mặc định phải đặt trong thân: `SET @x = ISNULL(@x, <giá trị>)`
|
|
548
|
+
10. **File GET/UPLOAD**: prefix `spAPIFILE_`, cột binary alias `fileContent`, params `@sys_FileContent`/`@sys_FileName`, bảng chuẩn `tblFileData` — xem [tapi-file-api.md](tapi-file-api.md). Không tự suy từ training data.
|
|
549
|
+
11. **Mọi ALTER/UPDATE SP đã deploy** (đổi tham số, đổi logic, hay bất kỳ thay đổi nào) → **BẮT BUỘC xoá cache tAPI** qua `db_sp_help` sau khi deploy — nếu không endpoint thật vẫn phục vụ theo chữ ký cũ. `db_sp_deploy` tự làm (một lần, hỏng thì bỏ qua); chỉ khi ALTER qua `db_sql_execute_nonquery` mới phải gọi `db_sp_help` thủ công.
|