@netoalmanca/advpl-sensei 1.1.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/agents/changelog-generator.md +63 -0
- package/agents/code-generator.md +215 -0
- package/agents/code-reviewer.md +145 -0
- package/agents/debugger.md +83 -0
- package/agents/doc-generator.md +67 -0
- package/agents/docs-reference.md +86 -0
- package/agents/migrator.md +84 -0
- package/agents/process-consultant.md +97 -0
- package/agents/refactorer.md +75 -0
- package/agents/sx-configurator.md +67 -0
- package/commands/changelog.md +66 -0
- package/commands/diagnose.md +67 -0
- package/commands/docs.md +81 -0
- package/commands/document.md +67 -0
- package/commands/explain.md +60 -0
- package/commands/generate.md +111 -0
- package/commands/migrate.md +81 -0
- package/commands/process.md +111 -0
- package/commands/refactor.md +65 -0
- package/commands/review.md +60 -0
- package/commands/sxgen.md +98 -0
- package/commands/test.md +103 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/package.json +30 -0
- package/skills/advpl-code-generation/SKILL.md +163 -0
- package/skills/advpl-code-generation/patterns-fwformbrowse.md +485 -0
- package/skills/advpl-code-generation/patterns-jobs.md +519 -0
- package/skills/advpl-code-generation/patterns-mvc.md +765 -0
- package/skills/advpl-code-generation/patterns-pontos-entrada.md +708 -0
- package/skills/advpl-code-generation/patterns-rest.md +974 -0
- package/skills/advpl-code-generation/patterns-soap.md +639 -0
- package/skills/advpl-code-generation/patterns-treport.md +481 -0
- package/skills/advpl-code-generation/patterns-workflow.md +779 -0
- package/skills/advpl-code-generation/templates-classes.md +1096 -0
- package/skills/advpl-code-review/SKILL.md +72 -0
- package/skills/advpl-code-review/rules-best-practices.md +444 -0
- package/skills/advpl-code-review/rules-modernization.md +290 -0
- package/skills/advpl-code-review/rules-performance.md +333 -0
- package/skills/advpl-code-review/rules-security.md +302 -0
- package/skills/advpl-debugging/SKILL.md +265 -0
- package/skills/advpl-debugging/common-errors.md +1124 -0
- package/skills/advpl-debugging/performance-tips.md +768 -0
- package/skills/advpl-refactoring/SKILL.md +139 -0
- package/skills/advpl-to-tlpp-migration/SKILL.md +293 -0
- package/skills/advpl-to-tlpp-migration/migration-checklist.md +122 -0
- package/skills/advpl-to-tlpp-migration/migration-rules.md +265 -0
- package/skills/changelog-patterns/SKILL.md +99 -0
- package/skills/code-explanation/SKILL.md +66 -0
- package/skills/documentation-patterns/SKILL.md +172 -0
- package/skills/embedded-sql/SKILL.md +379 -0
- package/skills/probat-testing/SKILL.md +226 -0
- package/skills/probat-testing/patterns-unit-tests.md +614 -0
- package/skills/protheus-business/SKILL.md +92 -0
- package/skills/protheus-business/modulo-compras.md +780 -0
- package/skills/protheus-business/modulo-contabilidade.md +874 -0
- package/skills/protheus-business/modulo-estoque.md +876 -0
- package/skills/protheus-business/modulo-faturamento.md +800 -0
- package/skills/protheus-business/modulo-financeiro.md +1015 -0
- package/skills/protheus-business/modulo-fiscal.md +749 -0
- package/skills/protheus-business/modulo-manutencao.md +848 -0
- package/skills/protheus-business/modulo-pcp.md +743 -0
- package/skills/protheus-reference/SKILL.md +119 -0
- package/skills/protheus-reference/native-functions.md +7029 -0
- package/skills/protheus-reference/rest-api-reference.md +1758 -0
- package/skills/protheus-reference/restricted-functions.md +265 -0
- package/skills/protheus-reference/sx-dictionary.md +854 -0
- package/skills/sx-configuration/SKILL.md +184 -0
|
@@ -0,0 +1,1758 @@
|
|
|
1
|
+
# Protheus REST API Reference
|
|
2
|
+
|
|
3
|
+
Reference for implementing and consuming REST APIs in TOTVS Protheus. Covers both the modern FWRest annotation-based framework and the legacy WsRestFul class-based approach.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. REST Server Configuration
|
|
8
|
+
|
|
9
|
+
### appserver.ini Settings
|
|
10
|
+
|
|
11
|
+
To enable the REST server in Protheus, configure the following sections in `appserver.ini`:
|
|
12
|
+
|
|
13
|
+
```ini
|
|
14
|
+
;-----------------------------------------------
|
|
15
|
+
; HTTP REST Configuration
|
|
16
|
+
;-----------------------------------------------
|
|
17
|
+
[HTTPSERVER]
|
|
18
|
+
Enable=1
|
|
19
|
+
Port=8080
|
|
20
|
+
|
|
21
|
+
[HTTPREST]
|
|
22
|
+
Port=8282
|
|
23
|
+
URIs=HTTPURI
|
|
24
|
+
Security=1
|
|
25
|
+
|
|
26
|
+
[HTTPURI]
|
|
27
|
+
URL=/rest
|
|
28
|
+
PrepareIn=ALL
|
|
29
|
+
Instances=2,5,1,1
|
|
30
|
+
CORSAllowOrigin=*
|
|
31
|
+
|
|
32
|
+
[HTTPJOB]
|
|
33
|
+
MAIN=HTTP_START
|
|
34
|
+
ENVIRONMENT=protheus_env
|
|
35
|
+
|
|
36
|
+
[HTTP_START]
|
|
37
|
+
MAIN=HTTP_START
|
|
38
|
+
ENVIRONMENT=protheus_env
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Configuration Parameters
|
|
42
|
+
|
|
43
|
+
| Parameter | Section | Description |
|
|
44
|
+
|-----------|---------|-------------|
|
|
45
|
+
| `Enable` | HTTPSERVER | 1 to enable HTTP server |
|
|
46
|
+
| `Port` | HTTPREST | Port the REST server listens on |
|
|
47
|
+
| `URIs` | HTTPREST | Points to the URI configuration section |
|
|
48
|
+
| `Security` | HTTPREST | 1 to enable authentication |
|
|
49
|
+
| `URL` | HTTPURI | Base URL path for REST endpoints |
|
|
50
|
+
| `PrepareIn` | HTTPURI | Environment name or ALL |
|
|
51
|
+
| `Instances` | HTTPURI | min, max, increment, min_free threads |
|
|
52
|
+
| `CORSAllowOrigin` | HTTPURI | Allowed CORS origins (* for all) |
|
|
53
|
+
|
|
54
|
+
### SSL Configuration
|
|
55
|
+
|
|
56
|
+
```ini
|
|
57
|
+
[HTTPREST]
|
|
58
|
+
Port=443
|
|
59
|
+
SSL2=0
|
|
60
|
+
SSL3=0
|
|
61
|
+
TLS1=1
|
|
62
|
+
CertificateFile=/certs/server.crt
|
|
63
|
+
KeyFile=/certs/server.key
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Verifying the REST Server
|
|
67
|
+
|
|
68
|
+
After starting the Application Server, verify the REST server is running:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
GET http://localhost:8282/rest/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A successful response returns a JSON with available endpoints and API documentation links.
|
|
75
|
+
|
|
76
|
+
You can also check the console output for the message:
|
|
77
|
+
```
|
|
78
|
+
[INFO ][SERVER] REST - Listening on port 8282
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 2. Authentication
|
|
84
|
+
|
|
85
|
+
### Basic Auth
|
|
86
|
+
|
|
87
|
+
Protheus REST supports HTTP Basic Authentication by default when `Security=1` is set in `[HTTPREST]`.
|
|
88
|
+
|
|
89
|
+
```advpl
|
|
90
|
+
// Client-side: calling Protheus REST with Basic Auth
|
|
91
|
+
#Include "TOTVS.CH"
|
|
92
|
+
|
|
93
|
+
User Function CallWithAuth()
|
|
94
|
+
Local cUrl := "http://localhost:8282/rest/myservice"
|
|
95
|
+
Local cUser := "admin"
|
|
96
|
+
Local cPass := "admin123"
|
|
97
|
+
Local cAuth := Encode64(cUser + ":" + cPass)
|
|
98
|
+
Local aHeaders := {}
|
|
99
|
+
Local cResponse := ""
|
|
100
|
+
|
|
101
|
+
aAdd(aHeaders, "Authorization: Basic " + cAuth)
|
|
102
|
+
aAdd(aHeaders, "Content-Type: application/json")
|
|
103
|
+
|
|
104
|
+
cResponse := HttpGet(cUrl, "", "", @aHeaders)
|
|
105
|
+
|
|
106
|
+
If !Empty(cResponse)
|
|
107
|
+
ConOut("Response: " + cResponse)
|
|
108
|
+
EndIf
|
|
109
|
+
Return
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### OAuth2 / Token-Based Authentication
|
|
113
|
+
|
|
114
|
+
Protheus supports token-based authentication through the `/api/oauth2/v1/token` endpoint:
|
|
115
|
+
|
|
116
|
+
```advpl
|
|
117
|
+
#Include "TOTVS.CH"
|
|
118
|
+
|
|
119
|
+
User Function GetToken()
|
|
120
|
+
Local cUrl := "http://localhost:8282/rest/api/oauth2/v1/token"
|
|
121
|
+
Local cPayload := ""
|
|
122
|
+
Local aHeaders := {}
|
|
123
|
+
Local cResponse := ""
|
|
124
|
+
Local oJson := Nil
|
|
125
|
+
|
|
126
|
+
aAdd(aHeaders, "Content-Type: application/x-www-form-urlencoded")
|
|
127
|
+
|
|
128
|
+
cPayload := "grant_type=password"
|
|
129
|
+
cPayload += "&username=admin"
|
|
130
|
+
cPayload += "&password=admin123"
|
|
131
|
+
|
|
132
|
+
cResponse := HttpPost(cUrl, "", cPayload, @aHeaders)
|
|
133
|
+
|
|
134
|
+
If !Empty(cResponse)
|
|
135
|
+
oJson := JsonObject():New()
|
|
136
|
+
oJson:FromJson(cResponse)
|
|
137
|
+
ConOut("Access Token: " + oJson["access_token"])
|
|
138
|
+
FreeObj(oJson)
|
|
139
|
+
EndIf
|
|
140
|
+
Return
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Using a Token in Subsequent Requests
|
|
144
|
+
|
|
145
|
+
```advpl
|
|
146
|
+
#Include "TOTVS.CH"
|
|
147
|
+
|
|
148
|
+
User Function CallWithToken()
|
|
149
|
+
Local cUrl := "http://localhost:8282/rest/api/v1/customers"
|
|
150
|
+
Local cToken := "eyJhbGciOiJIUzI1NiI..." // obtained from GetToken()
|
|
151
|
+
Local aHeaders := {}
|
|
152
|
+
Local cResponse := ""
|
|
153
|
+
|
|
154
|
+
aAdd(aHeaders, "Authorization: Bearer " + cToken)
|
|
155
|
+
aAdd(aHeaders, "Content-Type: application/json")
|
|
156
|
+
|
|
157
|
+
cResponse := HttpGet(cUrl, "", "", @aHeaders)
|
|
158
|
+
|
|
159
|
+
If !Empty(cResponse)
|
|
160
|
+
ConOut("Response: " + cResponse)
|
|
161
|
+
EndIf
|
|
162
|
+
Return
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 3. WsRestFul (Legacy Class-Based)
|
|
168
|
+
|
|
169
|
+
The WsRestFul approach uses the `RestFul.ch` include and class-based declarations. This is the traditional way to create REST services in ADVPL.
|
|
170
|
+
|
|
171
|
+
### Service Declaration
|
|
172
|
+
|
|
173
|
+
```advpl
|
|
174
|
+
#Include "TOTVS.CH"
|
|
175
|
+
#Include "RestFul.ch"
|
|
176
|
+
|
|
177
|
+
WsRestFul CustomerService Description "Customer CRUD Service" Format APPLICATION_JSON
|
|
178
|
+
|
|
179
|
+
WsData id As String
|
|
180
|
+
WsData page As Integer
|
|
181
|
+
WsData pageSize As Integer
|
|
182
|
+
|
|
183
|
+
WsMethod GET Description "List or get customer" WsSyntax "/customers/{id}" Path "/customers"
|
|
184
|
+
WsMethod POST Description "Create customer" WsSyntax "/customers" Path "/customers"
|
|
185
|
+
WsMethod PUT Description "Update customer" WsSyntax "/customers/{id}" Path "/customers"
|
|
186
|
+
WsMethod DELETE Description "Delete customer" WsSyntax "/customers/{id}" Path "/customers"
|
|
187
|
+
|
|
188
|
+
End WsRestFul
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### GET Method Implementation
|
|
192
|
+
|
|
193
|
+
```advpl
|
|
194
|
+
WsMethod GET WsReceive id, page, pageSize WsService CustomerService
|
|
195
|
+
Local oJson := JsonObject():New()
|
|
196
|
+
Local oJsonItem := Nil
|
|
197
|
+
Local aItems := {}
|
|
198
|
+
Local cAlias := "SA1"
|
|
199
|
+
Local nPage := IIf(::page == Nil, 1, ::page)
|
|
200
|
+
Local nPageSize := IIf(::pageSize == Nil, 20, ::pageSize)
|
|
201
|
+
Local nSkip := (nPage - 1) * nPageSize
|
|
202
|
+
Local nCount := 0
|
|
203
|
+
|
|
204
|
+
// If ID is provided, return single customer
|
|
205
|
+
If ::id != Nil .And. !Empty(::id)
|
|
206
|
+
DbSelectArea(cAlias)
|
|
207
|
+
(cAlias)->(DbSetOrder(1))
|
|
208
|
+
|
|
209
|
+
If (cAlias)->(DbSeek(xFilial(cAlias) + ::id))
|
|
210
|
+
oJson["id"] := Alltrim((cAlias)->A1_COD)
|
|
211
|
+
oJson["name"] := Alltrim((cAlias)->A1_NOME)
|
|
212
|
+
oJson["cnpj"] := Alltrim((cAlias)->A1_CGC)
|
|
213
|
+
oJson["email"] := Alltrim((cAlias)->A1_EMAIL)
|
|
214
|
+
|
|
215
|
+
::SetResponse(oJson:ToJson())
|
|
216
|
+
Else
|
|
217
|
+
::SetStatus(404)
|
|
218
|
+
oJson["code"] := "NOT_FOUND"
|
|
219
|
+
oJson["message"] := "Customer not found: " + ::id
|
|
220
|
+
::SetResponse(oJson:ToJson())
|
|
221
|
+
EndIf
|
|
222
|
+
|
|
223
|
+
FreeObj(oJson)
|
|
224
|
+
Return .T.
|
|
225
|
+
EndIf
|
|
226
|
+
|
|
227
|
+
// List customers with pagination
|
|
228
|
+
DbSelectArea(cAlias)
|
|
229
|
+
(cAlias)->(DbSetOrder(1))
|
|
230
|
+
(cAlias)->(DbGoTop())
|
|
231
|
+
|
|
232
|
+
// Skip to requested page
|
|
233
|
+
While nSkip > 0 .And. !(cAlias)->(Eof())
|
|
234
|
+
If (cAlias)->A1_FILIAL == xFilial(cAlias) .And. (cAlias)->(Deleted()) == .F.
|
|
235
|
+
nSkip--
|
|
236
|
+
EndIf
|
|
237
|
+
(cAlias)->(DbSkip())
|
|
238
|
+
EndDo
|
|
239
|
+
|
|
240
|
+
// Collect items for current page
|
|
241
|
+
While !(cAlias)->(Eof()) .And. nCount < nPageSize
|
|
242
|
+
If (cAlias)->A1_FILIAL == xFilial(cAlias) .And. (cAlias)->(Deleted()) == .F.
|
|
243
|
+
oJsonItem := JsonObject():New()
|
|
244
|
+
oJsonItem["id"] := Alltrim((cAlias)->A1_COD)
|
|
245
|
+
oJsonItem["name"] := Alltrim((cAlias)->A1_NOME)
|
|
246
|
+
oJsonItem["cnpj"] := Alltrim((cAlias)->A1_CGC)
|
|
247
|
+
oJsonItem["email"] := Alltrim((cAlias)->A1_EMAIL)
|
|
248
|
+
aAdd(aItems, oJsonItem)
|
|
249
|
+
nCount++
|
|
250
|
+
EndIf
|
|
251
|
+
(cAlias)->(DbSkip())
|
|
252
|
+
EndDo
|
|
253
|
+
|
|
254
|
+
oJson["items"] := aItems
|
|
255
|
+
oJson["page"] := nPage
|
|
256
|
+
oJson["pageSize"] := nPageSize
|
|
257
|
+
oJson["hasNext"] := !(cAlias)->(Eof())
|
|
258
|
+
|
|
259
|
+
::SetResponse(oJson:ToJson())
|
|
260
|
+
|
|
261
|
+
FreeObj(oJson)
|
|
262
|
+
Return .T.
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### POST Method Implementation
|
|
266
|
+
|
|
267
|
+
```advpl
|
|
268
|
+
WsMethod POST WsService CustomerService
|
|
269
|
+
Local cBody := ::GetContent()
|
|
270
|
+
Local oJson := JsonObject():New()
|
|
271
|
+
Local oResp := JsonObject():New()
|
|
272
|
+
Local cAlias := "SA1"
|
|
273
|
+
Local lSuccess := .F.
|
|
274
|
+
|
|
275
|
+
If Empty(cBody)
|
|
276
|
+
::SetStatus(400)
|
|
277
|
+
oResp["code"] := "BAD_REQUEST"
|
|
278
|
+
oResp["message"] := "Request body is required"
|
|
279
|
+
::SetResponse(oResp:ToJson())
|
|
280
|
+
FreeObj(oJson)
|
|
281
|
+
FreeObj(oResp)
|
|
282
|
+
Return .F.
|
|
283
|
+
EndIf
|
|
284
|
+
|
|
285
|
+
oJson:FromJson(cBody)
|
|
286
|
+
|
|
287
|
+
// Validate required fields
|
|
288
|
+
If oJson["name"] == Nil .Or. Empty(oJson["name"])
|
|
289
|
+
::SetStatus(400)
|
|
290
|
+
oResp["code"] := "VALIDATION_ERROR"
|
|
291
|
+
oResp["message"] := "Field 'name' is required"
|
|
292
|
+
::SetResponse(oResp:ToJson())
|
|
293
|
+
FreeObj(oJson)
|
|
294
|
+
FreeObj(oResp)
|
|
295
|
+
Return .F.
|
|
296
|
+
EndIf
|
|
297
|
+
|
|
298
|
+
DbSelectArea(cAlias)
|
|
299
|
+
(cAlias)->(DbSetOrder(1))
|
|
300
|
+
|
|
301
|
+
Begin Transaction
|
|
302
|
+
|
|
303
|
+
If RecLock(cAlias, .T.) // .T. = insert new record
|
|
304
|
+
(cAlias)->A1_FILIAL := xFilial(cAlias)
|
|
305
|
+
(cAlias)->A1_COD := GetSXENum("SA1", "A1_COD")
|
|
306
|
+
(cAlias)->A1_LOJA := "01"
|
|
307
|
+
(cAlias)->A1_NOME := oJson["name"]
|
|
308
|
+
(cAlias)->A1_CGC := IIf(oJson["cnpj"] != Nil, oJson["cnpj"], "")
|
|
309
|
+
(cAlias)->A1_EMAIL := IIf(oJson["email"] != Nil, oJson["email"], "")
|
|
310
|
+
MsUnlock()
|
|
311
|
+
ConfirmSX8()
|
|
312
|
+
lSuccess := .T.
|
|
313
|
+
Else
|
|
314
|
+
DisarmTransaction()
|
|
315
|
+
::SetStatus(500)
|
|
316
|
+
oResp["code"] := "INTERNAL_ERROR"
|
|
317
|
+
oResp["message"] := "Failed to lock record for insertion"
|
|
318
|
+
::SetResponse(oResp:ToJson())
|
|
319
|
+
EndIf
|
|
320
|
+
|
|
321
|
+
End Transaction
|
|
322
|
+
|
|
323
|
+
If lSuccess
|
|
324
|
+
::SetStatus(201)
|
|
325
|
+
oResp["id"] := Alltrim((cAlias)->A1_COD)
|
|
326
|
+
oResp["name"] := Alltrim((cAlias)->A1_NOME)
|
|
327
|
+
oResp["message"] := "Customer created successfully"
|
|
328
|
+
::SetResponse(oResp:ToJson())
|
|
329
|
+
EndIf
|
|
330
|
+
|
|
331
|
+
FreeObj(oJson)
|
|
332
|
+
FreeObj(oResp)
|
|
333
|
+
Return lSuccess
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### PUT Method Implementation
|
|
337
|
+
|
|
338
|
+
```advpl
|
|
339
|
+
WsMethod PUT WsReceive id WsService CustomerService
|
|
340
|
+
Local cBody := ::GetContent()
|
|
341
|
+
Local oJson := JsonObject():New()
|
|
342
|
+
Local oResp := JsonObject():New()
|
|
343
|
+
Local cAlias := "SA1"
|
|
344
|
+
Local lSuccess := .F.
|
|
345
|
+
|
|
346
|
+
If ::id == Nil .Or. Empty(::id)
|
|
347
|
+
::SetStatus(400)
|
|
348
|
+
oResp["code"] := "BAD_REQUEST"
|
|
349
|
+
oResp["message"] := "Customer ID is required"
|
|
350
|
+
::SetResponse(oResp:ToJson())
|
|
351
|
+
FreeObj(oJson)
|
|
352
|
+
FreeObj(oResp)
|
|
353
|
+
Return .F.
|
|
354
|
+
EndIf
|
|
355
|
+
|
|
356
|
+
If Empty(cBody)
|
|
357
|
+
::SetStatus(400)
|
|
358
|
+
oResp["code"] := "BAD_REQUEST"
|
|
359
|
+
oResp["message"] := "Request body is required"
|
|
360
|
+
::SetResponse(oResp:ToJson())
|
|
361
|
+
FreeObj(oJson)
|
|
362
|
+
FreeObj(oResp)
|
|
363
|
+
Return .F.
|
|
364
|
+
EndIf
|
|
365
|
+
|
|
366
|
+
oJson:FromJson(cBody)
|
|
367
|
+
|
|
368
|
+
DbSelectArea(cAlias)
|
|
369
|
+
(cAlias)->(DbSetOrder(1))
|
|
370
|
+
|
|
371
|
+
If !(cAlias)->(DbSeek(xFilial(cAlias) + ::id))
|
|
372
|
+
::SetStatus(404)
|
|
373
|
+
oResp["code"] := "NOT_FOUND"
|
|
374
|
+
oResp["message"] := "Customer not found: " + ::id
|
|
375
|
+
::SetResponse(oResp:ToJson())
|
|
376
|
+
FreeObj(oJson)
|
|
377
|
+
FreeObj(oResp)
|
|
378
|
+
Return .F.
|
|
379
|
+
EndIf
|
|
380
|
+
|
|
381
|
+
Begin Transaction
|
|
382
|
+
|
|
383
|
+
If RecLock(cAlias, .F.) // .F. = update existing record
|
|
384
|
+
If oJson["name"] != Nil
|
|
385
|
+
(cAlias)->A1_NOME := oJson["name"]
|
|
386
|
+
EndIf
|
|
387
|
+
If oJson["cnpj"] != Nil
|
|
388
|
+
(cAlias)->A1_CGC := oJson["cnpj"]
|
|
389
|
+
EndIf
|
|
390
|
+
If oJson["email"] != Nil
|
|
391
|
+
(cAlias)->A1_EMAIL := oJson["email"]
|
|
392
|
+
EndIf
|
|
393
|
+
MsUnlock()
|
|
394
|
+
lSuccess := .T.
|
|
395
|
+
Else
|
|
396
|
+
DisarmTransaction()
|
|
397
|
+
::SetStatus(500)
|
|
398
|
+
oResp["code"] := "INTERNAL_ERROR"
|
|
399
|
+
oResp["message"] := "Failed to lock record for update"
|
|
400
|
+
::SetResponse(oResp:ToJson())
|
|
401
|
+
EndIf
|
|
402
|
+
|
|
403
|
+
End Transaction
|
|
404
|
+
|
|
405
|
+
If lSuccess
|
|
406
|
+
oResp["id"] := Alltrim((cAlias)->A1_COD)
|
|
407
|
+
oResp["name"] := Alltrim((cAlias)->A1_NOME)
|
|
408
|
+
oResp["message"] := "Customer updated successfully"
|
|
409
|
+
::SetResponse(oResp:ToJson())
|
|
410
|
+
EndIf
|
|
411
|
+
|
|
412
|
+
FreeObj(oJson)
|
|
413
|
+
FreeObj(oResp)
|
|
414
|
+
Return lSuccess
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### DELETE Method Implementation
|
|
418
|
+
|
|
419
|
+
```advpl
|
|
420
|
+
WsMethod DELETE WsReceive id WsService CustomerService
|
|
421
|
+
Local oResp := JsonObject():New()
|
|
422
|
+
Local cAlias := "SA1"
|
|
423
|
+
Local lSuccess := .F.
|
|
424
|
+
|
|
425
|
+
If ::id == Nil .Or. Empty(::id)
|
|
426
|
+
::SetStatus(400)
|
|
427
|
+
oResp["code"] := "BAD_REQUEST"
|
|
428
|
+
oResp["message"] := "Customer ID is required"
|
|
429
|
+
::SetResponse(oResp:ToJson())
|
|
430
|
+
FreeObj(oResp)
|
|
431
|
+
Return .F.
|
|
432
|
+
EndIf
|
|
433
|
+
|
|
434
|
+
DbSelectArea(cAlias)
|
|
435
|
+
(cAlias)->(DbSetOrder(1))
|
|
436
|
+
|
|
437
|
+
If !(cAlias)->(DbSeek(xFilial(cAlias) + ::id))
|
|
438
|
+
::SetStatus(404)
|
|
439
|
+
oResp["code"] := "NOT_FOUND"
|
|
440
|
+
oResp["message"] := "Customer not found: " + ::id
|
|
441
|
+
::SetResponse(oResp:ToJson())
|
|
442
|
+
FreeObj(oResp)
|
|
443
|
+
Return .F.
|
|
444
|
+
EndIf
|
|
445
|
+
|
|
446
|
+
Begin Transaction
|
|
447
|
+
|
|
448
|
+
If RecLock(cAlias, .F.)
|
|
449
|
+
DbDelete()
|
|
450
|
+
MsUnlock()
|
|
451
|
+
lSuccess := .T.
|
|
452
|
+
Else
|
|
453
|
+
DisarmTransaction()
|
|
454
|
+
::SetStatus(500)
|
|
455
|
+
oResp["code"] := "INTERNAL_ERROR"
|
|
456
|
+
oResp["message"] := "Failed to lock record for deletion"
|
|
457
|
+
::SetResponse(oResp:ToJson())
|
|
458
|
+
EndIf
|
|
459
|
+
|
|
460
|
+
End Transaction
|
|
461
|
+
|
|
462
|
+
If lSuccess
|
|
463
|
+
oResp["id"] := ::id
|
|
464
|
+
oResp["message"] := "Customer deleted successfully"
|
|
465
|
+
::SetResponse(oResp:ToJson())
|
|
466
|
+
EndIf
|
|
467
|
+
|
|
468
|
+
FreeObj(oResp)
|
|
469
|
+
Return lSuccess
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## 4. TLPP REST (Annotation-Based / Modern)
|
|
475
|
+
|
|
476
|
+
The modern TLPP framework uses annotations to define REST endpoints. This approach is cleaner, more maintainable, and aligns with contemporary API design.
|
|
477
|
+
|
|
478
|
+
### Complete Service with Annotations
|
|
479
|
+
|
|
480
|
+
```tlpp
|
|
481
|
+
#Include "tlpp-core.th"
|
|
482
|
+
#Include "tlpp-rest.th"
|
|
483
|
+
|
|
484
|
+
@RestService("/api/v1/customers")
|
|
485
|
+
class CustomerAPI from LongClassName
|
|
486
|
+
|
|
487
|
+
@Get("")
|
|
488
|
+
public method list()
|
|
489
|
+
|
|
490
|
+
@Get("/:id")
|
|
491
|
+
public method getById()
|
|
492
|
+
|
|
493
|
+
@Post("")
|
|
494
|
+
public method create()
|
|
495
|
+
|
|
496
|
+
@Put("/:id")
|
|
497
|
+
public method update()
|
|
498
|
+
|
|
499
|
+
@Delete("/:id")
|
|
500
|
+
public method remove()
|
|
501
|
+
|
|
502
|
+
endclass
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### GET - List Method
|
|
506
|
+
|
|
507
|
+
```tlpp
|
|
508
|
+
method list() class CustomerAPI
|
|
509
|
+
local oJson := JsonObject():New()
|
|
510
|
+
local aItems := {}
|
|
511
|
+
local oItem := nil
|
|
512
|
+
local cAlias := "SA1"
|
|
513
|
+
local nPage := val(self:getQueryString("page", "1"))
|
|
514
|
+
local nPageSize := val(self:getQueryString("pageSize", "20"))
|
|
515
|
+
local nSkip := (nPage - 1) * nPageSize
|
|
516
|
+
local nCount := 0
|
|
517
|
+
|
|
518
|
+
DbSelectArea(cAlias)
|
|
519
|
+
(cAlias)->(DbSetOrder(1))
|
|
520
|
+
(cAlias)->(DbGoTop())
|
|
521
|
+
|
|
522
|
+
// Skip records for pagination
|
|
523
|
+
while nSkip > 0 .and. !(cAlias)->(Eof())
|
|
524
|
+
if (cAlias)->A1_FILIAL == xFilial(cAlias) .and. (cAlias)->(Deleted()) == .F.
|
|
525
|
+
nSkip--
|
|
526
|
+
endif
|
|
527
|
+
(cAlias)->(DbSkip())
|
|
528
|
+
enddo
|
|
529
|
+
|
|
530
|
+
// Collect page items
|
|
531
|
+
while !(cAlias)->(Eof()) .and. nCount < nPageSize
|
|
532
|
+
if (cAlias)->A1_FILIAL == xFilial(cAlias) .and. (cAlias)->(Deleted()) == .F.
|
|
533
|
+
oItem := JsonObject():New()
|
|
534
|
+
oItem["id"] := alltrim((cAlias)->A1_COD)
|
|
535
|
+
oItem["store"] := alltrim((cAlias)->A1_LOJA)
|
|
536
|
+
oItem["name"] := alltrim((cAlias)->A1_NOME)
|
|
537
|
+
oItem["cnpj"] := alltrim((cAlias)->A1_CGC)
|
|
538
|
+
oItem["email"] := alltrim((cAlias)->A1_EMAIL)
|
|
539
|
+
aAdd(aItems, oItem)
|
|
540
|
+
nCount++
|
|
541
|
+
endif
|
|
542
|
+
(cAlias)->(DbSkip())
|
|
543
|
+
enddo
|
|
544
|
+
|
|
545
|
+
oJson["items"] := aItems
|
|
546
|
+
oJson["page"] := nPage
|
|
547
|
+
oJson["pageSize"] := nPageSize
|
|
548
|
+
oJson["hasNext"] := !(cAlias)->(Eof())
|
|
549
|
+
|
|
550
|
+
self:setStatus(200)
|
|
551
|
+
self:setResponse(oJson:toJson())
|
|
552
|
+
|
|
553
|
+
FreeObj(oJson)
|
|
554
|
+
return
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### GET - Get By ID Method
|
|
558
|
+
|
|
559
|
+
```tlpp
|
|
560
|
+
method getById() class CustomerAPI
|
|
561
|
+
local oJson := JsonObject():New()
|
|
562
|
+
local cId := self:getPathParam("id")
|
|
563
|
+
local cAlias := "SA1"
|
|
564
|
+
|
|
565
|
+
if empty(cId)
|
|
566
|
+
self:setStatus(400)
|
|
567
|
+
oJson["code"] := "BAD_REQUEST"
|
|
568
|
+
oJson["message"] := "Customer ID is required"
|
|
569
|
+
self:setResponse(oJson:toJson())
|
|
570
|
+
FreeObj(oJson)
|
|
571
|
+
return
|
|
572
|
+
endif
|
|
573
|
+
|
|
574
|
+
DbSelectArea(cAlias)
|
|
575
|
+
(cAlias)->(DbSetOrder(1))
|
|
576
|
+
|
|
577
|
+
if (cAlias)->(DbSeek(xFilial(cAlias) + cId))
|
|
578
|
+
oJson["id"] := alltrim((cAlias)->A1_COD)
|
|
579
|
+
oJson["store"] := alltrim((cAlias)->A1_LOJA)
|
|
580
|
+
oJson["name"] := alltrim((cAlias)->A1_NOME)
|
|
581
|
+
oJson["cnpj"] := alltrim((cAlias)->A1_CGC)
|
|
582
|
+
oJson["email"] := alltrim((cAlias)->A1_EMAIL)
|
|
583
|
+
|
|
584
|
+
self:setStatus(200)
|
|
585
|
+
self:setResponse(oJson:toJson())
|
|
586
|
+
else
|
|
587
|
+
self:setStatus(404)
|
|
588
|
+
oJson["code"] := "NOT_FOUND"
|
|
589
|
+
oJson["message"] := "Customer not found: " + cId
|
|
590
|
+
self:setResponse(oJson:toJson())
|
|
591
|
+
endif
|
|
592
|
+
|
|
593
|
+
FreeObj(oJson)
|
|
594
|
+
return
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### POST - Create Method
|
|
598
|
+
|
|
599
|
+
```tlpp
|
|
600
|
+
method create() class CustomerAPI
|
|
601
|
+
local cBody := self:getContent()
|
|
602
|
+
local oJson := JsonObject():New()
|
|
603
|
+
local oResp := JsonObject():New()
|
|
604
|
+
local cAlias := "SA1"
|
|
605
|
+
local lSuccess := .F.
|
|
606
|
+
|
|
607
|
+
if empty(cBody)
|
|
608
|
+
self:setStatus(400)
|
|
609
|
+
oResp["code"] := "BAD_REQUEST"
|
|
610
|
+
oResp["message"] := "Request body is required"
|
|
611
|
+
self:setResponse(oResp:toJson())
|
|
612
|
+
FreeObj(oJson)
|
|
613
|
+
FreeObj(oResp)
|
|
614
|
+
return
|
|
615
|
+
endif
|
|
616
|
+
|
|
617
|
+
oJson:fromJson(cBody)
|
|
618
|
+
|
|
619
|
+
// Validate required fields
|
|
620
|
+
if oJson["name"] == nil .or. empty(oJson["name"])
|
|
621
|
+
self:setStatus(400)
|
|
622
|
+
oResp["code"] := "VALIDATION_ERROR"
|
|
623
|
+
oResp["message"] := "Field 'name' is required"
|
|
624
|
+
self:setResponse(oResp:toJson())
|
|
625
|
+
FreeObj(oJson)
|
|
626
|
+
FreeObj(oResp)
|
|
627
|
+
return
|
|
628
|
+
endif
|
|
629
|
+
|
|
630
|
+
DbSelectArea(cAlias)
|
|
631
|
+
(cAlias)->(DbSetOrder(1))
|
|
632
|
+
|
|
633
|
+
begin transaction
|
|
634
|
+
|
|
635
|
+
if RecLock(cAlias, .T.)
|
|
636
|
+
(cAlias)->A1_FILIAL := xFilial(cAlias)
|
|
637
|
+
(cAlias)->A1_COD := GetSXENum("SA1", "A1_COD")
|
|
638
|
+
(cAlias)->A1_LOJA := "01"
|
|
639
|
+
(cAlias)->A1_NOME := oJson["name"]
|
|
640
|
+
(cAlias)->A1_CGC := iif(oJson["cnpj"] != nil, oJson["cnpj"], "")
|
|
641
|
+
(cAlias)->A1_EMAIL := iif(oJson["email"] != nil, oJson["email"], "")
|
|
642
|
+
MsUnlock()
|
|
643
|
+
ConfirmSX8()
|
|
644
|
+
lSuccess := .T.
|
|
645
|
+
else
|
|
646
|
+
DisarmTransaction()
|
|
647
|
+
self:setStatus(500)
|
|
648
|
+
oResp["code"] := "INTERNAL_ERROR"
|
|
649
|
+
oResp["message"] := "Failed to lock record for insertion"
|
|
650
|
+
self:setResponse(oResp:toJson())
|
|
651
|
+
endif
|
|
652
|
+
|
|
653
|
+
end transaction
|
|
654
|
+
|
|
655
|
+
if lSuccess
|
|
656
|
+
self:setStatus(201)
|
|
657
|
+
oResp["id"] := alltrim((cAlias)->A1_COD)
|
|
658
|
+
oResp["name"] := alltrim((cAlias)->A1_NOME)
|
|
659
|
+
oResp["message"] := "Customer created successfully"
|
|
660
|
+
self:setResponse(oResp:toJson())
|
|
661
|
+
endif
|
|
662
|
+
|
|
663
|
+
FreeObj(oJson)
|
|
664
|
+
FreeObj(oResp)
|
|
665
|
+
return
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### PUT - Update Method
|
|
669
|
+
|
|
670
|
+
```tlpp
|
|
671
|
+
method update() class CustomerAPI
|
|
672
|
+
local cBody := self:getContent()
|
|
673
|
+
local cId := self:getPathParam("id")
|
|
674
|
+
local oJson := JsonObject():New()
|
|
675
|
+
local oResp := JsonObject():New()
|
|
676
|
+
local cAlias := "SA1"
|
|
677
|
+
local lSuccess := .F.
|
|
678
|
+
|
|
679
|
+
if empty(cId)
|
|
680
|
+
self:setStatus(400)
|
|
681
|
+
oResp["code"] := "BAD_REQUEST"
|
|
682
|
+
oResp["message"] := "Customer ID is required"
|
|
683
|
+
self:setResponse(oResp:toJson())
|
|
684
|
+
FreeObj(oJson)
|
|
685
|
+
FreeObj(oResp)
|
|
686
|
+
return
|
|
687
|
+
endif
|
|
688
|
+
|
|
689
|
+
if empty(cBody)
|
|
690
|
+
self:setStatus(400)
|
|
691
|
+
oResp["code"] := "BAD_REQUEST"
|
|
692
|
+
oResp["message"] := "Request body is required"
|
|
693
|
+
self:setResponse(oResp:toJson())
|
|
694
|
+
FreeObj(oJson)
|
|
695
|
+
FreeObj(oResp)
|
|
696
|
+
return
|
|
697
|
+
endif
|
|
698
|
+
|
|
699
|
+
oJson:fromJson(cBody)
|
|
700
|
+
|
|
701
|
+
DbSelectArea(cAlias)
|
|
702
|
+
(cAlias)->(DbSetOrder(1))
|
|
703
|
+
|
|
704
|
+
if !(cAlias)->(DbSeek(xFilial(cAlias) + cId))
|
|
705
|
+
self:setStatus(404)
|
|
706
|
+
oResp["code"] := "NOT_FOUND"
|
|
707
|
+
oResp["message"] := "Customer not found: " + cId
|
|
708
|
+
self:setResponse(oResp:toJson())
|
|
709
|
+
FreeObj(oJson)
|
|
710
|
+
FreeObj(oResp)
|
|
711
|
+
return
|
|
712
|
+
endif
|
|
713
|
+
|
|
714
|
+
begin transaction
|
|
715
|
+
|
|
716
|
+
if RecLock(cAlias, .F.)
|
|
717
|
+
if oJson["name"] != nil
|
|
718
|
+
(cAlias)->A1_NOME := oJson["name"]
|
|
719
|
+
endif
|
|
720
|
+
if oJson["cnpj"] != nil
|
|
721
|
+
(cAlias)->A1_CGC := oJson["cnpj"]
|
|
722
|
+
endif
|
|
723
|
+
if oJson["email"] != nil
|
|
724
|
+
(cAlias)->A1_EMAIL := oJson["email"]
|
|
725
|
+
endif
|
|
726
|
+
MsUnlock()
|
|
727
|
+
lSuccess := .T.
|
|
728
|
+
else
|
|
729
|
+
DisarmTransaction()
|
|
730
|
+
self:setStatus(500)
|
|
731
|
+
oResp["code"] := "INTERNAL_ERROR"
|
|
732
|
+
oResp["message"] := "Failed to lock record for update"
|
|
733
|
+
self:setResponse(oResp:toJson())
|
|
734
|
+
endif
|
|
735
|
+
|
|
736
|
+
end transaction
|
|
737
|
+
|
|
738
|
+
if lSuccess
|
|
739
|
+
self:setStatus(200)
|
|
740
|
+
oResp["id"] := alltrim((cAlias)->A1_COD)
|
|
741
|
+
oResp["name"] := alltrim((cAlias)->A1_NOME)
|
|
742
|
+
oResp["message"] := "Customer updated successfully"
|
|
743
|
+
self:setResponse(oResp:toJson())
|
|
744
|
+
endif
|
|
745
|
+
|
|
746
|
+
FreeObj(oJson)
|
|
747
|
+
FreeObj(oResp)
|
|
748
|
+
return
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### DELETE - Remove Method
|
|
752
|
+
|
|
753
|
+
```tlpp
|
|
754
|
+
method remove() class CustomerAPI
|
|
755
|
+
local cId := self:getPathParam("id")
|
|
756
|
+
local oResp := JsonObject():New()
|
|
757
|
+
local cAlias := "SA1"
|
|
758
|
+
local lSuccess := .F.
|
|
759
|
+
|
|
760
|
+
if empty(cId)
|
|
761
|
+
self:setStatus(400)
|
|
762
|
+
oResp["code"] := "BAD_REQUEST"
|
|
763
|
+
oResp["message"] := "Customer ID is required"
|
|
764
|
+
self:setResponse(oResp:toJson())
|
|
765
|
+
FreeObj(oResp)
|
|
766
|
+
return
|
|
767
|
+
endif
|
|
768
|
+
|
|
769
|
+
DbSelectArea(cAlias)
|
|
770
|
+
(cAlias)->(DbSetOrder(1))
|
|
771
|
+
|
|
772
|
+
if !(cAlias)->(DbSeek(xFilial(cAlias) + cId))
|
|
773
|
+
self:setStatus(404)
|
|
774
|
+
oResp["code"] := "NOT_FOUND"
|
|
775
|
+
oResp["message"] := "Customer not found: " + cId
|
|
776
|
+
self:setResponse(oResp:toJson())
|
|
777
|
+
FreeObj(oResp)
|
|
778
|
+
return
|
|
779
|
+
endif
|
|
780
|
+
|
|
781
|
+
begin transaction
|
|
782
|
+
|
|
783
|
+
if RecLock(cAlias, .F.)
|
|
784
|
+
DbDelete()
|
|
785
|
+
MsUnlock()
|
|
786
|
+
lSuccess := .T.
|
|
787
|
+
else
|
|
788
|
+
DisarmTransaction()
|
|
789
|
+
self:setStatus(500)
|
|
790
|
+
oResp["code"] := "INTERNAL_ERROR"
|
|
791
|
+
oResp["message"] := "Failed to lock record for deletion"
|
|
792
|
+
self:setResponse(oResp:toJson())
|
|
793
|
+
endif
|
|
794
|
+
|
|
795
|
+
end transaction
|
|
796
|
+
|
|
797
|
+
if lSuccess
|
|
798
|
+
self:setStatus(200)
|
|
799
|
+
oResp["id"] := cId
|
|
800
|
+
oResp["message"] := "Customer deleted successfully"
|
|
801
|
+
self:setResponse(oResp:toJson())
|
|
802
|
+
endif
|
|
803
|
+
|
|
804
|
+
FreeObj(oResp)
|
|
805
|
+
return
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
## 5. FWRest / FWRestModel
|
|
811
|
+
|
|
812
|
+
FWRestModel provides automatic CRUD operations based on existing MVC models, significantly reducing boilerplate code.
|
|
813
|
+
|
|
814
|
+
### Basic FWRestModel Setup
|
|
815
|
+
|
|
816
|
+
```advpl
|
|
817
|
+
#Include "TOTVS.CH"
|
|
818
|
+
#Include "RestFul.ch"
|
|
819
|
+
#Include "FWMVCDef.ch"
|
|
820
|
+
|
|
821
|
+
WsRestFul CustomerModel Description "Customer REST via FWRestModel" Format APPLICATION_JSON
|
|
822
|
+
|
|
823
|
+
WsMethod GET Description "List/Get customer" WsSyntax "/customermodel/{id}" Path "/customermodel"
|
|
824
|
+
WsMethod POST Description "Create customer" WsSyntax "/customermodel" Path "/customermodel"
|
|
825
|
+
WsMethod PUT Description "Update customer" WsSyntax "/customermodel/{id}" Path "/customermodel"
|
|
826
|
+
WsMethod DELETE Description "Delete customer" WsSyntax "/customermodel/{id}" Path "/customermodel"
|
|
827
|
+
|
|
828
|
+
End WsRestFul
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### FWRestModel GET Implementation
|
|
832
|
+
|
|
833
|
+
```advpl
|
|
834
|
+
WsMethod GET WsService CustomerModel
|
|
835
|
+
Local oRestModel := FWRestModel():New("COMP011_MVC") // MVC model ID
|
|
836
|
+
Local lRet := .T.
|
|
837
|
+
|
|
838
|
+
// Configure the model
|
|
839
|
+
oRestModel:SetQuery(.T.) // Enable query support
|
|
840
|
+
oRestModel:SetPagination(.T.) // Enable pagination
|
|
841
|
+
oRestModel:SetFields(.T.) // Enable field selection
|
|
842
|
+
|
|
843
|
+
// Process GET request
|
|
844
|
+
lRet := oRestModel:GetData()
|
|
845
|
+
|
|
846
|
+
::SetStatus(oRestModel:GetStatus())
|
|
847
|
+
::SetResponse(oRestModel:GetResponse())
|
|
848
|
+
|
|
849
|
+
FreeObj(oRestModel)
|
|
850
|
+
Return lRet
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### FWRestModel POST Implementation
|
|
854
|
+
|
|
855
|
+
```advpl
|
|
856
|
+
WsMethod POST WsService CustomerModel
|
|
857
|
+
Local oRestModel := FWRestModel():New("COMP011_MVC")
|
|
858
|
+
Local cBody := ::GetContent()
|
|
859
|
+
Local lRet := .T.
|
|
860
|
+
|
|
861
|
+
oRestModel:SetContent(cBody)
|
|
862
|
+
lRet := oRestModel:PostData()
|
|
863
|
+
|
|
864
|
+
::SetStatus(oRestModel:GetStatus())
|
|
865
|
+
::SetResponse(oRestModel:GetResponse())
|
|
866
|
+
|
|
867
|
+
FreeObj(oRestModel)
|
|
868
|
+
Return lRet
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### FWRestModel PUT Implementation
|
|
872
|
+
|
|
873
|
+
```advpl
|
|
874
|
+
WsMethod PUT WsService CustomerModel
|
|
875
|
+
Local oRestModel := FWRestModel():New("COMP011_MVC")
|
|
876
|
+
Local cBody := ::GetContent()
|
|
877
|
+
Local lRet := .T.
|
|
878
|
+
|
|
879
|
+
oRestModel:SetContent(cBody)
|
|
880
|
+
lRet := oRestModel:PutData()
|
|
881
|
+
|
|
882
|
+
::SetStatus(oRestModel:GetStatus())
|
|
883
|
+
::SetResponse(oRestModel:GetResponse())
|
|
884
|
+
|
|
885
|
+
FreeObj(oRestModel)
|
|
886
|
+
Return lRet
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### FWRestModel DELETE Implementation
|
|
890
|
+
|
|
891
|
+
```advpl
|
|
892
|
+
WsMethod DELETE WsService CustomerModel
|
|
893
|
+
Local oRestModel := FWRestModel():New("COMP011_MVC")
|
|
894
|
+
Local lRet := .T.
|
|
895
|
+
|
|
896
|
+
lRet := oRestModel:DeleteData()
|
|
897
|
+
|
|
898
|
+
::SetStatus(oRestModel:GetStatus())
|
|
899
|
+
::SetResponse(oRestModel:GetResponse())
|
|
900
|
+
|
|
901
|
+
FreeObj(oRestModel)
|
|
902
|
+
Return lRet
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
### Customizing FWRestModel Behavior
|
|
906
|
+
|
|
907
|
+
```advpl
|
|
908
|
+
WsMethod GET WsService CustomerModel
|
|
909
|
+
Local oRestModel := FWRestModel():New("COMP011_MVC")
|
|
910
|
+
Local lRet := .T.
|
|
911
|
+
|
|
912
|
+
// Custom field mapping (API field name -> DB field name)
|
|
913
|
+
oRestModel:AddField("id", "A1_COD")
|
|
914
|
+
oRestModel:AddField("name", "A1_NOME")
|
|
915
|
+
oRestModel:AddField("cnpj", "A1_CGC")
|
|
916
|
+
oRestModel:AddField("email", "A1_EMAIL")
|
|
917
|
+
|
|
918
|
+
// Set default order
|
|
919
|
+
oRestModel:SetOrder("A1_NOME")
|
|
920
|
+
|
|
921
|
+
// Set custom filter
|
|
922
|
+
oRestModel:SetWhere("A1_TIPO = '1'") // Only type 1 customers
|
|
923
|
+
|
|
924
|
+
// Enable pagination with custom page size
|
|
925
|
+
oRestModel:SetPagination(.T.)
|
|
926
|
+
oRestModel:SetPageSize(50)
|
|
927
|
+
|
|
928
|
+
lRet := oRestModel:GetData()
|
|
929
|
+
|
|
930
|
+
::SetStatus(oRestModel:GetStatus())
|
|
931
|
+
::SetResponse(oRestModel:GetResponse())
|
|
932
|
+
|
|
933
|
+
FreeObj(oRestModel)
|
|
934
|
+
Return lRet
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
## 6. JSON Handling
|
|
940
|
+
|
|
941
|
+
### JsonObject Class
|
|
942
|
+
|
|
943
|
+
The `JsonObject` class is the primary way to work with JSON in ADVPL/TLPP.
|
|
944
|
+
|
|
945
|
+
#### Creating JSON
|
|
946
|
+
|
|
947
|
+
```advpl
|
|
948
|
+
#Include "TOTVS.CH"
|
|
949
|
+
|
|
950
|
+
User Function JsonCreate()
|
|
951
|
+
Local oJson := JsonObject():New()
|
|
952
|
+
|
|
953
|
+
// Simple values
|
|
954
|
+
oJson["name"] := "TOTVS S.A."
|
|
955
|
+
oJson["code"] := 12345
|
|
956
|
+
oJson["active"] := .T.
|
|
957
|
+
oJson["rate"] := 99.90
|
|
958
|
+
|
|
959
|
+
// Convert to string
|
|
960
|
+
ConOut(oJson:ToJson())
|
|
961
|
+
// Output: {"name":"TOTVS S.A.","code":12345,"active":true,"rate":99.9}
|
|
962
|
+
|
|
963
|
+
FreeObj(oJson)
|
|
964
|
+
Return
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
#### Parsing JSON
|
|
968
|
+
|
|
969
|
+
```advpl
|
|
970
|
+
#Include "TOTVS.CH"
|
|
971
|
+
|
|
972
|
+
User Function JsonParse()
|
|
973
|
+
Local oJson := JsonObject():New()
|
|
974
|
+
Local cJson := '{"name":"TOTVS","code":123,"active":true}'
|
|
975
|
+
Local nResult := 0
|
|
976
|
+
|
|
977
|
+
nResult := oJson:FromJson(cJson)
|
|
978
|
+
|
|
979
|
+
If nResult == 0 // 0 = success
|
|
980
|
+
ConOut("Name: " + oJson["name"]) // "TOTVS"
|
|
981
|
+
ConOut("Code: " + cValToChar(oJson["code"])) // "123"
|
|
982
|
+
ConOut("Active: " + IIf(oJson["active"], "Yes", "No")) // "Yes"
|
|
983
|
+
Else
|
|
984
|
+
ConOut("JSON parse error code: " + cValToChar(nResult))
|
|
985
|
+
EndIf
|
|
986
|
+
|
|
987
|
+
FreeObj(oJson)
|
|
988
|
+
Return
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
#### Working with Nested JSON
|
|
992
|
+
|
|
993
|
+
```advpl
|
|
994
|
+
#Include "TOTVS.CH"
|
|
995
|
+
|
|
996
|
+
User Function JsonNested()
|
|
997
|
+
Local oJson := JsonObject():New()
|
|
998
|
+
Local oAddress := JsonObject():New()
|
|
999
|
+
Local oContact := JsonObject():New()
|
|
1000
|
+
|
|
1001
|
+
// Build nested structure
|
|
1002
|
+
oAddress["street"] := "Av. Braz Leme, 1631"
|
|
1003
|
+
oAddress["city"] := "Sao Paulo"
|
|
1004
|
+
oAddress["state"] := "SP"
|
|
1005
|
+
oAddress["zipCode"] := "02511-000"
|
|
1006
|
+
|
|
1007
|
+
oContact["phone"] := "(11) 4003-0015"
|
|
1008
|
+
oContact["email"] := "contato@totvs.com"
|
|
1009
|
+
|
|
1010
|
+
oJson["company"] := "TOTVS S.A."
|
|
1011
|
+
oJson["address"] := oAddress
|
|
1012
|
+
oJson["contact"] := oContact
|
|
1013
|
+
|
|
1014
|
+
ConOut(oJson:ToJson())
|
|
1015
|
+
// {"company":"TOTVS S.A.","address":{"street":"Av. Braz Leme, 1631",...},...}
|
|
1016
|
+
|
|
1017
|
+
// Reading nested values
|
|
1018
|
+
ConOut("City: " + oJson["address"]["city"]) // "Sao Paulo"
|
|
1019
|
+
|
|
1020
|
+
FreeObj(oJson)
|
|
1021
|
+
Return
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
#### Arrays in JSON
|
|
1025
|
+
|
|
1026
|
+
```advpl
|
|
1027
|
+
#Include "TOTVS.CH"
|
|
1028
|
+
|
|
1029
|
+
User Function JsonArrays()
|
|
1030
|
+
Local oJson := JsonObject():New()
|
|
1031
|
+
Local aItems := {}
|
|
1032
|
+
Local oItem := Nil
|
|
1033
|
+
Local nI := 0
|
|
1034
|
+
|
|
1035
|
+
// Build array of objects
|
|
1036
|
+
oItem := JsonObject():New()
|
|
1037
|
+
oItem["id"] := "001"
|
|
1038
|
+
oItem["name"] := "Product A"
|
|
1039
|
+
aAdd(aItems, oItem)
|
|
1040
|
+
|
|
1041
|
+
oItem := JsonObject():New()
|
|
1042
|
+
oItem["id"] := "002"
|
|
1043
|
+
oItem["name"] := "Product B"
|
|
1044
|
+
aAdd(aItems, oItem)
|
|
1045
|
+
|
|
1046
|
+
oItem := JsonObject():New()
|
|
1047
|
+
oItem["id"] := "003"
|
|
1048
|
+
oItem["name"] := "Product C"
|
|
1049
|
+
aAdd(aItems, oItem)
|
|
1050
|
+
|
|
1051
|
+
oJson["products"] := aItems
|
|
1052
|
+
oJson["total"] := Len(aItems)
|
|
1053
|
+
|
|
1054
|
+
ConOut(oJson:ToJson())
|
|
1055
|
+
// {"products":[{"id":"001","name":"Product A"},...],"total":3}
|
|
1056
|
+
|
|
1057
|
+
// Iterating over JSON array
|
|
1058
|
+
For nI := 1 To Len(oJson["products"])
|
|
1059
|
+
ConOut("Item: " + oJson["products"][nI]["name"])
|
|
1060
|
+
Next nI
|
|
1061
|
+
|
|
1062
|
+
FreeObj(oJson)
|
|
1063
|
+
Return
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
### FWJsonDeserialize
|
|
1067
|
+
|
|
1068
|
+
`FWJsonDeserialize` converts a JSON string into a structured ADVPL hash map.
|
|
1069
|
+
|
|
1070
|
+
```advpl
|
|
1071
|
+
#Include "TOTVS.CH"
|
|
1072
|
+
|
|
1073
|
+
User Function JsonDeserialize()
|
|
1074
|
+
Local cJson := '{"customer":{"name":"TOTVS","items":[1,2,3]}}'
|
|
1075
|
+
Local oResult := Nil
|
|
1076
|
+
|
|
1077
|
+
oResult := FWJsonDeserialize(cJson)
|
|
1078
|
+
|
|
1079
|
+
If oResult != Nil
|
|
1080
|
+
ConOut("Name: " + oResult:customer:name)
|
|
1081
|
+
ConOut("Items count: " + cValToChar(Len(oResult:customer:items)))
|
|
1082
|
+
EndIf
|
|
1083
|
+
Return
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
### FWJsonSerialize
|
|
1087
|
+
|
|
1088
|
+
`FWJsonSerialize` converts ADVPL objects/arrays into JSON strings.
|
|
1089
|
+
|
|
1090
|
+
```advpl
|
|
1091
|
+
#Include "TOTVS.CH"
|
|
1092
|
+
|
|
1093
|
+
User Function JsonSerialize()
|
|
1094
|
+
Local aData := {}
|
|
1095
|
+
Local cResult := ""
|
|
1096
|
+
Local aItem := {}
|
|
1097
|
+
|
|
1098
|
+
aAdd(aItem, {"id", "001"})
|
|
1099
|
+
aAdd(aItem, {"name", "Product A"})
|
|
1100
|
+
aAdd(aItem, {"price", 29.90})
|
|
1101
|
+
aAdd(aData, aItem)
|
|
1102
|
+
|
|
1103
|
+
cResult := FWJsonSerialize(aData)
|
|
1104
|
+
ConOut("JSON: " + cResult)
|
|
1105
|
+
Return
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
---
|
|
1109
|
+
|
|
1110
|
+
## 7. HTTP Client (Calling External APIs)
|
|
1111
|
+
|
|
1112
|
+
### HttpGet
|
|
1113
|
+
|
|
1114
|
+
```advpl
|
|
1115
|
+
#Include "TOTVS.CH"
|
|
1116
|
+
|
|
1117
|
+
User Function CallGet()
|
|
1118
|
+
Local cUrl := "https://api.example.com/users"
|
|
1119
|
+
Local aHeaders := {}
|
|
1120
|
+
Local cResponse := ""
|
|
1121
|
+
Local nStatus := 0
|
|
1122
|
+
|
|
1123
|
+
aAdd(aHeaders, "Content-Type: application/json")
|
|
1124
|
+
aAdd(aHeaders, "Authorization: Bearer mytoken123")
|
|
1125
|
+
|
|
1126
|
+
cResponse := HttpGet(cUrl, "", "", @aHeaders, @nStatus)
|
|
1127
|
+
|
|
1128
|
+
ConOut("Status: " + cValToChar(nStatus))
|
|
1129
|
+
ConOut("Response: " + cResponse)
|
|
1130
|
+
Return
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
### HttpPost
|
|
1134
|
+
|
|
1135
|
+
```advpl
|
|
1136
|
+
#Include "TOTVS.CH"
|
|
1137
|
+
|
|
1138
|
+
User Function CallPost()
|
|
1139
|
+
Local cUrl := "https://api.example.com/users"
|
|
1140
|
+
Local oJson := JsonObject():New()
|
|
1141
|
+
Local cPayload := ""
|
|
1142
|
+
Local aHeaders := {}
|
|
1143
|
+
Local cResponse := ""
|
|
1144
|
+
|
|
1145
|
+
oJson["name"] := "John Doe"
|
|
1146
|
+
oJson["email"] := "john@example.com"
|
|
1147
|
+
cPayload := oJson:ToJson()
|
|
1148
|
+
|
|
1149
|
+
aAdd(aHeaders, "Content-Type: application/json")
|
|
1150
|
+
aAdd(aHeaders, "Authorization: Bearer mytoken123")
|
|
1151
|
+
|
|
1152
|
+
cResponse := HttpPost(cUrl, "", cPayload, @aHeaders)
|
|
1153
|
+
|
|
1154
|
+
ConOut("Response: " + cResponse)
|
|
1155
|
+
|
|
1156
|
+
FreeObj(oJson)
|
|
1157
|
+
Return
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
### FWCallRest for External REST Calls
|
|
1161
|
+
|
|
1162
|
+
`FWCallRest` is the recommended class for making external HTTP calls in Protheus.
|
|
1163
|
+
|
|
1164
|
+
```advpl
|
|
1165
|
+
#Include "TOTVS.CH"
|
|
1166
|
+
|
|
1167
|
+
User Function ExternalAPI()
|
|
1168
|
+
Local oRest := FWCallRest():New()
|
|
1169
|
+
Local cUrl := "https://api.example.com"
|
|
1170
|
+
Local cEndpoint := "/api/v1/products"
|
|
1171
|
+
Local oJson := JsonObject():New()
|
|
1172
|
+
Local cPayload := ""
|
|
1173
|
+
Local cResponse := ""
|
|
1174
|
+
Local nStatus := 0
|
|
1175
|
+
|
|
1176
|
+
// Configure the REST client
|
|
1177
|
+
oRest:SetPath(cUrl + cEndpoint)
|
|
1178
|
+
|
|
1179
|
+
// Set headers
|
|
1180
|
+
oRest:SetHeader("Content-Type", "application/json")
|
|
1181
|
+
oRest:SetHeader("Authorization", "Bearer mytoken123")
|
|
1182
|
+
oRest:SetHeader("Accept", "application/json")
|
|
1183
|
+
|
|
1184
|
+
// Set timeout (in seconds)
|
|
1185
|
+
oRest:SetTimeout(30)
|
|
1186
|
+
|
|
1187
|
+
//------------------------------------------
|
|
1188
|
+
// GET request
|
|
1189
|
+
//------------------------------------------
|
|
1190
|
+
nStatus := oRest:Get()
|
|
1191
|
+
cResponse := oRest:GetResult()
|
|
1192
|
+
|
|
1193
|
+
ConOut("GET Status: " + cValToChar(nStatus))
|
|
1194
|
+
ConOut("GET Response: " + cResponse)
|
|
1195
|
+
|
|
1196
|
+
//------------------------------------------
|
|
1197
|
+
// POST request
|
|
1198
|
+
//------------------------------------------
|
|
1199
|
+
oJson["name"] := "New Product"
|
|
1200
|
+
oJson["price"] := 49.90
|
|
1201
|
+
cPayload := oJson:ToJson()
|
|
1202
|
+
|
|
1203
|
+
oRest:SetPath(cUrl + cEndpoint)
|
|
1204
|
+
oRest:SetPostParams(cPayload)
|
|
1205
|
+
|
|
1206
|
+
nStatus := oRest:Post()
|
|
1207
|
+
cResponse := oRest:GetResult()
|
|
1208
|
+
|
|
1209
|
+
ConOut("POST Status: " + cValToChar(nStatus))
|
|
1210
|
+
ConOut("POST Response: " + cResponse)
|
|
1211
|
+
|
|
1212
|
+
FreeObj(oJson)
|
|
1213
|
+
FreeObj(oRest)
|
|
1214
|
+
Return
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
### SSL/TLS Considerations
|
|
1218
|
+
|
|
1219
|
+
When calling HTTPS endpoints, you may need to configure certificates:
|
|
1220
|
+
|
|
1221
|
+
```advpl
|
|
1222
|
+
#Include "TOTVS.CH"
|
|
1223
|
+
|
|
1224
|
+
User Function SecureCall()
|
|
1225
|
+
Local oRest := FWCallRest():New()
|
|
1226
|
+
|
|
1227
|
+
// Configure SSL
|
|
1228
|
+
oRest:SetPath("https://secure-api.example.com/data")
|
|
1229
|
+
oRest:SetHeader("Content-Type", "application/json")
|
|
1230
|
+
|
|
1231
|
+
// For self-signed certificates or custom CAs
|
|
1232
|
+
oRest:SetCertificate("/certs/client.pem")
|
|
1233
|
+
oRest:SetSSLKey("/certs/client.key")
|
|
1234
|
+
|
|
1235
|
+
// Optionally disable SSL verification (NOT recommended for production)
|
|
1236
|
+
// oRest:SetInsecure(.T.)
|
|
1237
|
+
|
|
1238
|
+
Local nStatus := oRest:Get()
|
|
1239
|
+
|
|
1240
|
+
If nStatus == 200
|
|
1241
|
+
ConOut("Secure response: " + oRest:GetResult())
|
|
1242
|
+
Else
|
|
1243
|
+
ConOut("SSL call failed with status: " + cValToChar(nStatus))
|
|
1244
|
+
EndIf
|
|
1245
|
+
|
|
1246
|
+
FreeObj(oRest)
|
|
1247
|
+
Return
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
### Handling Timeouts
|
|
1251
|
+
|
|
1252
|
+
```advpl
|
|
1253
|
+
#Include "TOTVS.CH"
|
|
1254
|
+
|
|
1255
|
+
User Function TimeoutExample()
|
|
1256
|
+
Local oRest := FWCallRest():New()
|
|
1257
|
+
|
|
1258
|
+
oRest:SetPath("https://slow-api.example.com/data")
|
|
1259
|
+
oRest:SetHeader("Content-Type", "application/json")
|
|
1260
|
+
|
|
1261
|
+
// Set connection timeout (seconds)
|
|
1262
|
+
oRest:SetTimeout(60)
|
|
1263
|
+
|
|
1264
|
+
Local nStatus := oRest:Get()
|
|
1265
|
+
|
|
1266
|
+
If nStatus == 0
|
|
1267
|
+
ConOut("Request timed out or connection failed")
|
|
1268
|
+
ConOut("Error: " + oRest:GetLastError())
|
|
1269
|
+
ElseIf nStatus == 200
|
|
1270
|
+
ConOut("Response: " + oRest:GetResult())
|
|
1271
|
+
Else
|
|
1272
|
+
ConOut("HTTP Error: " + cValToChar(nStatus))
|
|
1273
|
+
EndIf
|
|
1274
|
+
|
|
1275
|
+
FreeObj(oRest)
|
|
1276
|
+
Return
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
---
|
|
1280
|
+
|
|
1281
|
+
## 8. Error Handling
|
|
1282
|
+
|
|
1283
|
+
### Standard Error Response Format
|
|
1284
|
+
|
|
1285
|
+
Protheus REST APIs should follow a consistent error response structure:
|
|
1286
|
+
|
|
1287
|
+
```json
|
|
1288
|
+
{
|
|
1289
|
+
"code": "ERROR_CODE",
|
|
1290
|
+
"message": "Human-readable error description",
|
|
1291
|
+
"details": [
|
|
1292
|
+
{
|
|
1293
|
+
"field": "fieldName",
|
|
1294
|
+
"reason": "Specific validation error"
|
|
1295
|
+
}
|
|
1296
|
+
]
|
|
1297
|
+
}
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
### HTTP Status Codes Used in Protheus
|
|
1301
|
+
|
|
1302
|
+
| Code | Meaning | When to Use |
|
|
1303
|
+
|------|---------|-------------|
|
|
1304
|
+
| 200 | OK | Successful GET, PUT, DELETE |
|
|
1305
|
+
| 201 | Created | Successful POST (resource created) |
|
|
1306
|
+
| 204 | No Content | Successful DELETE with no body |
|
|
1307
|
+
| 400 | Bad Request | Invalid input, missing fields |
|
|
1308
|
+
| 401 | Unauthorized | Authentication failed |
|
|
1309
|
+
| 403 | Forbidden | Authenticated but insufficient permissions |
|
|
1310
|
+
| 404 | Not Found | Resource does not exist |
|
|
1311
|
+
| 409 | Conflict | Duplicate record, lock conflict |
|
|
1312
|
+
| 422 | Unprocessable Entity | Business rule validation failure |
|
|
1313
|
+
| 500 | Internal Server Error | Unexpected server error |
|
|
1314
|
+
|
|
1315
|
+
### Setting Status Codes
|
|
1316
|
+
|
|
1317
|
+
**WsRestFul (Legacy):**
|
|
1318
|
+
```advpl
|
|
1319
|
+
::SetStatus(404)
|
|
1320
|
+
::SetResponse('{"code":"NOT_FOUND","message":"Resource not found"}')
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
**TLPP (Modern):**
|
|
1324
|
+
```tlpp
|
|
1325
|
+
self:setStatus(404)
|
|
1326
|
+
self:setResponse('{"code":"NOT_FOUND","message":"Resource not found"}')
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
### Error Handler Function
|
|
1330
|
+
|
|
1331
|
+
A reusable error handler for REST services:
|
|
1332
|
+
|
|
1333
|
+
```advpl
|
|
1334
|
+
#Include "TOTVS.CH"
|
|
1335
|
+
|
|
1336
|
+
Static Function RestError(oSelf, nStatus, cCode, cMessage, aDetails)
|
|
1337
|
+
Local oJson := JsonObject():New()
|
|
1338
|
+
Local aDetList := {}
|
|
1339
|
+
Local oDetail := Nil
|
|
1340
|
+
Local nI := 0
|
|
1341
|
+
|
|
1342
|
+
Default nStatus := 500
|
|
1343
|
+
Default cCode := "INTERNAL_ERROR"
|
|
1344
|
+
Default cMessage := "An unexpected error occurred"
|
|
1345
|
+
Default aDetails := {}
|
|
1346
|
+
|
|
1347
|
+
oJson["code"] := cCode
|
|
1348
|
+
oJson["message"] := cMessage
|
|
1349
|
+
|
|
1350
|
+
If Len(aDetails) > 0
|
|
1351
|
+
For nI := 1 To Len(aDetails)
|
|
1352
|
+
oDetail := JsonObject():New()
|
|
1353
|
+
oDetail["field"] := aDetails[nI][1]
|
|
1354
|
+
oDetail["reason"] := aDetails[nI][2]
|
|
1355
|
+
aAdd(aDetList, oDetail)
|
|
1356
|
+
Next nI
|
|
1357
|
+
oJson["details"] := aDetList
|
|
1358
|
+
EndIf
|
|
1359
|
+
|
|
1360
|
+
oSelf:SetStatus(nStatus)
|
|
1361
|
+
oSelf:SetResponse(oJson:ToJson())
|
|
1362
|
+
|
|
1363
|
+
// Log the error
|
|
1364
|
+
ConOut("[REST ERROR] " + cCode + ": " + cMessage)
|
|
1365
|
+
FWLogMsg("ERROR", , "REST", , , , cCode + ": " + cMessage, , , )
|
|
1366
|
+
|
|
1367
|
+
FreeObj(oJson)
|
|
1368
|
+
Return
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
### Using the Error Handler
|
|
1372
|
+
|
|
1373
|
+
```advpl
|
|
1374
|
+
WsMethod POST WsService CustomerService
|
|
1375
|
+
Local cBody := ::GetContent()
|
|
1376
|
+
Local oJson := JsonObject():New()
|
|
1377
|
+
Local aErrors := {}
|
|
1378
|
+
|
|
1379
|
+
If Empty(cBody)
|
|
1380
|
+
RestError(Self, 400, "BAD_REQUEST", "Request body is required")
|
|
1381
|
+
FreeObj(oJson)
|
|
1382
|
+
Return .F.
|
|
1383
|
+
EndIf
|
|
1384
|
+
|
|
1385
|
+
oJson:FromJson(cBody)
|
|
1386
|
+
|
|
1387
|
+
// Collect multiple validation errors
|
|
1388
|
+
If oJson["name"] == Nil .Or. Empty(oJson["name"])
|
|
1389
|
+
aAdd(aErrors, {"name", "Field is required"})
|
|
1390
|
+
EndIf
|
|
1391
|
+
If oJson["cnpj"] == Nil .Or. Empty(oJson["cnpj"])
|
|
1392
|
+
aAdd(aErrors, {"cnpj", "Field is required"})
|
|
1393
|
+
EndIf
|
|
1394
|
+
|
|
1395
|
+
If Len(aErrors) > 0
|
|
1396
|
+
RestError(Self, 422, "VALIDATION_ERROR", "One or more fields failed validation", aErrors)
|
|
1397
|
+
FreeObj(oJson)
|
|
1398
|
+
Return .F.
|
|
1399
|
+
EndIf
|
|
1400
|
+
|
|
1401
|
+
// ... proceed with creation
|
|
1402
|
+
FreeObj(oJson)
|
|
1403
|
+
Return .T.
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
### Error Handling with Begin Sequence
|
|
1407
|
+
|
|
1408
|
+
For catching unexpected exceptions:
|
|
1409
|
+
|
|
1410
|
+
```advpl
|
|
1411
|
+
WsMethod GET WsReceive id WsService CustomerService
|
|
1412
|
+
Local oError := Nil
|
|
1413
|
+
Local bError := Nil
|
|
1414
|
+
Local oResp := JsonObject():New()
|
|
1415
|
+
|
|
1416
|
+
bError := ErrorBlock({|e| oError := e, Break(e)})
|
|
1417
|
+
|
|
1418
|
+
Begin Sequence
|
|
1419
|
+
|
|
1420
|
+
// Code that might throw an error
|
|
1421
|
+
DbSelectArea("SA1")
|
|
1422
|
+
SA1->(DbSetOrder(1))
|
|
1423
|
+
|
|
1424
|
+
If SA1->(DbSeek(xFilial("SA1") + ::id))
|
|
1425
|
+
oResp["id"] := Alltrim(SA1->A1_COD)
|
|
1426
|
+
oResp["name"] := Alltrim(SA1->A1_NOME)
|
|
1427
|
+
::SetResponse(oResp:ToJson())
|
|
1428
|
+
Else
|
|
1429
|
+
RestError(Self, 404, "NOT_FOUND", "Customer not found")
|
|
1430
|
+
EndIf
|
|
1431
|
+
|
|
1432
|
+
Recover
|
|
1433
|
+
// Handle unexpected error
|
|
1434
|
+
RestError(Self, 500, "INTERNAL_ERROR", ;
|
|
1435
|
+
"Unexpected error: " + oError:Description + " at " + oError:SubSystem)
|
|
1436
|
+
End Sequence
|
|
1437
|
+
|
|
1438
|
+
ErrorBlock(bError) // Restore original error block
|
|
1439
|
+
|
|
1440
|
+
FreeObj(oResp)
|
|
1441
|
+
Return .T.
|
|
1442
|
+
```
|
|
1443
|
+
|
|
1444
|
+
---
|
|
1445
|
+
|
|
1446
|
+
## 9. Common Patterns
|
|
1447
|
+
|
|
1448
|
+
### Pagination
|
|
1449
|
+
|
|
1450
|
+
Standard TOTVS REST pagination pattern with `page`, `pageSize`, and `hasNext`:
|
|
1451
|
+
|
|
1452
|
+
```advpl
|
|
1453
|
+
Static Function BuildPaginatedResponse(cAlias, nPage, nPageSize, bBuildItem)
|
|
1454
|
+
Local oJson := JsonObject():New()
|
|
1455
|
+
Local aItems := {}
|
|
1456
|
+
Local nSkip := (nPage - 1) * nPageSize
|
|
1457
|
+
Local nCount := 0
|
|
1458
|
+
|
|
1459
|
+
(cAlias)->(DbGoTop())
|
|
1460
|
+
|
|
1461
|
+
// Skip to requested page
|
|
1462
|
+
While nSkip > 0 .And. !(cAlias)->(Eof())
|
|
1463
|
+
If (cAlias)->(Deleted()) == .F.
|
|
1464
|
+
nSkip--
|
|
1465
|
+
EndIf
|
|
1466
|
+
(cAlias)->(DbSkip())
|
|
1467
|
+
EndDo
|
|
1468
|
+
|
|
1469
|
+
// Build current page
|
|
1470
|
+
While !(cAlias)->(Eof()) .And. nCount < nPageSize
|
|
1471
|
+
If (cAlias)->(Deleted()) == .F.
|
|
1472
|
+
aAdd(aItems, Eval(bBuildItem))
|
|
1473
|
+
nCount++
|
|
1474
|
+
EndIf
|
|
1475
|
+
(cAlias)->(DbSkip())
|
|
1476
|
+
EndDo
|
|
1477
|
+
|
|
1478
|
+
oJson["items"] := aItems
|
|
1479
|
+
oJson["page"] := nPage
|
|
1480
|
+
oJson["pageSize"] := nPageSize
|
|
1481
|
+
oJson["hasNext"] := !(cAlias)->(Eof())
|
|
1482
|
+
|
|
1483
|
+
Return oJson
|
|
1484
|
+
```
|
|
1485
|
+
|
|
1486
|
+
**Usage:**
|
|
1487
|
+
|
|
1488
|
+
```advpl
|
|
1489
|
+
WsMethod GET WsReceive page, pageSize WsService CustomerService
|
|
1490
|
+
Local nPage := IIf(::page == Nil, 1, ::page)
|
|
1491
|
+
Local nPageSize := IIf(::pageSize == Nil, 20, ::pageSize)
|
|
1492
|
+
Local oJson := Nil
|
|
1493
|
+
Local bItem := Nil
|
|
1494
|
+
|
|
1495
|
+
DbSelectArea("SA1")
|
|
1496
|
+
SA1->(DbSetOrder(1))
|
|
1497
|
+
|
|
1498
|
+
bItem := {|| BuildCustomerItem("SA1") }
|
|
1499
|
+
oJson := BuildPaginatedResponse("SA1", nPage, nPageSize, bItem)
|
|
1500
|
+
|
|
1501
|
+
::SetResponse(oJson:ToJson())
|
|
1502
|
+
FreeObj(oJson)
|
|
1503
|
+
Return .T.
|
|
1504
|
+
|
|
1505
|
+
Static Function BuildCustomerItem(cAlias)
|
|
1506
|
+
Local oItem := JsonObject():New()
|
|
1507
|
+
|
|
1508
|
+
oItem["id"] := Alltrim((cAlias)->A1_COD)
|
|
1509
|
+
oItem["name"] := Alltrim((cAlias)->A1_NOME)
|
|
1510
|
+
oItem["email"] := Alltrim((cAlias)->A1_EMAIL)
|
|
1511
|
+
Return oItem
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
### Filtering (Query Parameters)
|
|
1515
|
+
|
|
1516
|
+
```advpl
|
|
1517
|
+
WsMethod GET WsReceive page, pageSize, name, status WsService CustomerService
|
|
1518
|
+
Local cFilter := ""
|
|
1519
|
+
Local nPage := IIf(::page == Nil, 1, ::page)
|
|
1520
|
+
Local nPageSize := IIf(::pageSize == Nil, 20, ::pageSize)
|
|
1521
|
+
Local oJson := JsonObject():New()
|
|
1522
|
+
Local aItems := {}
|
|
1523
|
+
Local nCount := 0
|
|
1524
|
+
|
|
1525
|
+
DbSelectArea("SA1")
|
|
1526
|
+
SA1->(DbSetOrder(1))
|
|
1527
|
+
SA1->(DbGoTop())
|
|
1528
|
+
|
|
1529
|
+
While !SA1->(Eof()) .And. nCount < nPageSize
|
|
1530
|
+
If SA1->(Deleted()) == .F.
|
|
1531
|
+
// Apply filters
|
|
1532
|
+
If ::name != Nil .And. !Empty(::name)
|
|
1533
|
+
If !(Alltrim(Upper(::name)) $ Upper(SA1->A1_NOME))
|
|
1534
|
+
SA1->(DbSkip())
|
|
1535
|
+
Loop
|
|
1536
|
+
EndIf
|
|
1537
|
+
EndIf
|
|
1538
|
+
|
|
1539
|
+
If ::status != Nil .And. !Empty(::status)
|
|
1540
|
+
If SA1->A1_MSBLQL != ::status
|
|
1541
|
+
SA1->(DbSkip())
|
|
1542
|
+
Loop
|
|
1543
|
+
EndIf
|
|
1544
|
+
EndIf
|
|
1545
|
+
|
|
1546
|
+
// Record passed all filters
|
|
1547
|
+
Local oItem := JsonObject():New()
|
|
1548
|
+
oItem["id"] := Alltrim(SA1->A1_COD)
|
|
1549
|
+
oItem["name"] := Alltrim(SA1->A1_NOME)
|
|
1550
|
+
oItem["status"] := Alltrim(SA1->A1_MSBLQL)
|
|
1551
|
+
aAdd(aItems, oItem)
|
|
1552
|
+
nCount++
|
|
1553
|
+
EndIf
|
|
1554
|
+
|
|
1555
|
+
SA1->(DbSkip())
|
|
1556
|
+
EndDo
|
|
1557
|
+
|
|
1558
|
+
oJson["items"] := aItems
|
|
1559
|
+
oJson["page"] := nPage
|
|
1560
|
+
oJson["pageSize"] := nPageSize
|
|
1561
|
+
oJson["hasNext"] := !SA1->(Eof())
|
|
1562
|
+
|
|
1563
|
+
::SetResponse(oJson:ToJson())
|
|
1564
|
+
FreeObj(oJson)
|
|
1565
|
+
Return .T.
|
|
1566
|
+
```
|
|
1567
|
+
|
|
1568
|
+
### Sorting
|
|
1569
|
+
|
|
1570
|
+
```advpl
|
|
1571
|
+
WsMethod GET WsReceive orderBy, direction WsService CustomerService
|
|
1572
|
+
Local cOrderBy := IIf(::orderBy == Nil, "name", ::orderBy)
|
|
1573
|
+
Local cDirection := IIf(::direction == Nil, "asc", Lower(::direction))
|
|
1574
|
+
Local nOrder := 1
|
|
1575
|
+
|
|
1576
|
+
DbSelectArea("SA1")
|
|
1577
|
+
|
|
1578
|
+
// Map field names to index orders
|
|
1579
|
+
Do Case
|
|
1580
|
+
Case cOrderBy == "name"
|
|
1581
|
+
nOrder := 1 // Index by A1_COD (filial + cod)
|
|
1582
|
+
Case cOrderBy == "code"
|
|
1583
|
+
nOrder := 1
|
|
1584
|
+
Case cOrderBy == "cnpj"
|
|
1585
|
+
nOrder := 3 // Index by A1_CGC
|
|
1586
|
+
Otherwise
|
|
1587
|
+
nOrder := 1
|
|
1588
|
+
EndCase
|
|
1589
|
+
|
|
1590
|
+
SA1->(DbSetOrder(nOrder))
|
|
1591
|
+
|
|
1592
|
+
If cDirection == "desc"
|
|
1593
|
+
SA1->(DbGoBottom())
|
|
1594
|
+
Else
|
|
1595
|
+
SA1->(DbGoTop())
|
|
1596
|
+
EndIf
|
|
1597
|
+
|
|
1598
|
+
// ... build response iterating records
|
|
1599
|
+
Return .T.
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
### Partial Responses (Fields Parameter)
|
|
1603
|
+
|
|
1604
|
+
Allow clients to request only specific fields:
|
|
1605
|
+
|
|
1606
|
+
```advpl
|
|
1607
|
+
WsMethod GET WsReceive id, fields WsService CustomerService
|
|
1608
|
+
Local oJson := JsonObject():New()
|
|
1609
|
+
Local aFields := {}
|
|
1610
|
+
Local cAlias := "SA1"
|
|
1611
|
+
|
|
1612
|
+
DbSelectArea(cAlias)
|
|
1613
|
+
(cAlias)->(DbSetOrder(1))
|
|
1614
|
+
|
|
1615
|
+
If !(cAlias)->(DbSeek(xFilial(cAlias) + ::id))
|
|
1616
|
+
::SetStatus(404)
|
|
1617
|
+
oJson["code"] := "NOT_FOUND"
|
|
1618
|
+
oJson["message"] := "Customer not found"
|
|
1619
|
+
::SetResponse(oJson:ToJson())
|
|
1620
|
+
FreeObj(oJson)
|
|
1621
|
+
Return .T.
|
|
1622
|
+
EndIf
|
|
1623
|
+
|
|
1624
|
+
// Parse requested fields
|
|
1625
|
+
If ::fields != Nil .And. !Empty(::fields)
|
|
1626
|
+
aFields := StrTokArr(::fields, ",")
|
|
1627
|
+
EndIf
|
|
1628
|
+
|
|
1629
|
+
// Build response with requested fields only
|
|
1630
|
+
If Len(aFields) == 0 .Or. aScan(aFields, "id") > 0
|
|
1631
|
+
oJson["id"] := Alltrim((cAlias)->A1_COD)
|
|
1632
|
+
EndIf
|
|
1633
|
+
If Len(aFields) == 0 .Or. aScan(aFields, "name") > 0
|
|
1634
|
+
oJson["name"] := Alltrim((cAlias)->A1_NOME)
|
|
1635
|
+
EndIf
|
|
1636
|
+
If Len(aFields) == 0 .Or. aScan(aFields, "cnpj") > 0
|
|
1637
|
+
oJson["cnpj"] := Alltrim((cAlias)->A1_CGC)
|
|
1638
|
+
EndIf
|
|
1639
|
+
If Len(aFields) == 0 .Or. aScan(aFields, "email") > 0
|
|
1640
|
+
oJson["email"] := Alltrim((cAlias)->A1_EMAIL)
|
|
1641
|
+
EndIf
|
|
1642
|
+
If Len(aFields) == 0 .Or. aScan(aFields, "phone") > 0
|
|
1643
|
+
oJson["phone"] := Alltrim((cAlias)->A1_TEL)
|
|
1644
|
+
EndIf
|
|
1645
|
+
|
|
1646
|
+
::SetResponse(oJson:ToJson())
|
|
1647
|
+
|
|
1648
|
+
FreeObj(oJson)
|
|
1649
|
+
Return .T.
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
**Client call example:**
|
|
1653
|
+
```
|
|
1654
|
+
GET /rest/customers/000001?fields=id,name,email
|
|
1655
|
+
```
|
|
1656
|
+
|
|
1657
|
+
### Batch Operations
|
|
1658
|
+
|
|
1659
|
+
Processing multiple records in a single request:
|
|
1660
|
+
|
|
1661
|
+
```advpl
|
|
1662
|
+
WsMethod POST WsService BatchCustomerService
|
|
1663
|
+
Local cBody := ::GetContent()
|
|
1664
|
+
Local oJson := JsonObject():New()
|
|
1665
|
+
Local oResp := JsonObject():New()
|
|
1666
|
+
Local aResults := {}
|
|
1667
|
+
Local oResult := Nil
|
|
1668
|
+
Local aItems := {}
|
|
1669
|
+
Local nI := 0
|
|
1670
|
+
Local cAlias := "SA1"
|
|
1671
|
+
Local lAllOk := .T.
|
|
1672
|
+
|
|
1673
|
+
If Empty(cBody)
|
|
1674
|
+
::SetStatus(400)
|
|
1675
|
+
oResp["code"] := "BAD_REQUEST"
|
|
1676
|
+
oResp["message"] := "Request body is required"
|
|
1677
|
+
::SetResponse(oResp:ToJson())
|
|
1678
|
+
FreeObj(oJson)
|
|
1679
|
+
FreeObj(oResp)
|
|
1680
|
+
Return .F.
|
|
1681
|
+
EndIf
|
|
1682
|
+
|
|
1683
|
+
oJson:FromJson(cBody)
|
|
1684
|
+
aItems := oJson["items"]
|
|
1685
|
+
|
|
1686
|
+
If aItems == Nil .Or. Len(aItems) == 0
|
|
1687
|
+
::SetStatus(400)
|
|
1688
|
+
oResp["code"] := "BAD_REQUEST"
|
|
1689
|
+
oResp["message"] := "At least one item is required in 'items' array"
|
|
1690
|
+
::SetResponse(oResp:ToJson())
|
|
1691
|
+
FreeObj(oJson)
|
|
1692
|
+
FreeObj(oResp)
|
|
1693
|
+
Return .F.
|
|
1694
|
+
EndIf
|
|
1695
|
+
|
|
1696
|
+
DbSelectArea(cAlias)
|
|
1697
|
+
(cAlias)->(DbSetOrder(1))
|
|
1698
|
+
|
|
1699
|
+
Begin Transaction
|
|
1700
|
+
|
|
1701
|
+
For nI := 1 To Len(aItems)
|
|
1702
|
+
oResult := JsonObject():New()
|
|
1703
|
+
oResult["index"] := nI
|
|
1704
|
+
|
|
1705
|
+
If RecLock(cAlias, .T.)
|
|
1706
|
+
(cAlias)->A1_FILIAL := xFilial(cAlias)
|
|
1707
|
+
(cAlias)->A1_COD := GetSXENum("SA1", "A1_COD")
|
|
1708
|
+
(cAlias)->A1_LOJA := "01"
|
|
1709
|
+
(cAlias)->A1_NOME := aItems[nI]["name"]
|
|
1710
|
+
(cAlias)->A1_CGC := IIf(aItems[nI]["cnpj"] != Nil, aItems[nI]["cnpj"], "")
|
|
1711
|
+
(cAlias)->A1_EMAIL := IIf(aItems[nI]["email"] != Nil, aItems[nI]["email"], "")
|
|
1712
|
+
MsUnlock()
|
|
1713
|
+
ConfirmSX8()
|
|
1714
|
+
|
|
1715
|
+
oResult["status"] := "created"
|
|
1716
|
+
oResult["id"] := Alltrim((cAlias)->A1_COD)
|
|
1717
|
+
Else
|
|
1718
|
+
oResult["status"] := "error"
|
|
1719
|
+
oResult["message"] := "Failed to lock record"
|
|
1720
|
+
lAllOk := .F.
|
|
1721
|
+
EndIf
|
|
1722
|
+
|
|
1723
|
+
aAdd(aResults, oResult)
|
|
1724
|
+
Next nI
|
|
1725
|
+
|
|
1726
|
+
If !lAllOk
|
|
1727
|
+
DisarmTransaction()
|
|
1728
|
+
EndIf
|
|
1729
|
+
|
|
1730
|
+
End Transaction
|
|
1731
|
+
|
|
1732
|
+
oResp["results"] := aResults
|
|
1733
|
+
oResp["total"] := Len(aItems)
|
|
1734
|
+
oResp["successful"] := Len(aItems) - aScan(aResults, {|x| x["status"] == "error"})
|
|
1735
|
+
|
|
1736
|
+
If lAllOk
|
|
1737
|
+
::SetStatus(201)
|
|
1738
|
+
Else
|
|
1739
|
+
::SetStatus(207) // Multi-Status
|
|
1740
|
+
EndIf
|
|
1741
|
+
|
|
1742
|
+
::SetResponse(oResp:ToJson())
|
|
1743
|
+
|
|
1744
|
+
FreeObj(oJson)
|
|
1745
|
+
FreeObj(oResp)
|
|
1746
|
+
Return lAllOk
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
**Batch request body example:**
|
|
1750
|
+
```json
|
|
1751
|
+
{
|
|
1752
|
+
"items": [
|
|
1753
|
+
{"name": "Customer A", "cnpj": "11111111000101", "email": "a@test.com"},
|
|
1754
|
+
{"name": "Customer B", "cnpj": "22222222000102", "email": "b@test.com"},
|
|
1755
|
+
{"name": "Customer C", "cnpj": "33333333000103", "email": "c@test.com"}
|
|
1756
|
+
]
|
|
1757
|
+
}
|
|
1758
|
+
```
|