sd-render 1.2.1 → 1.2.4

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 (93) hide show
  1. package/api-docs.html +1460 -0
  2. package/field-docs.html +15358 -0
  3. package/package.json +1 -1
  4. package/{sd-lib-CdzQnrjh.js → sd-lib-Cczl-l04.js} +2136 -1906
  5. package/{sd-render-FK6Mjp-B.js → sd-render-DETM6GzA.js} +903 -816
  6. package/sd-render.es.js +58 -57
  7. package/sd-render.style.css +1 -1
  8. package/types/src/components/form-render/form-field/alert-ui.vue.d.ts +1 -0
  9. package/types/src/components/form-render/form-field/apexchart-ui.vue.d.ts +1 -0
  10. package/types/src/components/form-render/form-field/autonumber-input.vue.d.ts +1 -0
  11. package/types/src/components/form-render/form-field/avatar-ui.vue.d.ts +1 -0
  12. package/types/src/components/form-render/form-field/btn-editor-input.vue.d.ts +1 -0
  13. package/types/src/components/form-render/form-field/button-ui.vue.d.ts +1 -0
  14. package/types/src/components/form-render/form-field/carousel-ui.vue.d.ts +1 -0
  15. package/types/src/components/form-render/form-field/cascader-form-input.vue.d.ts +372 -0
  16. package/types/src/components/form-render/form-field/chart-ui.vue.d.ts +1 -0
  17. package/types/src/components/form-render/form-field/checkbox-input.vue.d.ts +1 -0
  18. package/types/src/components/form-render/form-field/code-input.vue.d.ts +1 -0
  19. package/types/src/components/form-render/form-field/color-input.vue.d.ts +1 -0
  20. package/types/src/components/form-render/form-field/crop-upload-input.vue.d.ts +1 -0
  21. package/types/src/components/form-render/form-field/datagrid-form-ui.vue.d.ts +1 -0
  22. package/types/src/components/form-render/form-field/datagrid-sql-ui.vue.d.ts +1 -0
  23. package/types/src/components/form-render/form-field/date-input.vue.d.ts +1 -0
  24. package/types/src/components/form-render/form-field/date-range-input.vue.d.ts +1 -0
  25. package/types/src/components/form-render/form-field/divider-ui.vue.d.ts +1 -0
  26. package/types/src/components/form-render/form-field/dropdown-ui.vue.d.ts +1 -0
  27. package/types/src/components/form-render/form-field/dynamic-input.vue.d.ts +1 -0
  28. package/types/src/components/form-render/form-field/file-upload-input.vue.d.ts +1 -0
  29. package/types/src/components/form-render/form-field/group-list-input.vue.d.ts +1 -0
  30. package/types/src/components/form-render/form-field/html-input.vue.d.ts +1 -0
  31. package/types/src/components/form-render/form-field/html-ui.vue.d.ts +1 -0
  32. package/types/src/components/form-render/form-field/icon-input.vue.d.ts +1 -0
  33. package/types/src/components/form-render/form-field/image-ui.vue.d.ts +1 -0
  34. package/types/src/components/form-render/form-field/json-input.vue.d.ts +1 -0
  35. package/types/src/components/form-render/form-field/link-ui.vue.d.ts +1 -0
  36. package/types/src/components/form-render/form-field/list-ui.vue.d.ts +1 -0
  37. package/types/src/components/form-render/form-field/masked-input.vue.d.ts +1 -0
  38. package/types/src/components/form-render/form-field/multiple-date.vue.d.ts +1 -0
  39. package/types/src/components/form-render/form-field/number-input.vue.d.ts +1 -0
  40. package/types/src/components/form-render/form-field/objectid-input.vue.d.ts +1 -0
  41. package/types/src/components/form-render/form-field/picture-upload-input.vue.d.ts +1 -0
  42. package/types/src/components/form-render/form-field/progress-ui.vue.d.ts +1 -0
  43. package/types/src/components/form-render/form-field/qrcode-ui.vue.d.ts +1 -0
  44. package/types/src/components/form-render/form-field/radio-input.vue.d.ts +1 -0
  45. package/types/src/components/form-render/form-field/radio-text-input.vue.d.ts +1 -0
  46. package/types/src/components/form-render/form-field/rate-input.vue.d.ts +1 -0
  47. package/types/src/components/form-render/form-field/record-ui.vue.d.ts +1 -0
  48. package/types/src/components/form-render/form-field/report-ui.vue.d.ts +1 -0
  49. package/types/src/components/form-render/form-field/segmented-ui.vue.d.ts +1 -0
  50. package/types/src/components/form-render/form-field/select-data-input.vue.d.ts +1 -0
  51. package/types/src/components/form-render/form-field/select-form-input.vue.d.ts +1 -0
  52. package/types/src/components/form-render/form-field/select-input.vue.d.ts +1 -0
  53. package/types/src/components/form-render/form-field/select-path-input.vue.d.ts +1 -0
  54. package/types/src/components/form-render/form-field/select-sql-input.vue.d.ts +1 -0
  55. package/types/src/components/form-render/form-field/side-menu-ui.vue.d.ts +1 -0
  56. package/types/src/components/form-render/form-field/slider-input.vue.d.ts +1 -0
  57. package/types/src/components/form-render/form-field/statistic-ui.vue.d.ts +1 -0
  58. package/types/src/components/form-render/form-field/step-ui.vue.d.ts +1 -0
  59. package/types/src/components/form-render/form-field/svg-input.vue.d.ts +1 -0
  60. package/types/src/components/form-render/form-field/svg-ui.vue.d.ts +1 -0
  61. package/types/src/components/form-render/form-field/switch-input.vue.d.ts +1 -0
  62. package/types/src/components/form-render/form-field/tags-input.vue.d.ts +1 -0
  63. package/types/src/components/form-render/form-field/text-input.vue.d.ts +1 -0
  64. package/types/src/components/form-render/form-field/text-ui.vue.d.ts +1 -0
  65. package/types/src/components/form-render/form-field/textarea-input.vue.d.ts +1 -0
  66. package/types/src/components/form-render/form-field/time-input.vue.d.ts +1 -0
  67. package/types/src/components/form-render/form-field/time-range-input.vue.d.ts +1 -0
  68. package/types/src/components/form-render/form-field/time-select-input.vue.d.ts +1 -0
  69. package/types/src/components/form-render/form-field/tour-ui.vue.d.ts +1 -0
  70. package/types/src/components/form-render/mixins/CoreFieldMixin.d.ts +1 -0
  71. package/types/src/components/input3/eltiptap/widget/ExtensionViews/ImageView.vue.d.ts +8 -8
  72. package/types/src/components/input3/eltiptap/widget/MenuCommands/ColorPopover.vue.d.ts +8 -8
  73. package/types/src/components/input3/eltiptap/widget/MenuCommands/CommandButton.vue.d.ts +6 -6
  74. package/types/src/components/input3/eltiptap/widget/MenuCommands/FontFamilyDropdown.vue.d.ts +28 -28
  75. package/types/src/components/input3/eltiptap/widget/MenuCommands/FontSizeDropdown.vue.d.ts +28 -28
  76. package/types/src/components/input3/eltiptap/widget/MenuCommands/HeadingDropdown.vue.d.ts +28 -28
  77. package/types/src/components/input3/eltiptap/widget/MenuCommands/HighlightPopover.vue.d.ts +8 -8
  78. package/types/src/components/input3/eltiptap/widget/MenuCommands/Image/ImageDisplayCommandButton.vue.d.ts +8 -8
  79. package/types/src/components/input3/eltiptap/widget/MenuCommands/Image/InsertImageCommandButton.vue.d.ts +8 -8
  80. package/types/src/components/input3/eltiptap/widget/MenuCommands/LineHeightDropdown.vue.d.ts +28 -28
  81. package/types/src/components/input3/eltiptap/widget/MenuCommands/TablePopover/CreateTablePopover.vue.d.ts +8 -8
  82. package/types/src/components/input3/eltiptap/widget/MenuCommands/TablePopover/index.vue.d.ts +8 -8
  83. package/types/src/components/sdlib.d.ts +1 -0
  84. package/types/src/components/sdwidget/SdCascaderForm.vue.d.ts +128 -0
  85. package/types/src/components/sdwidget/SdCrudGrid.vue.d.ts +9 -0
  86. package/types/src/components/sdwidget/SdFormSchema.vue.d.ts +3 -5
  87. package/types/src/components/sdwidget/SdFormSchemaForm.vue.d.ts +3 -5
  88. package/types/src/components/sdwidget/SdGallery.vue.d.ts +6 -10
  89. package/types/src/components/sdwidget/SdUiCarousel.vue.d.ts +9 -0
  90. package/types/src/components/sdwidget/SdUiListView.vue.d.ts +9 -0
  91. package/types/src/components/sdwidget/SdUiMenu.vue.d.ts +9 -0
  92. package/types/src/components/sdwidget/SdUiRecordView.vue.d.ts +9 -0
  93. package/types/src/components/sdwidget/SdChart.vue.d.ts +0 -282
