@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.
Files changed (70) hide show
  1. package/agents/changelog-generator.md +63 -0
  2. package/agents/code-generator.md +215 -0
  3. package/agents/code-reviewer.md +145 -0
  4. package/agents/debugger.md +83 -0
  5. package/agents/doc-generator.md +67 -0
  6. package/agents/docs-reference.md +86 -0
  7. package/agents/migrator.md +84 -0
  8. package/agents/process-consultant.md +97 -0
  9. package/agents/refactorer.md +75 -0
  10. package/agents/sx-configurator.md +67 -0
  11. package/commands/changelog.md +66 -0
  12. package/commands/diagnose.md +67 -0
  13. package/commands/docs.md +81 -0
  14. package/commands/document.md +67 -0
  15. package/commands/explain.md +60 -0
  16. package/commands/generate.md +111 -0
  17. package/commands/migrate.md +81 -0
  18. package/commands/process.md +111 -0
  19. package/commands/refactor.md +65 -0
  20. package/commands/review.md +60 -0
  21. package/commands/sxgen.md +98 -0
  22. package/commands/test.md +103 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +143 -0
  26. package/dist/index.js.map +1 -0
  27. package/package.json +30 -0
  28. package/skills/advpl-code-generation/SKILL.md +163 -0
  29. package/skills/advpl-code-generation/patterns-fwformbrowse.md +485 -0
  30. package/skills/advpl-code-generation/patterns-jobs.md +519 -0
  31. package/skills/advpl-code-generation/patterns-mvc.md +765 -0
  32. package/skills/advpl-code-generation/patterns-pontos-entrada.md +708 -0
  33. package/skills/advpl-code-generation/patterns-rest.md +974 -0
  34. package/skills/advpl-code-generation/patterns-soap.md +639 -0
  35. package/skills/advpl-code-generation/patterns-treport.md +481 -0
  36. package/skills/advpl-code-generation/patterns-workflow.md +779 -0
  37. package/skills/advpl-code-generation/templates-classes.md +1096 -0
  38. package/skills/advpl-code-review/SKILL.md +72 -0
  39. package/skills/advpl-code-review/rules-best-practices.md +444 -0
  40. package/skills/advpl-code-review/rules-modernization.md +290 -0
  41. package/skills/advpl-code-review/rules-performance.md +333 -0
  42. package/skills/advpl-code-review/rules-security.md +302 -0
  43. package/skills/advpl-debugging/SKILL.md +265 -0
  44. package/skills/advpl-debugging/common-errors.md +1124 -0
  45. package/skills/advpl-debugging/performance-tips.md +768 -0
  46. package/skills/advpl-refactoring/SKILL.md +139 -0
  47. package/skills/advpl-to-tlpp-migration/SKILL.md +293 -0
  48. package/skills/advpl-to-tlpp-migration/migration-checklist.md +122 -0
  49. package/skills/advpl-to-tlpp-migration/migration-rules.md +265 -0
  50. package/skills/changelog-patterns/SKILL.md +99 -0
  51. package/skills/code-explanation/SKILL.md +66 -0
  52. package/skills/documentation-patterns/SKILL.md +172 -0
  53. package/skills/embedded-sql/SKILL.md +379 -0
  54. package/skills/probat-testing/SKILL.md +226 -0
  55. package/skills/probat-testing/patterns-unit-tests.md +614 -0
  56. package/skills/protheus-business/SKILL.md +92 -0
  57. package/skills/protheus-business/modulo-compras.md +780 -0
  58. package/skills/protheus-business/modulo-contabilidade.md +874 -0
  59. package/skills/protheus-business/modulo-estoque.md +876 -0
  60. package/skills/protheus-business/modulo-faturamento.md +800 -0
  61. package/skills/protheus-business/modulo-financeiro.md +1015 -0
  62. package/skills/protheus-business/modulo-fiscal.md +749 -0
  63. package/skills/protheus-business/modulo-manutencao.md +848 -0
  64. package/skills/protheus-business/modulo-pcp.md +743 -0
  65. package/skills/protheus-reference/SKILL.md +119 -0
  66. package/skills/protheus-reference/native-functions.md +7029 -0
  67. package/skills/protheus-reference/rest-api-reference.md +1758 -0
  68. package/skills/protheus-reference/restricted-functions.md +265 -0
  69. package/skills/protheus-reference/sx-dictionary.md +854 -0
  70. 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
+ ```