package/api-docs.html ADDED
@@ -0,0 +1,1460 @@
1
+ <!DOCTYPE html>
2
+ <html lang="th">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SDForm API Process - Function Reference</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: 'Segoe UI', Tahoma, sans-serif; background: #1e1e2e; color: #cdd6f4; display: flex; }
10
+
11
+ /* Sidebar */
12
+ .sidebar { width: 280px; height: 100vh; background: #181825; border-right: 1px solid #313244; position: fixed; overflow-y: auto; padding: 16px 0; }
13
+ .sidebar h2 { padding: 8px 20px 16px; color: #cba6f7; font-size: 16px; border-bottom: 1px solid #313244; }
14
+ .sidebar .group { padding: 12px 0 4px; }
15
+ .sidebar .group-title { padding: 4px 20px; font-size: 11px; text-transform: uppercase; color: #6c7086; font-weight: 700; letter-spacing: 1px; }
16
+ .sidebar a { display: block; padding: 6px 20px 6px 28px; color: #a6adc8; text-decoration: none; font-size: 13px; font-family: 'Fira Code', 'Consolas', monospace; transition: all .15s; }
17
+ .sidebar a:hover { background: #313244; color: #cba6f7; }
18
+ .sidebar a.active { background: #313244; color: #cba6f7; border-left: 3px solid #cba6f7; padding-left: 25px; }
19
+
20
+ /* Main */
21
+ .main { margin-left: 280px; padding: 32px 48px; flex: 1; max-width: 900px; }
22
+ .main h1 { color: #cba6f7; font-size: 28px; margin-bottom: 8px; }
23
+ .main .subtitle { color: #6c7086; margin-bottom: 32px; font-size: 14px; }
24
+
25
+ /* Section */
26
+ .section { margin-bottom: 48px; scroll-margin-top: 24px; }
27
+ .section h2 { color: #f5c2e7; font-size: 20px; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid #313244; }
28
+ .section h3 { color: #89b4fa; font-size: 15px; margin: 0 0 4px; font-family: 'Fira Code', monospace; }
29
+
30
+ /* Card */
31
+ .card { background: #1e1e2e; border: 1px solid #313244; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
32
+ .card:hover { border-color: #45475a; }
33
+ .sig { background: #181825; padding: 10px 14px; border-radius: 6px; font-family: 'Fira Code', monospace; font-size: 13px; color: #a6e3a1; margin: 8px 0; overflow-x: auto; }
34
+ .desc { color: #9399b2; font-size: 13px; margin: 6px 0; line-height: 1.6; }
35
+ .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-right: 4px; }
36
+ .tag-return { background: #1e3a2f; color: #a6e3a1; }
37
+ .tag-param { background: #1e2d3d; color: #89b4fa; }
38
+ .tag-warn { background: #3d2e1e; color: #fab387; }
39
+
40
+ /* Code */
41
+ pre { background: #11111b; border: 1px solid #313244; border-radius: 8px; padding: 16px; overflow-x: auto; margin: 10px 0; font-size: 13px; line-height: 1.6; }
42
+ pre code { font-family: 'Fira Code', 'Consolas', monospace; color: #cdd6f4; }
43
+ .kw { color: #cba6f7; }
44
+ .str { color: #a6e3a1; }
45
+ .cm { color: #6c7086; }
46
+ .fn { color: #89b4fa; }
47
+ .num { color: #fab387; }
48
+ .var { color: #f38ba8; }
49
+
50
+ /* Table */
51
+ table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 13px; }
52
+ th { background: #181825; color: #cba6f7; text-align: left; padding: 8px 12px; }
53
+ td { padding: 8px 12px; border-bottom: 1px solid #313244; color: #bac2de; }
54
+ td code { background: #313244; padding: 2px 6px; border-radius: 4px; font-size: 12px; color: #89b4fa; }
55
+ </style>
56
+ </head>
57
+ <body>
58
+
59
+ <nav class="sidebar">
60
+ <h2>SDForm API Docs</h2>
61
+
62
+ <div class="group">
63
+ <div class="group-title">Database CRUD</div>
64
+ <a href="#dbInsert">dbInsert</a>
65
+ <a href="#dbInsertMany">dbInsertMany</a>
66
+ <a href="#dbFindAll">dbFindAll</a>
67
+ <a href="#dbFindOne">dbFindOne</a>
68
+ <a href="#dbFindById">dbFindById</a>
69
+ <a href="#dbUpdate">dbUpdate</a>
70
+ <a href="#dbUpdateMany">dbUpdateMany</a>
71
+ <a href="#dbDelete">dbDelete</a>
72
+ <a href="#dbObjectId">dbObjectId</a>
73
+ <a href="#curDate">curDate</a>
74
+ </div>
75
+
76
+ <div class="group">
77
+ <div class="group-title">SDForm Functions</div>
78
+ <a href="#sdformGetOne">sdformGetOne</a>
79
+ <a href="#sdformGetAll">sdformGetAll</a>
80
+ <a href="#sdformSetOne">sdformSetOne</a>
81
+ <a href="#sdformDelOne">sdformDelOne</a>
82
+ </div>
83
+
84
+ <div class="group">
85
+ <div class="group-title">Form Management</div>
86
+ <a href="#dataPolicyAudit">dataPolicyAudit</a>
87
+ <a href="#initSaveForm">initSaveForm</a>
88
+ <a href="#insertData">insertData</a>
89
+ <a href="#insertDataForm">insertDataForm</a>
90
+ <a href="#updateFileStatus">updateFileStatus</a>
91
+ <a href="#deleteFileSystem">deleteFileSystem</a>
92
+ <a href="#afterSaveForm">afterSaveForm</a>
93
+ <a href="#afterDeleteForm">afterDeleteForm</a>
94
+ </div>
95
+
96
+ <div class="group">
97
+ <div class="group-title">Process & Event</div>
98
+ <a href="#runProcess">runProcess</a>
99
+ <a href="#subProcess">subProcess</a>
100
+ </div>
101
+
102
+ <div class="group">
103
+ <div class="group-title">Transactions & Locks</div>
104
+ <a href="#txn-overview">ภาพรวม Transactions</a>
105
+ <a href="#mongoTxn">mongoTxn</a>
106
+ <a href="#withVersion">withVersion</a>
107
+ <a href="#txn-patterns">Pattern การใช้งาน</a>
108
+ <a href="#txn-errors">Error handling</a>
109
+ </div>
110
+
111
+ <div class="group">
112
+ <div class="group-title">xformDatax (Return to Form)</div>
113
+ <a href="#xformDatax-overview">วิธีส่งค่ากลับฟอร์ม</a>
114
+ <a href="#xformDatax-basic">ตัวอย่าง: คำนวณ BMI</a>
115
+ <a href="#xformDatax-multi">ส่งกลับหลาย field</a>
116
+ <a href="#xformDatax-lookup">ดึงข้อมูลเติมฟอร์ม</a>
117
+ <a href="#xformDatax-rules">กฎสำคัญ</a>
118
+ </div>
119
+
120
+ <div class="group">
121
+ <div class="group-title">SQL & Query</div>
122
+ <a href="#runSql">runSql</a>
123
+ <a href="#parsedSQL">parsedSQL</a>
124
+ <a href="#parsedProvider">parsedProvider</a>
125
+ <a href="#pgQuery">pgQuery</a>
126
+ </div>
127
+
128
+ <div class="group">
129
+ <div class="group-title">WebSocket & Notify</div>
130
+ <a href="#wsSend">wsSend</a>
131
+ <a href="#notifySend">notifySend</a>
132
+ </div>
133
+
134
+ <div class="group">
135
+ <div class="group-title">User & Roles</div>
136
+ <a href="#getUserInfo">getUserInfo</a>
137
+ <a href="#isRole">isRole</a>
138
+ <a href="#isAdmin">isAdmin</a>
139
+ <a href="#isManager">isManager</a>
140
+ <a href="#isSuper">isSuper</a>
141
+ <a href="#isAuth">isAuth</a>
142
+ <a href="#assignRole">assignRole</a>
143
+ <a href="#revokeRole">revokeRole</a>
144
+ </div>
145
+
146
+ <div class="group">
147
+ <div class="group-title">Encryption</div>
148
+ <a href="#encode">encode</a>
149
+ <a href="#decode">decode</a>
150
+ <a href="#encodeObj">encodeObj</a>
151
+ <a href="#decodeObj">decodeObj</a>
152
+ <a href="#generateKeyPair">generateKeyPair</a>
153
+ </div>
154
+
155
+ <div class="group">
156
+ <div class="group-title">Report</div>
157
+ <a href="#wordReport">wordReport</a>
158
+ </div>
159
+
160
+ <div class="group">
161
+ <div class="group-title">Utilities (this.*)</div>
162
+ <a href="#isNotNull">isNotNull / isNull</a>
163
+ <a href="#isEmptyStr">isEmptyStr / isEmptyObj</a>
164
+ <a href="#generateId_util">generateId / genUidTime / genUUID</a>
165
+ <a href="#deepClone">deepClone</a>
166
+ <a href="#compareHash">compareHash</a>
167
+ <a href="#objectPath">setObjectByPath / getObjectByPath</a>
168
+ <a href="#string2Json">string2Json / string2boolean</a>
169
+ <a href="#object2Path">object2Path</a>
170
+ </div>
171
+
172
+ <div class="group">
173
+ <div class="group-title">Interfaces</div>
174
+ <a href="#SdProvider">SdProvider</a>
175
+ <a href="#SdDataProvider">SdDataProvider</a>
176
+ <a href="#UserInfo">UserInfo</a>
177
+ </div>
178
+ </nav>
179
+
180
+ <main class="main">
181
+ <h1>SDForm API Process</h1>
182
+ <p class="subtitle">Function Reference สำหรับเขียน api_process ใน API Factory</p>
183
+
184
+ <!-- ============================================ -->
185
+ <!-- DATABASE CRUD -->
186
+ <!-- ============================================ -->
187
+ <div class="section" id="db-crud">
188
+ <h2>Database CRUD</h2>
189
+ <p class="desc">เข้าถึง MongoDB โดยตรง ไม่ผ่านระบบสิทธิ์ของ sdform</p>
190
+
191
+ <div class="card" id="dbInsert">
192
+ <h3>app.dbInsert()</h3>
193
+ <div class="sig">app.dbInsert(data, from, userInfo)</div>
194
+ <p class="desc">Insert เอกสารเดียวลง collection พร้อมเพิ่ม audit fields อัตโนมัติ (created_by, created_at, updated_by, updated_at)</p>
195
+ <table>
196
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
197
+ <tr><td><code>data</code></td><td>object</td><td>ข้อมูลที่ต้องการ insert</td></tr>
198
+ <tr><td><code>from</code></td><td>string</td><td>ชื่อ collection</td></tr>
199
+ <tr><td><code>userInfo</code></td><td>UserInfo</td><td>ข้อมูลผู้ใช้</td></tr>
200
+ </table>
201
+ <span class="tag tag-return">return</span> <code>{ success, reply: { message, data, id } }</code>
202
+ <pre><code><span class="cm">// Insert ข้อมูลลง collection ตรง</span>
203
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbInsert</span>({
204
+ name: <span class="str">'สมชาย'</span>,
205
+ email: <span class="str">'somchai@email.com'</span>,
206
+ age: <span class="num">30</span>
207
+ }, <span class="str">'zdata_employee'</span>, userInfo);
208
+
209
+ <span class="kw">if</span> (result.success) {
210
+ <span class="kw">return</span> { id: result.id, data: result.reply.data };
211
+ }</code></pre>
212
+ </div>
213
+
214
+ <div class="card" id="dbInsertMany">
215
+ <h3>app.dbInsertMany()</h3>
216
+ <div class="sig">app.dbInsertMany(data, from, userInfo)</div>
217
+ <p class="desc">Insert หลายเอกสารพร้อมกัน</p>
218
+ <table>
219
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
220
+ <tr><td><code>data</code></td><td>array</td><td>array ของ object ที่ต้องการ insert</td></tr>
221
+ <tr><td><code>from</code></td><td>string</td><td>ชื่อ collection</td></tr>
222
+ <tr><td><code>userInfo</code></td><td>UserInfo</td><td>ข้อมูลผู้ใช้</td></tr>
223
+ </table>
224
+ <span class="tag tag-return">return</span> <code>{ success, reply: { message, data, ids[] } }</code>
225
+ <pre><code><span class="kw">const</span> items = [
226
+ { name: <span class="str">'A'</span>, value: <span class="num">1</span> },
227
+ { name: <span class="str">'B'</span>, value: <span class="num">2</span> },
228
+ { name: <span class="str">'C'</span>, value: <span class="num">3</span> }
229
+ ];
230
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbInsertMany</span>(items, <span class="str">'zdata_items'</span>, userInfo);</code></pre>
231
+ </div>
232
+
233
+ <div class="card" id="dbFindAll">
234
+ <h3>app.dbFindAll()</h3>
235
+ <div class="sig">app.dbFindAll(dataProvider, totalEnable, limitEnable)</div>
236
+ <p class="desc">ดึงข้อมูลหลายรายการ รองรับ where, join, pagination, sort, group by</p>
237
+ <table>
238
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
239
+ <tr><td><code>dataProvider</code></td><td>SdDataProvider</td><td>เงื่อนไขการ query</td></tr>
240
+ <tr><td><code>totalEnable</code></td><td>boolean</td><td>นับจำนวนทั้งหมดด้วย</td></tr>
241
+ <tr><td><code>limitEnable</code></td><td>boolean</td><td>เปิดใช้ limit/offset</td></tr>
242
+ </table>
243
+ <span class="tag tag-return">return</span> <code>{ success, reply: { message, data[], total, offset } }</code>
244
+ <pre><code><span class="cm">// ดึงข้อมูลพนักงานที่ active เรียงตามชื่อ</span>
245
+ <span class="kw">let</span> dp = {
246
+ from: <span class="str">'zdata_employee'</span>,
247
+ select: [<span class="str">'name'</span>, <span class="str">'email'</span>, <span class="str">'department'</span>],
248
+ where: <span class="str">"xrstatx NOT IN(0,3) AND status = :status"</span>,
249
+ orderBy: [{ column: <span class="str">'name'</span>, sort: <span class="str">'ASC'</span> }],
250
+ limit: <span class="num">20</span>,
251
+ page: <span class="num">1</span>,
252
+ params: { status: <span class="str">'active'</span> }
253
+ };
254
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbFindAll</span>(dp, <span class="kw">true</span>, <span class="kw">true</span>);
255
+
256
+ <span class="cm">// result.reply.data = [{ name, email, department }, ...]</span>
257
+ <span class="cm">// result.reply.total = 150 (จำนวนทั้งหมด)</span>
258
+ <span class="cm">// result.reply.offset = 0</span></code></pre>
259
+
260
+ <pre><code><span class="cm">// ใช้ NoSQL aggregate ตรง</span>
261
+ <span class="kw">let</span> dp = {
262
+ from: <span class="str">'zdata_employee'</span>,
263
+ nosql: {
264
+ type: <span class="str">'aggregate'</span>,
265
+ collections: [<span class="str">'zdata_employee'</span>],
266
+ pipeline: [
267
+ { $match: { xrstatx: { $nin: [<span class="num">0</span>, <span class="num">3</span>] } } },
268
+ { $group: { _id: <span class="str">'$department'</span>, count: { $sum: <span class="num">1</span> } } }
269
+ ]
270
+ }
271
+ };
272
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbFindAll</span>(dp, <span class="kw">false</span>, <span class="kw">false</span>);</code></pre>
273
+ </div>
274
+
275
+ <div class="card" id="dbFindOne">
276
+ <h3>app.dbFindOne()</h3>
277
+ <div class="sig">app.dbFindOne(dataProvider)</div>
278
+ <p class="desc">ดึงข้อมูล 1 รายการ</p>
279
+ <span class="tag tag-return">return</span> <code>{ success, id, reply: { message, data } }</code>
280
+ <pre><code><span class="kw">let</span> dp = {
281
+ from: <span class="str">'zdata_employee'</span>,
282
+ where: <span class="str">"_id = CONVERT(:id, 'objectId')"</span>,
283
+ params: { id: params.dataid }
284
+ };
285
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbFindOne</span>(dp);
286
+
287
+ <span class="kw">if</span> (result.success) {
288
+ <span class="kw">return</span> { employee: result.reply.data };
289
+ }</code></pre>
290
+ </div>
291
+
292
+ <div class="card" id="dbFindById">
293
+ <h3>app.dbFindById()</h3>
294
+ <div class="sig">app.dbFindById(dataObjectId, from, projection?)</div>
295
+ <p class="desc">ดึงข้อมูลด้วย ObjectId ตรง ง่ายกว่า dbFindOne</p>
296
+ <table>
297
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
298
+ <tr><td><code>dataObjectId</code></td><td>ObjectId</td><td>ObjectId ของเอกสาร</td></tr>
299
+ <tr><td><code>from</code></td><td>string</td><td>ชื่อ collection</td></tr>
300
+ <tr><td><code>projection</code></td><td>object?</td><td>เลือก field ที่ต้องการ</td></tr>
301
+ </table>
302
+ <pre><code><span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbFindById</span>(
303
+ app.<span class="fn">dbObjectId</span>(params.dataid),
304
+ <span class="str">'zdata_employee'</span>,
305
+ { name: <span class="num">1</span>, email: <span class="num">1</span> } <span class="cm">// เฉพาะ field ที่ต้องการ</span>
306
+ );
307
+
308
+ <span class="kw">if</span> (result.success) {
309
+ <span class="kw">return</span> { data: result.reply.data };
310
+ }</code></pre>
311
+ </div>
312
+
313
+ <div class="card" id="dbUpdate">
314
+ <h3>app.dbUpdate()</h3>
315
+ <div class="sig">app.dbUpdate(data, from, userInfo, filter, upsert?)</div>
316
+ <p class="desc">อัพเดทเอกสาร 1 รายการ merge กับข้อมูลเดิม เพิ่ม updated_by, updated_at อัตโนมัติ</p>
317
+ <table>
318
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
319
+ <tr><td><code>data</code></td><td>object</td><td>ข้อมูลที่ต้องการอัพเดท</td></tr>
320
+ <tr><td><code>from</code></td><td>string</td><td>ชื่อ collection</td></tr>
321
+ <tr><td><code>userInfo</code></td><td>UserInfo</td><td>ข้อมูลผู้ใช้</td></tr>
322
+ <tr><td><code>filter</code></td><td>object</td><td>เงื่อนไข filter</td></tr>
323
+ <tr><td><code>upsert</code></td><td>boolean?</td><td>insert ถ้าไม่เจอ (default: false)</td></tr>
324
+ </table>
325
+ <pre><code><span class="cm">// อัพเดท status ของพนักงาน</span>
326
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbUpdate</span>(
327
+ { status: <span class="str">'inactive'</span>, reason: <span class="str">'ลาออก'</span> },
328
+ <span class="str">'zdata_employee'</span>,
329
+ userInfo,
330
+ { _id: app.<span class="fn">dbObjectId</span>(params.dataid) }
331
+ );
332
+
333
+ <span class="cm">// upsert: insert ถ้าไม่มี</span>
334
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbUpdate</span>(
335
+ { count: <span class="num">1</span>, name: <span class="str">'ใหม่'</span> },
336
+ <span class="str">'zdata_counter'</span>,
337
+ userInfo,
338
+ { code: <span class="str">'VISIT'</span> },
339
+ <span class="kw">true</span> <span class="cm">// upsert</span>
340
+ );</code></pre>
341
+ </div>
342
+
343
+ <div class="card" id="dbUpdateMany">
344
+ <h3>app.dbUpdateMany()</h3>
345
+ <div class="sig">app.dbUpdateMany(data, from, userInfo, filter, upsert?)</div>
346
+ <p class="desc">อัพเดทหลายเอกสารที่ตรงเงื่อนไข</p>
347
+ <pre><code><span class="cm">// เปลี่ยน status พนักงานทั้งแผนก</span>
348
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbUpdateMany</span>(
349
+ { status: <span class="str">'transferred'</span> },
350
+ <span class="str">'zdata_employee'</span>,
351
+ userInfo,
352
+ { department: <span class="str">'IT'</span> }
353
+ );</code></pre>
354
+ </div>
355
+
356
+ <div class="card" id="dbDelete">
357
+ <h3>app.dbDelete()</h3>
358
+ <div class="sig">app.dbDelete(from, filter)</div>
359
+ <p class="desc">ลบเอกสารจริงจาก collection</p>
360
+ <span class="tag tag-warn">Warning</span> ลบถาวร ไม่สามารถกู้คืนได้
361
+ <pre><code><span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">dbDelete</span>(
362
+ <span class="str">'zdata_temp'</span>,
363
+ { expired: <span class="kw">true</span> }
364
+ );</code></pre>
365
+ </div>
366
+
367
+ <div class="card" id="dbObjectId">
368
+ <h3>app.dbObjectId()</h3>
369
+ <div class="sig">app.dbObjectId(id?)</div>
370
+ <p class="desc">แปลง string เป็น MongoDB ObjectId หรือสร้างใหม่ถ้าไม่ส่งค่า</p>
371
+ <pre><code><span class="cm">// แปลง string → ObjectId</span>
372
+ <span class="kw">const</span> oid = app.<span class="fn">dbObjectId</span>(<span class="str">'69d78e1a160f454e4e480cd3'</span>);
373
+
374
+ <span class="cm">// สร้าง ObjectId ใหม่</span>
375
+ <span class="kw">const</span> newOid = app.<span class="fn">dbObjectId</span>();</code></pre>
376
+ </div>
377
+
378
+ <div class="card" id="curDate">
379
+ <h3>app.curDate()</h3>
380
+ <div class="sig">app.curDate(format?)</div>
381
+ <p class="desc">วันที่-เวลาปัจจุบัน format ด้วย dayjs (default: 'YYYY-MM-DD HH:mm:ss')</p>
382
+ <pre><code><span class="kw">const</span> now = app.<span class="fn">curDate</span>(); <span class="cm">// "2026-04-09 18:30:00"</span>
383
+ <span class="kw">const</span> date = app.<span class="fn">curDate</span>(<span class="str">'YYYY-MM-DD'</span>); <span class="cm">// "2026-04-09"</span>
384
+ <span class="kw">const</span> thai = app.<span class="fn">curDate</span>(<span class="str">'DD/MM/YYYY'</span>); <span class="cm">// "09/04/2026"</span></code></pre>
385
+ </div>
386
+ </div>
387
+
388
+ <!-- ============================================ -->
389
+ <!-- SDFORM FUNCTIONS -->
390
+ <!-- ============================================ -->
391
+ <div class="section" id="sdform-func">
392
+ <h2>SDForm Functions</h2>
393
+ <p class="desc">เข้าถึงข้อมูลผ่านระบบ sdform — เช็คสิทธิ์ + รัน form event อัตโนมัติ</p>
394
+
395
+ <div class="card" id="sdformGetOne">
396
+ <h3>app.sdformGetOne()</h3>
397
+ <div class="sig">app.sdformGetOne(sdProvider, userInfo)</div>
398
+ <p class="desc">ดึงข้อมูล 1 รายการผ่าน sdform เช็คสิทธิ์ view + data sharing policy</p>
399
+ <span class="tag tag-return">return</span> <code>{ success, data, message, sdformModel, id }</code>
400
+ <pre><code><span class="kw">let</span> sdProvider = {
401
+ providerId: <span class="str">'69d785d4160f454e4e480c82'</span>, <span class="cm">// formId</span>
402
+ providerType: <span class="str">'FORM'</span>,
403
+ options: {
404
+ where: <span class="str">"_id = CONVERT(:id, 'objectId')"</span>
405
+ },
406
+ params: { id: params.dataid }
407
+ };
408
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">sdformGetOne</span>(sdProvider, userInfo);
409
+
410
+ <span class="kw">if</span> (result.success) {
411
+ <span class="kw">return</span> { data: result.data };
412
+ }</code></pre>
413
+ </div>
414
+
415
+ <div class="card" id="sdformGetAll">
416
+ <h3>app.sdformGetAll()</h3>
417
+ <div class="sig">app.sdformGetAll(sdProvider, totalEnable, userInfo)</div>
418
+ <p class="desc">ดึงข้อมูลหลายรายการผ่าน sdform เช็คสิทธิ์ view</p>
419
+ <span class="tag tag-return">return</span> <code>{ success, data[], message, sdformModel, id }</code>
420
+ <pre><code><span class="kw">let</span> sdProvider = {
421
+ providerId: <span class="str">'69d785d4160f454e4e480c82'</span>,
422
+ providerType: <span class="str">'FORM'</span>,
423
+ options: {
424
+ limit: <span class="num">10</span>,
425
+ orderBy: [{ column: <span class="str">'created_at'</span>, sort: <span class="str">'DESC'</span> }]
426
+ },
427
+ params: {}
428
+ };
429
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">sdformGetAll</span>(sdProvider, <span class="kw">true</span>, userInfo);</code></pre>
430
+ </div>
431
+
432
+ <div class="card" id="sdformSetOne">
433
+ <h3>app.sdformSetOne()</h3>
434
+ <div class="sig">app.sdformSetOne(formId, dataId, dataUpdate, rstat, userInfo)</div>
435
+ <p class="desc">บันทึกข้อมูลผ่าน sdform — เช็คสิทธิ์ insert/update, รัน initSaveForm + afterSaveForm event</p>
436
+ <table>
437
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
438
+ <tr><td><code>formId</code></td><td>string</td><td>ID ของฟอร์มใน sdform_manage</td></tr>
439
+ <tr><td><code>dataId</code></td><td>string</td><td>ID ของ record (ว่าง = insert ใหม่)</td></tr>
440
+ <tr><td><code>dataUpdate</code></td><td>object</td><td>ข้อมูลที่ต้องการบันทึก</td></tr>
441
+ <tr><td><code>rstat</code></td><td>number</td><td>0=draft, 1=active, 2=submitted, 3=deleted</td></tr>
442
+ <tr><td><code>userInfo</code></td><td>UserInfo</td><td>ข้อมูลผู้ใช้</td></tr>
443
+ </table>
444
+ <pre><code><span class="cm">// อัพเดท record ที่มีอยู่</span>
445
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">sdformSetOne</span>(
446
+ <span class="str">'69d785d4160f454e4e480c82'</span>, <span class="cm">// formId</span>
447
+ params.dataid, <span class="cm">// dataId</span>
448
+ { weight: <span class="num">70</span>, height: <span class="num">175</span> }, <span class="cm">// data</span>
449
+ <span class="num">1</span>, <span class="cm">// rstat = active</span>
450
+ userInfo
451
+ );
452
+
453
+ <span class="kw">if</span> (result.success) {
454
+ <span class="kw">return</span> { data: result.data, id: result.id };
455
+ }</code></pre>
456
+ </div>
457
+
458
+ <div class="card" id="sdformDelOne">
459
+ <h3>app.sdformDelOne()</h3>
460
+ <div class="sig">app.sdformDelOne(formId, dataId, userInfo)</div>
461
+ <p class="desc">ลบข้อมูลผ่าน sdform (soft delete: xrstatx = 3) เช็คสิทธิ์ delete + รัน afterDeleteForm event</p>
462
+ <pre><code><span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">sdformDelOne</span>(
463
+ <span class="str">'69d785d4160f454e4e480c82'</span>,
464
+ params.dataid,
465
+ userInfo
466
+ );</code></pre>
467
+ </div>
468
+ </div>
469
+
470
+ <!-- ============================================ -->
471
+ <!-- FORM MANAGEMENT -->
472
+ <!-- ============================================ -->
473
+ <div class="section" id="form-mgmt">
474
+ <h2>Form Management</h2>
475
+
476
+ <div class="card" id="dataPolicyAudit">
477
+ <h3>app.dataPolicyAudit()</h3>
478
+ <div class="sig">app.dataPolicyAudit(sdProvider, userInfo, policyAction, data, type)</div>
479
+ <p class="desc">ตรวจสอบสิทธิ์การเข้าถึงฟอร์ม/SQL ตาม form_share, policy, roles</p>
480
+ <table>
481
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
482
+ <tr><td><code>policyAction</code></td><td>string</td><td>'insert' | 'update' | 'delete' | 'view'</td></tr>
483
+ <tr><td><code>type</code></td><td>string</td><td>'one' | 'all'</td></tr>
484
+ </table>
485
+ <pre><code><span class="kw">let</span> sdProvider = { providerId: formId, providerType: <span class="str">'FORM'</span> };
486
+ <span class="kw">const</span> perm = <span class="kw">await</span> app.<span class="fn">dataPolicyAudit</span>(
487
+ sdProvider, userInfo, <span class="str">'view'</span>, <span class="kw">undefined</span>, <span class="str">'all'</span>
488
+ );
489
+
490
+ <span class="kw">if</span> (perm.permissionDenied) {
491
+ <span class="kw">return</span> { success: <span class="kw">false</span>, message: <span class="str">'ไม่มีสิทธิ์'</span> };
492
+ }
493
+ <span class="cm">// perm.from = collection name</span>
494
+ <span class="cm">// perm.dataProvider = prepared SdDataProvider</span>
495
+ <span class="cm">// perm.formModel = SdFormType model</span></code></pre>
496
+ </div>
497
+
498
+ <div class="card" id="initSaveForm">
499
+ <h3>app.initSaveForm()</h3>
500
+ <div class="sig">app.initSaveForm(sdformModel, dataUpdate, dataObjectId, rstat, userInfo)</div>
501
+ <p class="desc">เตรียมข้อมูลก่อน save — แปลง type, จัดการ auto-number, เช็ค unique record</p>
502
+ <pre><code><span class="kw">const</span> init = <span class="kw">await</span> app.<span class="fn">initSaveForm</span>(
503
+ sdformModel, dataUpdate, app.<span class="fn">dbObjectId</span>(dataId), <span class="num">1</span>, userInfo
504
+ );
505
+ <span class="kw">if</span> (init.success) {
506
+ <span class="cm">// init.data = processed data ready to save</span>
507
+ }</code></pre>
508
+ </div>
509
+
510
+ <div class="card" id="insertData">
511
+ <h3>app.insertData()</h3>
512
+ <div class="sig">app.insertData(formId, userInfo)</div>
513
+ <p class="desc">สร้าง record เปล่าใหม่ (xrstatx = 0) สำหรับฟอร์ม</p>
514
+ <pre><code><span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">insertData</span>(
515
+ <span class="str">'69d785d4160f454e4e480c82'</span>, userInfo
516
+ );
517
+ <span class="cm">// result.id = ID ของ record ใหม่</span></code></pre>
518
+ </div>
519
+
520
+ <div class="card" id="insertDataForm">
521
+ <h3>app.insertDataForm()</h3>
522
+ <div class="sig">app.insertDataForm(sdformModel, userInfo)</div>
523
+ <p class="desc">สร้าง record ใหม่จาก sdformModel พร้อม system fields (site, unit, version)</p>
524
+ <pre><code><span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">insertDataForm</span>(sdformModel, userInfo);</code></pre>
525
+ </div>
526
+
527
+ <div class="card" id="updateFileStatus">
528
+ <h3>app.updateFileStatus()</h3>
529
+ <div class="sig">app.updateFileStatus(sdformModel, formData, userInfo)</div>
530
+ <p class="desc">อัพเดท use_status ของไฟล์ใน core_files_manage เป็น true</p>
531
+ </div>
532
+
533
+ <div class="card" id="deleteFileSystem">
534
+ <h3>app.deleteFileSystem()</h3>
535
+ <div class="sig">app.deleteFileSystem(formData, userInfo)</div>
536
+ <p class="desc">ลบไฟล์จาก filesystem และ record ใน core_files_manage</p>
537
+ </div>
538
+
539
+ <div class="card" id="afterSaveForm">
540
+ <h3>app.afterSaveForm()</h3>
541
+ <div class="sig">app.afterSaveForm(sdformModel, dataUpdate, dataObjectId, isInsert, userInfo)</div>
542
+ <p class="desc">รัน form event หลัง save (api_onevent, update_relational_fields, harvest_data)</p>
543
+ <span class="tag tag-return">return</span> <code>{ updateData: boolean, data }</code>
544
+ </div>
545
+
546
+ <div class="card" id="afterDeleteForm">
547
+ <h3>app.afterDeleteForm()</h3>
548
+ <div class="sig">app.afterDeleteForm(sdformModel, dataUpdate, dataObjectId, userInfo)</div>
549
+ <p class="desc">รัน form event หลัง delete (api_onevent on delete, delete_children_record, harvest_data)</p>
550
+ <span class="tag tag-return">return</span> <code>{ updateData: boolean, data }</code>
551
+ </div>
552
+ </div>
553
+
554
+ <!-- ============================================ -->
555
+ <!-- PROCESS & EVENT -->
556
+ <!-- ============================================ -->
557
+ <div class="section" id="process-event">
558
+ <h2>Process & Event</h2>
559
+
560
+ <div class="card" id="runProcess">
561
+ <h3>app.runProcess()</h3>
562
+ <div class="sig">app.runProcess(processId, params, userInfo)</div>
563
+ <p class="desc">เรียก API Factory ตัวอื่น (full result รวม apiModel)</p>
564
+ <span class="tag tag-return">return</span> <code>{ success, permissionDenied, reply: { status, message, data, error }, apiModel }</code>
565
+ <pre><code><span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">runProcess</span>(
566
+ <span class="str">'69d78073160f454e4e480c7c'</span>,
567
+ { title: <span class="str">'Hello'</span>, message: <span class="str">'Test'</span> },
568
+ userInfo
569
+ );
570
+ <span class="kw">if</span> (result.success) {
571
+ <span class="kw">return</span> result.reply.data;
572
+ }</code></pre>
573
+ </div>
574
+
575
+ <div class="card" id="subProcess">
576
+ <h3>app.subProcess()</h3>
577
+ <div class="sig">app.subProcess(processId, params, userInfo)</div>
578
+ <p class="desc">เรียก API Factory ตัวอื่น (simplified result) เหมาะใช้ chain หลาย API</p>
579
+ <span class="tag tag-return">return</span> <code>{ success, message, data }</code>
580
+ <pre><code><span class="cm">// เรียก API ต่อเนื่อง</span>
581
+ <span class="kw">const</span> step1 = <span class="kw">await</span> app.<span class="fn">subProcess</span>(<span class="str">'api_id_1'</span>, params, userInfo);
582
+ <span class="kw">if</span> (step1.success) {
583
+ <span class="kw">const</span> step2 = <span class="kw">await</span> app.<span class="fn">subProcess</span>(<span class="str">'api_id_2'</span>, step1.data, userInfo);
584
+ <span class="kw">return</span> step2.data;
585
+ }</code></pre>
586
+ </div>
587
+
588
+ </div>
589
+
590
+ <!-- ============================================ -->
591
+ <!-- TRANSACTIONS & OPTIMISTIC LOCK -->
592
+ <!-- ============================================ -->
593
+ <div class="section" id="transactions">
594
+ <h2>Transactions & Optimistic Lock</h2>
595
+ <p class="desc">
596
+ ใช้เมื่อต้องการความถูกต้องของข้อมูลภายใต้ <strong>concurrent writes</strong> เช่น ตัดงบประมาณ, โอนเงิน, ลดสต๊อก, update balance
597
+ ระบบใช้ MongoDB Transaction + Optimistic Lock (version field) ป้องกัน lost update
598
+ </p>
599
+
600
+ <div class="card" id="txn-overview">
601
+ <h3>ภาพรวม</h3>
602
+ <p class="desc">มี 2 helper ที่ทำงานคู่กัน:</p>
603
+ <table>
604
+ <tr><th>Helper</th><th>หน้าที่</th></tr>
605
+ <tr><td><code>this.mongoTxn</code></td><td>ห่อฟังก์ชันใน <strong>MongoDB transaction</strong> — commit ทั้งหมดหรือ rollback ทั้งหมด + retry อัตโนมัติเมื่อเจอ conflict</td></tr>
606
+ <tr><td><code>this.withVersion</code></td><td><strong>Optimistic lock</strong> — อ่าน doc → คำนวณ patch → update ด้วย filter <code>{_id, version: current}</code> + bump version (+1). ถ้าคนอื่นเขียนก่อน → throw <code>VERSION_CONFLICT</code> → mongoTxn retry ให้</td></tr>
607
+ </table>
608
+ <p class="desc">ใช้ร่วมกันเมื่อต้องการความถูกต้องระดับ <strong>ACID</strong> + <strong>lost-update protection</strong></p>
609
+
610
+ <pre><code><span class="cm">// โครงสร้างพื้นฐาน</span>
611
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">mongoTxn</span>(<span class="kw">async</span> (session) =&gt; {
612
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'zdata_budget'</span>, budgetId, (doc) =&gt; {
613
+ <span class="kw">if</span> (doc.balance &lt; amount) <span class="kw">throw new</span> <span class="fn">Error</span>(<span class="str">'Insufficient'</span>);
614
+ <span class="kw">return</span> { $inc: { balance: -amount, expense: amount } };
615
+ });
616
+ }, { name: <span class="str">'deductBudget'</span>, maxRetry: <span class="num">5</span> });</code></pre>
617
+
618
+ <span class="tag tag-warn">Requirement</span> MongoDB ต้องเป็น <strong>Replica Set</strong> ไม่ใช่ standalone (ระบบ setup แล้วเป็น <code>rs0</code>)
619
+ </div>
620
+
621
+ <div class="card" id="mongoTxn">
622
+ <h3>this.mongoTxn()</h3>
623
+ <div class="sig">this.mongoTxn(fn, options?)</div>
624
+ <p class="desc">
625
+ รัน <code>fn(session)</code> ภายใน MongoDB transaction พร้อม retry อัตโนมัติ (exponential backoff + jitter)
626
+ เมื่อเจอ conflict ที่ retry ได้ (VERSION_CONFLICT, WriteConflict/112, NoSuchTransaction/251, TransientTransactionError, UnknownTransactionCommitResult)
627
+ </p>
628
+ <table>
629
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
630
+ <tr><td><code>fn</code></td><td>async (session) =&gt; any</td><td>ฟังก์ชันที่ทำงานภายใน transaction — ต้องส่ง <code>session</code> เข้า mongo operations ทุกตัว</td></tr>
631
+ <tr><td><code>options.name</code></td><td>string?</td><td>ชื่อสำหรับ log (default: <code>'anonymous'</code>)</td></tr>
632
+ <tr><td><code>options.maxRetry</code></td><td>number?</td><td>จำนวนครั้งที่ retry ได้ (default: <code>3</code>, ต้อง &gt;= 1)</td></tr>
633
+ <tr><td><code>options.baseDelayMs</code></td><td>number?</td><td>delay เริ่มต้น (default: <code>50</code>) — delay จริง = <code>base * 2^attempt + jitter</code></td></tr>
634
+ <tr><td><code>options.timeoutMs</code></td><td>number? | null</td><td><strong>CSOT</strong> (Client Side Operation Timeout) — จำกัดเวลาที่ mongo driver ทำ <strong>internal retry loop</strong> เมื่อเจอ <code>TransientTransactionError</code> (default: <code>15000</code> = 15s เหมาะกับ interactive request / API response) ตั้งเป็น <code>null</code> เพื่อปิด CSOT แล้วใช้ <code>maxCommitTimeMS=10s</code> + driver default 120s แทน</td></tr>
635
+ </table>
636
+ <span class="tag tag-return">return</span> <code>ค่าที่ fn return กลับมา</code>
637
+ <span class="tag tag-warn">throws</span> <code>'Transaction conflict — please retry later'</code> เมื่อ maxRetry หมด หรือ original error หาก non-retryable
638
+ <span class="tag tag-warn">throws</span> <code>MongoOperationTimeoutError</code> เมื่อ CSOT หมดและไม่ succeed — <strong>ไม่ถูก retry อัตโนมัติ</strong> เพราะเสี่ยง double-commit (ดู warning ด้านล่าง)
639
+
640
+ <div style="border-top: 1px solid #313244;border-bottom: 1px solid #313244;border-right: 1px solid #313244; border-left:4px solid #ff9800;padding:10px 14px;margin:12px 0;border-radius:4px">
641
+ <strong>⚠️ ทำไม default = 15s?</strong><br>
642
+ MongoDB driver v6+ ถ้าไม่ตั้ง <code>timeoutMs</code> จะใช้ internal retry timeout <strong>hardcoded 120 วินาที</strong> — อันตรายมากสำหรับ interactive request เพราะผู้ใช้จะเห็นหน้าค้าง 2 นาที ค่า 15s คือ sweet spot สำหรับ API ทั่วไป<br><br>
643
+
644
+ <strong>⚠️ Double-commit risk เมื่อ CSOT fire ตอน commit phase</strong><br>
645
+ ถ้า CSOT timeout เกิดระหว่างตอน <code>commitTransaction()</code> — server อาจ commit สำเร็จไปแล้ว แต่ ack กลับมาช้าเกิน timeout → <code>mongoTxn</code> จะ <strong>ไม่ retry</strong> ให้ (โยน <code>MongoOperationTimeoutError</code> ออกไป) เพราะถ้า retry จะไป mutate doc อีกครั้ง → <strong>double-commit</strong><br><br>
646
+
647
+ <strong>วิธีการใช้ที่แนะนำ:</strong>
648
+ <ul style="margin:6px 0 0 20px">
649
+ <li><strong>Interactive API</strong> (default 15s) — เพียงพอสำหรับ transaction ปกติ</li>
650
+ <li><strong>Batch job / Migration</strong> — ตั้ง <code>timeoutMs: 60000</code> ขึ้นไป หรือ <code>null</code> (เพื่อใช้ 120s default)</li>
651
+ <li><strong>Heavy contention</strong> — ตั้งสูง ๆ เช่น <code>120000+</code> ป้องกัน CSOT fire ตอน commit</li>
652
+ <li>ถ้าต้อง catch <code>MongoOperationTimeoutError</code> เอง ให้ตรวจสถานะ doc ก่อนตัดสินใจ retry</li>
653
+ </ul>
654
+ </div>
655
+
656
+ <pre><code><span class="cm">// ตัดงบประมาณ 500 บาท (full pattern)</span>
657
+ <span class="kw">const</span> result = <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">mongoTxn</span>(<span class="kw">async</span> (session) =&gt; {
658
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'zdata_budget'</span>, params.budgetId, (doc) =&gt; {
659
+ <span class="kw">if</span> (doc.balance &lt; <span class="num">500</span>) <span class="kw">throw new</span> <span class="fn">Error</span>(<span class="str">'งบประมาณไม่พอ'</span>);
660
+ <span class="kw">return</span> { $inc: { balance: -<span class="num">500</span>, expense: <span class="num">500</span> } };
661
+ });
662
+
663
+ <span class="cm">// ใส่ log (ใช้ session เดียวกัน)</span>
664
+ <span class="kw">await</span> app.db.<span class="fn">collection</span>(<span class="str">'zdata_budget_log'</span>).<span class="fn">insertOne</span>({
665
+ budget_id: params.budgetId,
666
+ amount: <span class="num">500</span>,
667
+ type: <span class="str">'EXPENSE'</span>,
668
+ created_at: <span class="kw">new</span> <span class="fn">Date</span>()
669
+ }, { session });
670
+
671
+ <span class="kw">return</span> { ok: <span class="kw">true</span>, amount: <span class="num">500</span> };
672
+ }, {
673
+ name: <span class="str">'deductBudget'</span>,
674
+ maxRetry: <span class="num">5</span>,
675
+ timeoutMs: <span class="num">15000</span> <span class="cm">// default 15s — safe for interactive API</span>
676
+ });
677
+
678
+ <span class="kw">return</span> { xformDatax: { status: <span class="str">'DEDUCTED'</span>, amount: result.amount } };</code></pre>
679
+ </div>
680
+
681
+ <div class="card" id="withVersion">
682
+ <h3>this.withVersion()</h3>
683
+ <div class="sig">this.withVersion(session, collection, id, mutate)</div>
684
+ <p class="desc">
685
+ Optimistic lock helper — อ่าน doc ด้วย <code>session</code> → เรียก <code>mutate(doc)</code> ให้คืน update operator →
686
+ write กลับด้วย filter <code>{_id, version: current}</code> + auto bump <code>$inc: {version: 1}</code> + auto <code>$set: {updated_at: now}</code>
687
+ </p>
688
+ <table>
689
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
690
+ <tr><td><code>session</code></td><td>ClientSession</td><td>session จาก <code>mongoTxn</code></td></tr>
691
+ <tr><td><code>collection</code></td><td>string</td><td>ชื่อ collection</td></tr>
692
+ <tr><td><code>id</code></td><td>string | ObjectId</td><td>_id ของ document (รองรับทั้ง string และ ObjectId)</td></tr>
693
+ <tr><td><code>mutate</code></td><td>(doc) =&gt; patch</td><td>ฟังก์ชัน (async ได้) รับ doc ปัจจุบันและคืน update operator <code>{ $inc, $set, ... }</code> หรือ <code>null</code> เพื่อ no-op</td></tr>
694
+ </table>
695
+ <span class="tag tag-return">return</span> <code>{ ...doc, version: currentVersion + 1 }</code> (in-memory, merge กับ doc ก่อน patch)
696
+ <span class="tag tag-warn">throws</span> <code>'VERSION_CONFLICT'</code> เมื่อ doc ถูกเขียนทับไปแล้ว — <code>mongoTxn</code> จะ catch และ retry ให้อัตโนมัติ
697
+ <span class="tag tag-warn">throws</span> <code>'[withVersion] collection/id not found'</code> เมื่อไม่เจอ doc
698
+
699
+ <pre><code><span class="cm">// Patch แบบต่าง ๆ</span>
700
+
701
+ <span class="cm">// 1) ตัดเงินธรรมดา</span>
702
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'account'</span>, accId, (doc) =&gt; ({
703
+ $inc: { balance: -<span class="num">100</span> }
704
+ }));
705
+
706
+ <span class="cm">// 2) ผสม $inc + $set</span>
707
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'expense'</span>, expId, (doc) =&gt; ({
708
+ $inc: { amount: <span class="num">50</span> },
709
+ $set: { status: <span class="str">'APPROVED'</span>, approved_by: userInfo.username }
710
+ }));
711
+
712
+ <span class="cm">// 3) Business rule check — throw ภายใน mutate ก็ได้</span>
713
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'budget'</span>, budgetId, (doc) =&gt; {
714
+ <span class="kw">if</span> (doc.status === <span class="str">'CLOSED'</span>) <span class="kw">throw new</span> <span class="fn">Error</span>(<span class="str">'งบปิดแล้ว'</span>);
715
+ <span class="kw">if</span> (doc.balance &lt; params.amount) <span class="kw">throw new</span> <span class="fn">Error</span>(<span class="str">'ยอดไม่พอ'</span>);
716
+ <span class="kw">return</span> { $inc: { balance: -params.amount } };
717
+ });
718
+
719
+ <span class="cm">// 4) Conditional no-op — return null</span>
720
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'budget'</span>, budgetId, (doc) =&gt; {
721
+ <span class="kw">if</span> (doc.already_processed) <span class="kw">return</span> <span class="kw">null</span>; <span class="cm">// ไม่ทำอะไร</span>
722
+ <span class="kw">return</span> { $set: { already_processed: <span class="kw">true</span> } };
723
+ });
724
+
725
+ <span class="cm">// 5) Async mutate — await ภายในได้</span>
726
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'budget'</span>, budgetId, <span class="kw">async</span> (doc) =&gt; {
727
+ <span class="kw">const</span> rate = <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">fetchRate</span>();
728
+ <span class="kw">return</span> { $inc: { balance: -params.amount * rate } };
729
+ });</code></pre>
730
+
731
+ <p class="desc"><strong>การป้องกันระบบ:</strong> helper ป้องกันการ override version โดยผู้ใช้:</p>
732
+ <table>
733
+ <tr><th>ความพยายาม</th><th>ผลลัพธ์</th></tr>
734
+ <tr><td><code>$set: { version: 999 }</code></td><td>ถูกลบออก → version += 1 ปกติ</td></tr>
735
+ <tr><td><code>$unset: { version: 1 }</code></td><td>ถูกลบออก → version += 1 ปกติ</td></tr>
736
+ <tr><td><code>$inc: { version: 99 }</code></td><td>ถูก override เป็น +1</td></tr>
737
+ <tr><td>doc ไม่มี version field</td><td>รองรับ — filter ใช้ <code>{$or: [{version:0}, {version: {$exists: false}}]}</code></td></tr>
738
+ </table>
739
+ </div>
740
+
741
+ <div class="card" id="txn-patterns">
742
+ <h3>Pattern การใช้งานที่พบบ่อย</h3>
743
+
744
+ <h4 style="color:#89b4fa; margin:12px 0 6px; font-size:14px;">1. Transfer เงินระหว่าง 2 accounts</h4>
745
+ <pre><code><span class="kw">await</span> <span class="kw">this</span>.<span class="fn">mongoTxn</span>(<span class="kw">async</span> (session) =&gt; {
746
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'account'</span>, fromId, (doc) =&gt; {
747
+ <span class="kw">if</span> (doc.balance &lt; amount) <span class="kw">throw new</span> <span class="fn">Error</span>(<span class="str">'Insufficient'</span>);
748
+ <span class="kw">return</span> { $inc: { balance: -amount } };
749
+ });
750
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'account'</span>, toId, () =&gt; ({
751
+ $inc: { balance: amount }
752
+ }));
753
+ }, { name: <span class="str">'transfer'</span> });
754
+ <span class="cm">// ถ้า account ที่ 2 fail → account ที่ 1 rollback อัตโนมัติ</span></code></pre>
755
+
756
+ <h4 style="color:#89b4fa; margin:12px 0 6px; font-size:14px;">2. ตัดงบ + สร้าง expense record + insert log</h4>
757
+ <pre><code><span class="kw">const</span> result = <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">mongoTxn</span>(<span class="kw">async</span> (session) =&gt; {
758
+ <span class="cm">// 1) ตัดงบประมาณ</span>
759
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'zdata_budget'</span>, params.budget_id, (doc) =&gt; {
760
+ <span class="kw">if</span> (doc.balance &lt; params.amount) <span class="kw">throw new</span> <span class="fn">Error</span>(<span class="str">'งบไม่พอ'</span>);
761
+ <span class="kw">return</span> { $inc: { balance: -params.amount, expense: params.amount } };
762
+ });
763
+
764
+ <span class="cm">// 2) สร้าง expense record</span>
765
+ <span class="kw">const</span> expId = app.<span class="fn">dbObjectId</span>();
766
+ <span class="kw">await</span> app.db.<span class="fn">collection</span>(<span class="str">'zdata_expense'</span>).<span class="fn">insertOne</span>({
767
+ _id: expId,
768
+ budget_id: params.budget_id,
769
+ amount: params.amount,
770
+ status: <span class="str">'POSTED'</span>,
771
+ version: <span class="num">0</span>,
772
+ created_at: <span class="kw">new</span> <span class="fn">Date</span>(),
773
+ created_by: userInfo.username
774
+ }, { session });
775
+
776
+ <span class="cm">// 3) Log transaction</span>
777
+ <span class="kw">await</span> app.db.<span class="fn">collection</span>(<span class="str">'zdata_audit_log'</span>).<span class="fn">insertOne</span>({
778
+ ref_type: <span class="str">'expense'</span>,
779
+ ref_id: expId,
780
+ action: <span class="str">'CREATE'</span>,
781
+ amount: params.amount,
782
+ created_at: <span class="kw">new</span> <span class="fn">Date</span>()
783
+ }, { session });
784
+
785
+ <span class="kw">return</span> { expId };
786
+ }, { name: <span class="str">'postExpense'</span>, maxRetry: <span class="num">5</span> });
787
+
788
+ <span class="kw">return</span> { xformDatax: { status: <span class="str">'POSTED'</span>, expense_id: result.expId } };</code></pre>
789
+
790
+ <h4 style="color:#89b4fa; margin:12px 0 6px; font-size:14px;">3. Approve expense (flip status + trail history)</h4>
791
+ <pre><code><span class="kw">await</span> <span class="kw">this</span>.<span class="fn">mongoTxn</span>(<span class="kw">async</span> (session) =&gt; {
792
+ <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">withVersion</span>(session, <span class="str">'zdata_expense'</span>, params.expId, (doc) =&gt; {
793
+ <span class="kw">if</span> (doc.status !== <span class="str">'PENDING'</span>) <span class="kw">throw new</span> <span class="fn">Error</span>(<span class="str">'ต้องเป็น PENDING เท่านั้น'</span>);
794
+ <span class="kw">return</span> {
795
+ $set: {
796
+ status: <span class="str">'APPROVED'</span>,
797
+ approved_by: userInfo.username,
798
+ approved_at: <span class="kw">new</span> <span class="fn">Date</span>()
799
+ }
800
+ };
801
+ });
802
+
803
+ <span class="kw">await</span> app.db.<span class="fn">collection</span>(<span class="str">'zdata_status_history'</span>).<span class="fn">insertOne</span>({
804
+ ref_id: params.expId,
805
+ from_status: <span class="str">'PENDING'</span>,
806
+ to_status: <span class="str">'APPROVED'</span>,
807
+ by: userInfo.username,
808
+ at: <span class="kw">new</span> <span class="fn">Date</span>()
809
+ }, { session });
810
+ });</code></pre>
811
+ </div>
812
+
813
+ <div class="card" id="txn-errors">
814
+ <h3>Error handling &amp; ข้อควรระวัง</h3>
815
+
816
+ <h4 style="color:#f38ba8; margin:12px 0 6px; font-size:14px;">ข้อควรระวัง (Pitfalls)</h4>
817
+ <table>
818
+ <tr><th>อย่าทำ</th><th>ทำแบบนี้แทน</th></tr>
819
+ <tr>
820
+ <td>เรียก <code>app.dbInsert</code> / <code>app.dbUpdate</code> ภายใน mongoTxn</td>
821
+ <td>ใช้ <code>app.db.collection(...).insertOne(..., { session })</code> ตรง ๆ</td>
822
+ </tr>
823
+ <tr>
824
+ <td>ลืมส่ง <code>{ session }</code> ให้ mongo operation</td>
825
+ <td>ทุก <code>insertOne/updateOne/deleteOne/find</code> ในทรานแซกชัน <strong>ต้อง</strong>มี <code>{ session }</code></td>
826
+ </tr>
827
+ <tr>
828
+ <td>ทำงานหนัก (loop ใหญ่ / call external API) ใน fn</td>
829
+ <td>แยก logic ออกมาก่อน เข้า txn แค่ส่วน write — ไม่งั้น lock ค้างนาน</td>
830
+ </tr>
831
+ <tr>
832
+ <td>เรียก <code>mongoTxn</code> ซ้อน (nested)</td>
833
+ <td>ทำในชั้นเดียว — MongoDB ไม่รองรับ nested transaction</td>
834
+ </tr>
835
+ <tr>
836
+ <td>Side effect (email, webhook) ภายใน fn</td>
837
+ <td>เก็บข้อมูลไว้ใน result แล้วยิงหลัง <code>mongoTxn</code> commit เสร็จ</td>
838
+ </tr>
839
+ </table>
840
+
841
+ <h4 style="color:#f38ba8; margin:12px 0 6px; font-size:14px;">Error ที่เจอบ่อย</h4>
842
+ <table>
843
+ <tr><th>Error</th><th>สาเหตุ / วิธีแก้</th></tr>
844
+ <tr>
845
+ <td><code>Transaction conflict — please retry later</code></td>
846
+ <td>retry หมดแล้วยังชน → เพิ่ม <code>maxRetry</code> หรือลด contention โดยการแยก doc ออก</td>
847
+ </tr>
848
+ <tr>
849
+ <td><code>[withVersion] xxx not found</code></td>
850
+ <td>doc ไม่มีใน collection — ตรวจ id, collection name, หรือเช็คก่อนเรียก</td>
851
+ </tr>
852
+ <tr>
853
+ <td><code>Transaction numbers are only allowed on a replica set</code></td>
854
+ <td>MongoDB ไม่ใช่ replica set — ตรวจ <code>MONGODB_URL</code> ว่ามี <code>?replicaSet=rs0</code> ไหม</td>
855
+ </tr>
856
+ <tr>
857
+ <td><code>maxRetry must be &gt;= 1</code></td>
858
+ <td>ส่ง <code>maxRetry: 0</code> ไม่ได้ — ต้องอย่างน้อย 1</td>
859
+ </tr>
860
+ <tr>
861
+ <td><code>MongoOperationTimeoutError</code></td>
862
+ <td>CSOT (<code>timeoutMs</code>) หมดเวลา — ops ช้าเกิน หรือ contention สูงเกิน <strong>ไม่ถูก auto-retry</strong> เพราะเสี่ยง double-commit — ให้ตั้ง <code>timeoutMs</code> สูงขึ้น หรือลด contention</td>
863
+ </tr>
864
+ </table>
865
+
866
+ <h4 style="color:#a6e3a1; margin:12px 0 6px; font-size:14px;">เมื่อไหร่ถึงควรใช้?</h4>
867
+ <table>
868
+ <tr><th>ควรใช้</th><th>ไม่จำเป็นต้องใช้</th></tr>
869
+ <tr>
870
+ <td>ตัดงบ / เบิกจ่าย / โอนเงิน</td>
871
+ <td>Insert บันทึกข้อมูลธรรมดา (เช่น zdata_employee)</td>
872
+ </tr>
873
+ <tr>
874
+ <td>Update ที่ต้องป้องกัน lost update (balance, stock)</td>
875
+ <td>Update audit fields (created_at, note)</td>
876
+ </tr>
877
+ <tr>
878
+ <td>หลายเอกสาร/หลาย collection ต้อง commit พร้อมกัน</td>
879
+ <td>Read-only query</td>
880
+ </tr>
881
+ <tr>
882
+ <td>Workflow state transition (ต้องไม่กระโดด step)</td>
883
+ <td>Log บันทึกเหตุการณ์ (ไม่กระทบ state อื่น)</td>
884
+ </tr>
885
+ </table>
886
+
887
+ <p class="desc" style="margin-top:12px;">
888
+ <span class="tag tag-warn">Stress tested</span>
889
+ ผ่าน 91/91 assertions รวมถึง 10,000 concurrent ops (T16) และ 1,000 ops/doc contention test (T17)
890
+ — run <code>npm run stress</code> เพื่อทดสอบใหม่ หรือ <code>npm run stress:quick</code> สำหรับ quick run (ข้าม 10k tests)
891
+ </p>
892
+ </div>
893
+
894
+ </div>
895
+
896
+ <!-- ============================================ -->
897
+ <!-- xformDatax - RETURN DATA TO FORM -->
898
+ <!-- ============================================ -->
899
+ <div class="section" id="xformDatax">
900
+ <h2>xformDatax — ส่งค่ากลับไปให้ฟอร์ม</h2>
901
+ <p class="desc">
902
+ กลไกที่สำคัญที่สุดของ API Factory คือการ <strong>ส่งค่ากลับไปเขียนทับข้อมูลในฟอร์ม</strong> หลังจาก save
903
+ โดยใช้ key พิเศษ <code>xformDatax</code> ใน return ของ api_process
904
+ </p>
905
+
906
+ <div class="card" id="xformDatax-overview">
907
+ <h3>วิธีการทำงาน</h3>
908
+ <p class="desc">เมื่อฟอร์มมี <strong>Form Event</strong> ผูก API ไว้ (on: save / insert / update / delete) ระบบจะ:</p>
909
+ <table>
910
+ <tr><th>ลำดับ</th><th>สิ่งที่เกิดขึ้น</th></tr>
911
+ <tr><td>1</td><td>User กด Save → ฟอร์มบันทึกข้อมูลลง DB ก่อน</td></tr>
912
+ <tr><td>2</td><td>ระบบเรียก <code>afterSaveForm()</code> → วน loop form_event ที่ตรงกับ event</td></tr>
913
+ <tr><td>3</td><td>แต่ละ event เรียก <code>app.subProcess(apiId, params, userInfo)</code></td></tr>
914
+ <tr><td>4</td><td>api_process ทำงาน → <code>return { xformDatax: { ... } }</code></td></tr>
915
+ <tr><td>5</td><td>ระบบเจอ key <code>xformDatax</code> → <strong>merge ข้อมูลกลับเข้า document ที่เพิ่ง save</strong></td></tr>
916
+ <tr><td>6</td><td>ข้อมูลถูก update ลง DB อัตโนมัติ + frontend รับค่าใหม่ไปแสดง</td></tr>
917
+ </table>
918
+
919
+ <pre><code><span class="cm">// โค้ดภายในระบบ (ZzEventForm.ts) — เพื่อให้เข้าใจกลไก</span>
920
+ <span class="kw">const</span> apiOutput = <span class="kw">await</span> app.<span class="fn">subProcess</span>(processId, dataUpdate, userInfo);
921
+ <span class="kw">if</span> (apiOutput.success) {
922
+ <span class="kw">if</span> (!!apiOutput.data &amp;&amp; !!apiOutput.data[<span class="str">'xformDatax'</span>]) {
923
+ updateData = <span class="kw">true</span>;
924
+ formData = { ...formData, ...apiOutput.data[<span class="str">'xformDatax'</span>] };
925
+ }
926
+ }</code></pre>
927
+ <p class="desc">สังเกตว่าระบบใช้ <code>{ ...formData, ...xformDatax }</code> คือ <strong>merge ทับ</strong> เฉพาะ field ที่ส่งมา field อื่นไม่กระทบ</p>
928
+ </div>
929
+
930
+ <div class="card" id="xformDatax-basic">
931
+ <h3>ตัวอย่าง: คำนวณ BMI แล้วส่งกลับ</h3>
932
+ <p class="desc">ฟอร์มมี field: <code>weight</code>, <code>height</code>, <code>bmi</code> — ผูก Form Event on: save → API นี้</p>
933
+ <pre><code><span class="cm">// api_process: คำนวณ BMI แล้วเขียนกลับลง field "bmi" ของฟอร์ม</span>
934
+ <span class="kw">const</span> weight = <span class="fn">Number</span>(params.weight);
935
+ <span class="kw">const</span> height = <span class="fn">Number</span>(params.height);
936
+
937
+ <span class="kw">if</span> (!weight || !height) {
938
+ <span class="kw">await</span> app.<span class="fn">notifySend</span>({
939
+ title: <span class="str">'BMI'</span>,
940
+ detail: <span class="str">'กรุณากรอกน้ำหนักและส่วนสูง'</span>,
941
+ mode: <span class="str">'target'</span>,
942
+ target: [userInfo.username]
943
+ }, userInfo);
944
+ <span class="kw">return</span> {};
945
+ }
946
+
947
+ <span class="kw">const</span> heightM = height / <span class="num">100</span>;
948
+ <span class="kw">const</span> bmi = (weight / (heightM * heightM)).<span class="fn">toFixed</span>(<span class="num">2</span>);
949
+
950
+ <span class="cm">// ✅ ส่งค่ากลับฟอร์ม — field "bmi" จะถูก update อัตโนมัติ</span>
951
+ <span class="kw">return</span> { <span class="var">xformDatax</span>: { bmi: bmi } };</code></pre>
952
+ <span class="tag tag-warn">สำคัญ</span> ชื่อ key ใน xformDatax <strong>ต้องตรงกับชื่อ field ในฟอร์มเป๊ะ</strong> — ถ้าฟอร์มตั้งชื่อ <code>detile</code> (แม้สะกดผิด) ก็ต้องใช้ <code>detile</code>
953
+ </div>
954
+
955
+ <div class="card" id="xformDatax-multi">
956
+ <h3>ส่งกลับหลาย field พร้อมกัน</h3>
957
+ <p class="desc">สามารถส่งกลับหลาย field ในครั้งเดียว ระบบจะ merge ทุก field ที่ส่งมา</p>
958
+ <pre><code><span class="cm">// คำนวณราคารวมแล้วส่งกลับหลาย field</span>
959
+ <span class="kw">const</span> qty = <span class="fn">Number</span>(params.quantity) || <span class="num">0</span>;
960
+ <span class="kw">const</span> price = <span class="fn">Number</span>(params.unit_price) || <span class="num">0</span>;
961
+ <span class="kw">const</span> subtotal = qty * price;
962
+ <span class="kw">const</span> vat = subtotal * <span class="num">0.07</span>;
963
+ <span class="kw">const</span> total = subtotal + vat;
964
+
965
+ <span class="kw">return</span> {
966
+ <span class="var">xformDatax</span>: {
967
+ subtotal: subtotal,
968
+ vat: vat.<span class="fn">toFixed</span>(<span class="num">2</span>),
969
+ total: total.<span class="fn">toFixed</span>(<span class="num">2</span>),
970
+ last_calc: <span class="kw">new</span> <span class="fn">Date</span>().<span class="fn">toISOString</span>()
971
+ }
972
+ };</code></pre>
973
+ </div>
974
+
975
+ <div class="card" id="xformDatax-lookup">
976
+ <h3>ตัวอย่าง: ดึงข้อมูลจาก DB เติมกลับฟอร์ม</h3>
977
+ <p class="desc">เช่น user เลือกรหัสพนักงาน → API ดึงชื่อ, แผนก, ตำแหน่ง มาเติมให้อัตโนมัติ</p>
978
+ <pre><code><span class="cm">// params.emp_code มาจาก field ในฟอร์ม</span>
979
+ <span class="kw">const</span> emp = <span class="kw">await</span> app.<span class="fn">dbFindOne</span>(
980
+ { emp_code: params.emp_code },
981
+ <span class="str">'zdata_employee'</span>
982
+ );
983
+
984
+ <span class="kw">if</span> (!emp.success || !emp.reply.data) {
985
+ <span class="kw">return</span> {};
986
+ }
987
+
988
+ <span class="kw">const</span> d = emp.reply.data;
989
+ <span class="kw">return</span> {
990
+ <span class="var">xformDatax</span>: {
991
+ emp_name: d.fname + <span class="str">' '</span> + d.lname,
992
+ department: d.department,
993
+ position: d.position
994
+ }
995
+ };</code></pre>
996
+ </div>
997
+
998
+ <div class="card" id="xformDatax-rules">
999
+ <h3>กฎสำคัญที่ต้องจำ</h3>
1000
+ <table>
1001
+ <tr><th>#</th><th>กฎ</th><th>รายละเอียด</th></tr>
1002
+ <tr><td>1</td><td><strong>ชื่อ field ต้องตรง</strong></td><td>key ใน xformDatax ต้องตรงกับ field name ในฟอร์ม ตัวพิมพ์เล็ก/ใหญ่ต้องเหมือนกัน</td></tr>
1003
+ <tr><td>2</td><td><strong>ทำงานผ่าน Form Event เท่านั้น</strong></td><td>xformDatax ทำงานเมื่อ API ถูกเรียกจาก form_event (on: save/insert/update/delete) เท่านั้น เรียก API ตรงจาก script จะไม่ merge กลับ</td></tr>
1004
+ <tr><td>3</td><td><strong>Merge ไม่ใช่ Replace</strong></td><td>ระบบใช้ spread <code>{ ...formData, ...xformDatax }</code> ส่งเฉพาะ field ที่ต้องการ field อื่นไม่หาย</td></tr>
1005
+ <tr><td>4</td><td><strong>return {} ถ้าไม่ต้องการ update</strong></td><td>ถ้าไม่ส่ง xformDatax กลับ ระบบจะไม่ update อะไร</td></tr>
1006
+ <tr><td>5</td><td><strong>params = ข้อมูลฟอร์มที่เพิ่ง save</strong></td><td>params ที่ API ได้รับคือข้อมูลของ document ที่เพิ่งบันทึก รวม <code>_id</code> ด้วย</td></tr>
1007
+ <tr><td>6</td><td><strong>ใช้ได้กับทุก event</strong></td><td>ใช้ได้ทั้ง save, insert, update, delete — ทุก event ที่มี api ผูกอยู่จะเช็ค xformDatax</td></tr>
1008
+ </table>
1009
+
1010
+ <pre><code><span class="cm">// ❌ ผิด — ไม่มี xformDatax ครอบ ข้อมูลจะไม่กลับไปที่ฟอร์ม</span>
1011
+ <span class="kw">return</span> { bmi: <span class="num">25.5</span> };
1012
+
1013
+ <span class="cm">// ✅ ถูก — ครอบด้วย xformDatax ข้อมูลจะถูก merge กลับ</span>
1014
+ <span class="kw">return</span> { <span class="var">xformDatax</span>: { bmi: <span class="num">25.5</span> } };
1015
+
1016
+ <span class="cm">// ✅ ไม่ต้องการ update — return เปล่า</span>
1017
+ <span class="kw">return</span> {};</code></pre>
1018
+ </div>
1019
+ </div>
1020
+
1021
+ <!-- ============================================ -->
1022
+ <!-- SQL & QUERY -->
1023
+ <!-- ============================================ -->
1024
+ <div class="section" id="sql-query">
1025
+ <h2>SQL & Query</h2>
1026
+
1027
+ <div class="card" id="runSql">
1028
+ <h3>app.runSql()</h3>
1029
+ <div class="sig">app.runSql(type, sqlId, params, userInfo, totalEnable?, options?)</div>
1030
+ <p class="desc">เรียก SQL Factory ที่สร้างไว้ เช็คสิทธิ์อัตโนมัติ</p>
1031
+ <table>
1032
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
1033
+ <tr><td><code>type</code></td><td>string</td><td>'one' | 'all'</td></tr>
1034
+ <tr><td><code>sqlId</code></td><td>string</td><td>_id ของ SQL Factory ใน module_sql</td></tr>
1035
+ <tr><td><code>params</code></td><td>object</td><td>พารามิเตอร์สำหรับ :paramName</td></tr>
1036
+ <tr><td><code>totalEnable</code></td><td>boolean?</td><td>นับจำนวนทั้งหมด</td></tr>
1037
+ </table>
1038
+ <span class="tag tag-return">return</span> <code>{ success, data, model }</code>
1039
+ <span class="tag tag-warn">Note</span> return เป็น <code>result.data</code> ตรงๆ ไม่มี <code>result.reply</code>
1040
+ <pre><code><span class="cm">// ดึงหลายรายการ</span>
1041
+ <span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">runSql</span>(
1042
+ <span class="str">'all'</span>,
1043
+ <span class="str">'69d793bf503ebb2a35565c47'</span>,
1044
+ { p_code: String(params.p_code) },
1045
+ userInfo,
1046
+ <span class="kw">true</span>
1047
+ );
1048
+ <span class="kw">if</span> (result.success) {
1049
+ <span class="kw">return</span> { data: result.data };
1050
+ }
1051
+
1052
+ <span class="cm">// ดึง 1 รายการ</span>
1053
+ <span class="kw">const</span> one = <span class="kw">await</span> app.<span class="fn">runSql</span>(
1054
+ <span class="str">'one'</span>, sqlId, params, userInfo
1055
+ );
1056
+ <span class="kw">if</span> (one.success) {
1057
+ <span class="kw">return</span> { item: one.data };
1058
+ }</code></pre>
1059
+ </div>
1060
+
1061
+ <div class="card" id="parsedSQL">
1062
+ <h3>app.parsedSQL()</h3>
1063
+ <div class="sig">app.parsedSQL(sql, params)</div>
1064
+ <p class="desc">แปลง SQL string เป็น MongoDB query ผ่าน @synatic/noql พร้อม inject params</p>
1065
+ <span class="tag tag-warn">Note</span> nested field ใช้ <code>`parent.child`</code> ครอบ backtick ก้อนเดียว
1066
+ <pre><code><span class="kw">const</span> nsql = app.<span class="fn">parsedSQL</span>(
1067
+ <span class="str">"SELECT * FROM zdata_prov WHERE `p_code` = :code"</span>,
1068
+ { code: <span class="str">'97'</span> }
1069
+ );
1070
+ <span class="cm">// nsql = { collection, type, query, projection, ... }</span></code></pre>
1071
+ </div>
1072
+
1073
+ <div class="card" id="parsedProvider">
1074
+ <h3>app.parsedProvider()</h3>
1075
+ <div class="sig">app.parsedProvider(dataProvider)</div>
1076
+ <p class="desc">แปลง SdDataProvider เป็น MongoDB query + total count query</p>
1077
+ <span class="tag tag-return">return</span> <code>{ nsql, nsqlTotal, nsqlSum, sql, sqlTotal }</code>
1078
+ <pre><code><span class="kw">let</span> dp = {
1079
+ from: <span class="str">'zdata_prov'</span>,
1080
+ select: [<span class="str">'p_code'</span>, <span class="str">'p_name'</span>],
1081
+ where: <span class="str">"xrstatx NOT IN(0,3)"</span>,
1082
+ orderBy: [{ column: <span class="str">'p_name'</span>, sort: <span class="str">'ASC'</span> }]
1083
+ };
1084
+ <span class="kw">const</span> parsed = app.<span class="fn">parsedProvider</span>(dp);
1085
+ <span class="cm">// parsed.nsql = MongoDB query object</span>
1086
+ <span class="cm">// parsed.sql = Generated SQL string</span></code></pre>
1087
+ </div>
1088
+
1089
+ <div class="card" id="pgQuery">
1090
+ <h3>app.pgQuery()</h3>
1091
+ <div class="sig">app.pgQuery(sql, params?)</div>
1092
+ <p class="desc">รัน PostgreSQL query ตรง — ใช้ <code>:paramName</code> สำหรับ parameter substitution</p>
1093
+ <table>
1094
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
1095
+ <tr><td><code>sql</code></td><td>string</td><td>SQL query ใช้ <code>:paramName</code> แทนค่า</td></tr>
1096
+ <tr><td><code>params</code></td><td>object?</td><td>ค่า parameter เป็น key-value</td></tr>
1097
+ </table>
1098
+ <pre><code><span class="kw">const</span> result = <span class="kw">await</span> app.<span class="fn">pgQuery</span>(
1099
+ <span class="str">'SELECT * FROM employees WHERE dept = :dept'</span>,
1100
+ { dept: <span class="str">'IT'</span> }
1101
+ );
1102
+ <span class="kw">return</span> { data: result.rows };</code></pre>
1103
+ </div>
1104
+ </div>
1105
+
1106
+ <!-- ============================================ -->
1107
+ <!-- WEBSOCKET & NOTIFY -->
1108
+ <!-- ============================================ -->
1109
+ <div class="section" id="ws-notify">
1110
+ <h2>WebSocket & Notify</h2>
1111
+
1112
+ <div class="card" id="wsSend">
1113
+ <h3>app.wsSend()</h3>
1114
+ <div class="sig">app.wsSend(channel, clientId, username, sendData)</div>
1115
+ <p class="desc">ส่ง WebSocket message ไปยัง client ที่เชื่อมต่ออยู่</p>
1116
+ <table>
1117
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
1118
+ <tr><td><code>channel</code></td><td>string</td><td>ชื่อ channel เช่น 'notify', 'gridform', 'sdform'</td></tr>
1119
+ <tr><td><code>clientId</code></td><td>string</td><td>ID ของ client หรือ 'broadcast'</td></tr>
1120
+ <tr><td><code>username</code></td><td>string</td><td>username ผู้ส่ง</td></tr>
1121
+ <tr><td><code>sendData</code></td><td>object</td><td>{ data, method, keyid, target?, params? }</td></tr>
1122
+ </table>
1123
+ <pre><code><span class="cm">// broadcast อัพเดท datagrid</span>
1124
+ app.<span class="fn">wsSend</span>(<span class="str">'gridform'</span>, formId, userInfo.username, {
1125
+ data: updatedData,
1126
+ method: <span class="str">'update'</span>,
1127
+ keyid: <span class="str">'_id'</span>
1128
+ });
1129
+
1130
+ <span class="cm">// ส่งให้เฉพาะคน</span>
1131
+ app.<span class="fn">wsSend</span>(<span class="str">'notify'</span>, <span class="str">'broadcast'</span>, userInfo.username, {
1132
+ data: notifyData,
1133
+ method: <span class="str">'insert'</span>,
1134
+ keyid: resultId,
1135
+ target: [<span class="str">'admin'</span>] <span class="cm">// frontend จะ filter เอง</span>
1136
+ });</code></pre>
1137
+ </div>
1138
+
1139
+ <div class="card" id="notifySend">
1140
+ <h3>app.notifySend()</h3>
1141
+ <div class="sig">app.notifySend(title, message, detail, mode, type, userInfo, target?, site?, unit?, tage?)</div>
1142
+ <p class="desc">สร้าง notification + ส่งผ่าน WebSocket อัตโนมัติ เก็บใน module_notify</p>
1143
+ <table>
1144
+ <tr><th>Param</th><th>Type</th><th>Description</th></tr>
1145
+ <tr><td><code>mode</code></td><td>string</td><td>'broadcast' | 'target' | 'site' | 'unit'</td></tr>
1146
+ <tr><td><code>type</code></td><td>string</td><td>'info' | 'success' | 'warning' | 'error'</td></tr>
1147
+ <tr><td><code>target</code></td><td>string[]?</td><td>array ของ username (ใช้กับ mode target)</td></tr>
1148
+ <tr><td><code>site</code></td><td>string[]?</td><td>array ของ site code (ใช้กับ mode site)</td></tr>
1149
+ <tr><td><code>unit</code></td><td>string[]?</td><td>array ของ unit code (ใช้กับ mode unit)</td></tr>
1150
+ </table>
1151
+ <pre><code><span class="cm">// แจ้งเตือนทุกคน</span>
1152
+ <span class="kw">await</span> app.<span class="fn">notifySend</span>(
1153
+ <span class="str">'ประกาศ'</span>, <span class="str">'ระบบจะปิดปรับปรุง'</span>, <span class="str">'รายละเอียด...'</span>,
1154
+ <span class="str">'broadcast'</span>, <span class="str">'warning'</span>, userInfo
1155
+ );
1156
+
1157
+ <span class="cm">// แจ้งเฉพาะคนที่กรอก</span>
1158
+ <span class="kw">await</span> app.<span class="fn">notifySend</span>(
1159
+ <span class="str">'Error'</span>, <span class="str">'กรุณากรอกข้อมูลให้ครบ'</span>, <span class="str">''</span>,
1160
+ <span class="str">'target'</span>, <span class="str">'error'</span>, userInfo,
1161
+ [userInfo.username]
1162
+ );
1163
+
1164
+ <span class="cm">// แจ้งตามหน่วยงาน</span>
1165
+ <span class="kw">await</span> app.<span class="fn">notifySend</span>(
1166
+ <span class="str">'งานใหม่'</span>, <span class="str">'มีเอกสารรออนุมัติ'</span>, <span class="str">''</span>,
1167
+ <span class="str">'unit'</span>, <span class="str">'info'</span>, userInfo,
1168
+ <span class="kw">undefined</span>, <span class="kw">undefined</span>, [<span class="str">'00001'</span>]
1169
+ );</code></pre>
1170
+ </div>
1171
+ </div>
1172
+
1173
+ <!-- ============================================ -->
1174
+ <!-- USER & ROLES -->
1175
+ <!-- ============================================ -->
1176
+ <div class="section" id="user-roles">
1177
+ <h2>User & Roles</h2>
1178
+
1179
+ <div class="card" id="getUserInfo">
1180
+ <h3>app.getUserInfo()</h3>
1181
+ <div class="sig">app.getUserInfo(request)</div>
1182
+ <p class="desc">ดึงข้อมูล user จาก JWT token ใน request header</p>
1183
+ <span class="tag tag-return">return</span> <code>UserInfo { username, account, roles, site, unit }</code>
1184
+ </div>
1185
+
1186
+ <div class="card" id="isRole">
1187
+ <h3>app.isRole()</h3>
1188
+ <div class="sig">app.isRole(role, roles)</div>
1189
+ <p class="desc">เช็คว่า user มี role ที่ระบุหรือไม่</p>
1190
+ <pre><code><span class="kw">if</span> (app.<span class="fn">isRole</span>(<span class="str">'editor'</span>, userInfo.roles)) {
1191
+ <span class="cm">// user มี role editor</span>
1192
+ }</code></pre>
1193
+ </div>
1194
+
1195
+ <div class="card" id="isAdmin">
1196
+ <h3>app.isAdmin() / isManager() / isSuper() / isAuth()</h3>
1197
+ <div class="sig">app.isAdmin(roles) | app.isManager(roles) | app.isSuper(roles) | app.isAuth(roles)</div>
1198
+ <p class="desc">เช็ค role ระดับต่างๆ</p>
1199
+ <pre><code>app.<span class="fn">isAuth</span>(roles) <span class="cm">// มี role ที่ไม่ใช่ guest</span>
1200
+ app.<span class="fn">isManager</span>(roles) <span class="cm">// มี role manager, admin, หรือ super</span>
1201
+ app.<span class="fn">isAdmin</span>(roles) <span class="cm">// มี role admin หรือ super</span>
1202
+ app.<span class="fn">isSuper</span>(roles) <span class="cm">// มี role super เท่านั้น</span></code></pre>
1203
+ </div>
1204
+
1205
+ <div class="card" id="assignRole">
1206
+ <h3>app.assignRole() / revokeRole()</h3>
1207
+ <div class="sig">app.assignRole(userId, role) | app.revokeRole(userId, role)</div>
1208
+ <p class="desc">เพิ่ม/ลบ role ของ user อัพเดททั้ง DB และ cache</p>
1209
+ <pre><code><span class="cm">// เพิ่ม role</span>
1210
+ <span class="kw">await</span> app.<span class="fn">assignRole</span>(params.user_id, <span class="str">'editor'</span>);
1211
+
1212
+ <span class="cm">// ลบ role</span>
1213
+ <span class="kw">await</span> app.<span class="fn">revokeRole</span>(params.user_id, <span class="str">'editor'</span>);</code></pre>
1214
+ </div>
1215
+ </div>
1216
+
1217
+ <!-- ============================================ -->
1218
+ <!-- ENCRYPTION -->
1219
+ <!-- ============================================ -->
1220
+ <div class="section" id="encryption">
1221
+ <h2>Encryption</h2>
1222
+
1223
+ <div class="card" id="encode">
1224
+ <h3>app.encode() / app.decode()</h3>
1225
+ <div class="sig">app.encode(str) | app.decode(strEncrypted)</div>
1226
+ <p class="desc">RSA encrypt/decrypt string ด้วย private/public key</p>
1227
+ <pre><code><span class="kw">const</span> encrypted = app.<span class="fn">encode</span>(<span class="str">'secret data'</span>);
1228
+ <span class="kw">const</span> decrypted = app.<span class="fn">decode</span>(encrypted);</code></pre>
1229
+ </div>
1230
+
1231
+ <div class="card" id="encodeObj">
1232
+ <h3>app.encodeObj() / app.decodeObj()</h3>
1233
+ <div class="sig">app.encodeObj(obj) | app.decodeObj(payload, publicKey)</div>
1234
+ <p class="desc">AES-256-CBC encrypt/decrypt object (key ถูก RSA encrypt อีกชั้น)</p>
1235
+ <span class="tag tag-return">return encodeObj</span> <code>{ key, iv, data }</code>
1236
+ <pre><code><span class="cm">// Encrypt object</span>
1237
+ <span class="kw">const</span> payload = <span class="kw">await</span> app.<span class="fn">encodeObj</span>({ secret: <span class="str">'data'</span> });
1238
+ <span class="cm">// payload = { key: "RSA encrypted AES key", iv: "hex", data: "AES encrypted" }</span>
1239
+
1240
+ <span class="cm">// Decrypt object</span>
1241
+ <span class="kw">const</span> obj = app.<span class="fn">decodeObj</span>(payload, publicKey);</code></pre>
1242
+ </div>
1243
+
1244
+ <div class="card" id="generateKeyPair">
1245
+ <h3>app.generateKeyPair()</h3>
1246
+ <div class="sig">app.generateKeyPair()</div>
1247
+ <p class="desc">สร้าง RSA key pair ขนาด 2048 bit</p>
1248
+ <span class="tag tag-return">return</span> <code>{ privateKey, publicKey }</code>
1249
+ </div>
1250
+ </div>
1251
+
1252
+ <!-- ============================================ -->
1253
+ <!-- REPORT -->
1254
+ <!-- ============================================ -->
1255
+ <div class="section" id="report">
1256
+ <h2>Report</h2>
1257
+
1258
+ <div class="card" id="wordReport">
1259
+ <h3>app.wordReport()</h3>
1260
+ <div class="sig">app.wordReport(reportData, params, userInfo, subForm?)</div>
1261
+ <p class="desc">สร้าง Word document จาก Report Factory รองรับ table, text, image, QR code</p>
1262
+ <pre><code><span class="cm">// reportData มาจาก module_report</span>
1263
+ <span class="kw">const</span> patches = <span class="kw">await</span> app.<span class="fn">wordReport</span>(
1264
+ reportData, params, userInfo
1265
+ );
1266
+ <span class="cm">// patches = { widget_name: IPatch, ... }</span></code></pre>
1267
+ </div>
1268
+ </div>
1269
+
1270
+ <!-- ============================================ -->
1271
+ <!-- UTILITIES -->
1272
+ <!-- ============================================ -->
1273
+ <div class="section" id="utilities">
1274
+ <h2>Utilities (this.*)</h2>
1275
+ <p class="desc">ฟังก์ชันที่เรียกผ่าน <code>this</code> ใน api_process</p>
1276
+
1277
+ <div class="card" id="isNotNull">
1278
+ <h3>this.isNotNull() / this.isNull()</h3>
1279
+ <div class="sig">this.isNotNull(value) | this.isNull(value)</div>
1280
+ <p class="desc">เช็คว่า value เป็น null/undefined หรือไม่</p>
1281
+ <pre><code><span class="kw">if</span> (<span class="kw">this</span>.<span class="fn">isNotNull</span>(params.name)) {
1282
+ <span class="cm">// มีค่า ไม่ใช่ null/undefined</span>
1283
+ }
1284
+ <span class="kw">if</span> (<span class="kw">this</span>.<span class="fn">isNull</span>(params.email)) {
1285
+ <span class="cm">// เป็น null หรือ undefined</span>
1286
+ }</code></pre>
1287
+ </div>
1288
+
1289
+ <div class="card" id="isEmptyStr">
1290
+ <h3>this.isEmptyStr() / this.isEmptyObj()</h3>
1291
+ <div class="sig">this.isEmptyStr(str) | this.isEmptyObj(obj)</div>
1292
+ <pre><code><span class="kw">this</span>.<span class="fn">isEmptyStr</span>(<span class="str">''</span>) <span class="cm">// true</span>
1293
+ <span class="kw">this</span>.<span class="fn">isEmptyStr</span>(<span class="str">' '</span>) <span class="cm">// true (whitespace)</span>
1294
+ <span class="kw">this</span>.<span class="fn">isEmptyStr</span>(<span class="str">'hello'</span>) <span class="cm">// false</span>
1295
+ <span class="kw">this</span>.<span class="fn">isEmptyObj</span>({}) <span class="cm">// true</span>
1296
+ <span class="kw">this</span>.<span class="fn">isEmptyObj</span>({a:<span class="num">1</span>}) <span class="cm">// false</span></code></pre>
1297
+ </div>
1298
+
1299
+ <div class="card" id="generateId_util">
1300
+ <h3>this.generateId() / genUidTime() / genUUID()</h3>
1301
+ <div class="sig">this.generateId() | this.genUidTime() | this.genUUID()</div>
1302
+ <pre><code><span class="kw">this</span>.<span class="fn">generateId</span>() <span class="cm">// 58291 (random 5 digits)</span>
1303
+ <span class="kw">this</span>.<span class="fn">genUidTime</span>() <span class="cm">// 17441234567891234 (timestamp-based)</span>
1304
+ <span class="kw">this</span>.<span class="fn">genUUID</span>() <span class="cm">// "550e8400-e29b-41d4-a716-446655440000"</span></code></pre>
1305
+ </div>
1306
+
1307
+ <div class="card" id="deepClone">
1308
+ <h3>this.deepClone()</h3>
1309
+ <div class="sig">this.deepClone(object)</div>
1310
+ <p class="desc">Deep clone object (JSON parse/stringify)</p>
1311
+ <pre><code><span class="kw">const</span> copy = <span class="kw">this</span>.<span class="fn">deepClone</span>(params);
1312
+ copy.name = <span class="str">'changed'</span>;
1313
+ <span class="cm">// params.name ไม่เปลี่ยน</span></code></pre>
1314
+ </div>
1315
+
1316
+ <div class="card" id="compareHash">
1317
+ <h3>this.compareHash()</h3>
1318
+ <div class="sig">this.compareHash(hash, value)</div>
1319
+ <p class="desc">เปรียบเทียบ bcrypt hash กับ plain text</p>
1320
+ <pre><code><span class="kw">const</span> match = <span class="kw">await</span> <span class="kw">this</span>.<span class="fn">compareHash</span>(
1321
+ user.password_hash,
1322
+ params.password
1323
+ );
1324
+ <span class="kw">if</span> (!match) {
1325
+ <span class="kw">return</span> { success: <span class="kw">false</span>, message: <span class="str">'รหัสผ่านไม่ถูกต้อง'</span> };
1326
+ }</code></pre>
1327
+ </div>
1328
+
1329
+ <div class="card" id="objectPath">
1330
+ <h3>this.setObjectByPath() / this.getObjectByPath()</h3>
1331
+ <div class="sig">this.setObjectByPath(obj, path, value) | this.getObjectByPath(obj, path)</div>
1332
+ <p class="desc">อ่าน/เขียน nested object ด้วย dot-notation path</p>
1333
+ <pre><code><span class="kw">let</span> obj = { a: { b: { c: <span class="num">10</span> } } };
1334
+
1335
+ <span class="kw">this</span>.<span class="fn">getObjectByPath</span>(obj, <span class="str">'a.b.c'</span>); <span class="cm">// 10</span>
1336
+ <span class="kw">this</span>.<span class="fn">setObjectByPath</span>(obj, <span class="str">'a.b.c'</span>, <span class="num">20</span>); <span class="cm">// obj.a.b.c = 20</span>
1337
+ <span class="kw">this</span>.<span class="fn">setObjectByPath</span>(obj, <span class="str">'x.y'</span>, <span class="num">99</span>); <span class="cm">// สร้าง obj.x.y = 99</span></code></pre>
1338
+ </div>
1339
+
1340
+ <div class="card" id="string2Json">
1341
+ <h3>this.string2Json() / this.string2boolean()</h3>
1342
+ <div class="sig">this.string2Json(jsonStr, defaultNull?) | this.string2boolean(value)</div>
1343
+ <pre><code><span class="kw">this</span>.<span class="fn">string2Json</span>(<span class="str">'{"a":1}'</span>) <span class="cm">// { a: 1 }</span>
1344
+ <span class="kw">this</span>.<span class="fn">string2Json</span>(<span class="str">'invalid'</span>, {}) <span class="cm">// {} (default)</span>
1345
+
1346
+ <span class="kw">this</span>.<span class="fn">string2boolean</span>(<span class="str">'true'</span>) <span class="cm">// true</span>
1347
+ <span class="kw">this</span>.<span class="fn">string2boolean</span>(<span class="str">'1'</span>) <span class="cm">// true</span>
1348
+ <span class="kw">this</span>.<span class="fn">string2boolean</span>(<span class="str">'false'</span>) <span class="cm">// false</span></code></pre>
1349
+ </div>
1350
+
1351
+ <div class="card" id="object2Path">
1352
+ <h3>this.object2Path()</h3>
1353
+ <div class="sig">this.object2Path(object)</div>
1354
+ <p class="desc">แปลง object เป็น template path สำหรับ string substitution</p>
1355
+ <pre><code><span class="kw">const</span> paths = <span class="kw">this</span>.<span class="fn">object2Path</span>({ name: <span class="str">'สมชาย'</span>, age: <span class="num">30</span> });
1356
+ <span class="cm">// { "{{name}}": "สมชาย", "{{age}}": "30" }</span></code></pre>
1357
+ </div>
1358
+ </div>
1359
+
1360
+ <!-- ============================================ -->
1361
+ <!-- INTERFACES -->
1362
+ <!-- ============================================ -->
1363
+ <div class="section" id="interfaces">
1364
+ <h2>Interfaces</h2>
1365
+
1366
+ <div class="card" id="SdProvider">
1367
+ <h3>SdProvider</h3>
1368
+ <pre><code>{
1369
+ providerId: <span class="str">string</span>, <span class="cm">// formId หรือ sqlId</span>
1370
+ providerType: <span class="str">'FORM'</span> | <span class="str">'SQL'</span> | <span class="str">'SYS'</span>,
1371
+ params: <span class="str">object</span>, <span class="cm">// พารามิเตอร์</span>
1372
+ options: { <span class="cm">// ตัวเลือกเพิ่มเติม</span>
1373
+ select: <span class="str">string[]</span>,
1374
+ where: <span class="str">string</span>,
1375
+ orderBy: [{ column: <span class="str">string</span>, sort: <span class="str">'ASC'</span>|<span class="str">'DESC'</span> }],
1376
+ groupBy: <span class="str">string[]</span>,
1377
+ limit: <span class="str">number</span>,
1378
+ page: <span class="str">number</span>
1379
+ }
1380
+ }</code></pre>
1381
+ </div>
1382
+
1383
+ <div class="card" id="SdDataProvider">
1384
+ <h3>SdDataProvider</h3>
1385
+ <pre><code>{
1386
+ from: <span class="str">string</span>, <span class="cm">// ชื่อ collection</span>
1387
+ select: <span class="str">string[]</span>, <span class="cm">// field ที่ต้องการ</span>
1388
+ where: <span class="str">string</span>, <span class="cm">// SQL-like WHERE</span>
1389
+ params: <span class="str">object</span>, <span class="cm">// :paramName substitution</span>
1390
+ search: <span class="str">string[]</span>, <span class="cm">// field ที่ค้นหาด้วย LIKE :q</span>
1391
+ orderBy: [{ column, sort }],
1392
+ groupBy: <span class="str">string[]</span>,
1393
+ join: [{
1394
+ type: <span class="str">'INNER JOIN'</span> | <span class="str">'LEFT JOIN'</span>,
1395
+ hint: <span class="str">''</span> | <span class="str">'FIRST'</span> | <span class="str">'LAST'</span> | <span class="str">'UNWIND'</span> | <span class="str">'OPTIMIZE'</span>,
1396
+ table: <span class="str">string</span>,
1397
+ on: <span class="str">string</span>
1398
+ }],
1399
+ limit: <span class="str">number</span>,
1400
+ page: <span class="str">number</span>,
1401
+ offset: <span class="str">number</span>,
1402
+ nosql: { <span class="cm">// MongoDB native query</span>
1403
+ type: <span class="str">'query'</span> | <span class="str">'aggregate'</span>,
1404
+ collection: <span class="str">string</span>,
1405
+ query: <span class="str">object</span>,
1406
+ projection: <span class="str">object</span>,
1407
+ pipeline: <span class="str">any[]</span>
1408
+ },
1409
+ pgSql: <span class="str">string</span> <span class="cm">// PostgreSQL query</span>
1410
+ }</code></pre>
1411
+ </div>
1412
+
1413
+ <div class="card" id="UserInfo">
1414
+ <h3>UserInfo</h3>
1415
+ <pre><code>{
1416
+ username: <span class="str">string</span>, <span class="cm">// "admin"</span>
1417
+ account: {
1418
+ id: <span class="str">ObjectId</span>, <span class="cm">// user _id</span>
1419
+ name: <span class="str">string</span> <span class="cm">// "SuperAdmin InitAPI (admin@initSDK.com)"</span>
1420
+ },
1421
+ roles: <span class="str">string[]</span>, <span class="cm">// ["super", "admin", "auth", "user"]</span>
1422
+ site: {
1423
+ code: <span class="str">string</span>, <span class="cm">// "00000"</span>
1424
+ name: <span class="str">string</span> <span class="cm">// "Center"</span>
1425
+ },
1426
+ unit: {
1427
+ code: <span class="str">string</span>, <span class="cm">// "00000"</span>
1428
+ name: <span class="str">string</span> <span class="cm">// "Center"</span>
1429
+ }
1430
+ }</code></pre>
1431
+ </div>
1432
+ </div>
1433
+
1434
+ </main>
1435
+
1436
+ <script>
1437
+ // Active sidebar link
1438
+ document.querySelectorAll('.sidebar a').forEach(link => {
1439
+ link.addEventListener('click', () => {
1440
+ document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active'));
1441
+ link.classList.add('active');
1442
+ });
1443
+ });
1444
+
1445
+ // Highlight on scroll
1446
+ const observer = new IntersectionObserver(entries => {
1447
+ entries.forEach(entry => {
1448
+ if (entry.isIntersecting) {
1449
+ const id = entry.target.id;
1450
+ document.querySelectorAll('.sidebar a').forEach(a => {
1451
+ a.classList.toggle('active', a.getAttribute('href') === '#' + id);
1452
+ });
1453
+ }
1454
+ });
1455
+ }, { rootMargin: '-20% 0px -80% 0px' });
1456
+
1457
+ document.querySelectorAll('.card[id], .section[id]').forEach(el => observer.observe(el));
1458
+ </script>
1459
+ </body>
1460
+ </html>