@open-rap/compiler 0.0.0-stage → 0.1.1

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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -2
  3. package/dist/agent/server.d.ts +35 -0
  4. package/dist/agent/server.js +408 -0
  5. package/dist/agent/server.js.map +1 -0
  6. package/dist/authoring/abapgit.d.ts +38 -0
  7. package/dist/authoring/abapgit.js +242 -0
  8. package/dist/authoring/abapgit.js.map +1 -0
  9. package/dist/authoring/catalog.d.ts +48 -0
  10. package/dist/authoring/catalog.js +117 -0
  11. package/dist/authoring/catalog.js.map +1 -0
  12. package/dist/authoring/floorplan.d.ts +28 -0
  13. package/dist/authoring/floorplan.js +31 -0
  14. package/dist/authoring/floorplan.js.map +1 -0
  15. package/dist/authoring/generate.d.ts +57 -0
  16. package/dist/authoring/generate.js +630 -0
  17. package/dist/authoring/generate.js.map +1 -0
  18. package/dist/authoring/mcp.d.ts +15 -0
  19. package/dist/authoring/mcp.js +52 -0
  20. package/dist/authoring/mcp.js.map +1 -0
  21. package/dist/authoring/new.d.ts +35 -0
  22. package/dist/authoring/new.js +389 -0
  23. package/dist/authoring/new.js.map +1 -0
  24. package/dist/bdef/ast.d.ts +249 -0
  25. package/dist/bdef/ast.js +3 -0
  26. package/dist/bdef/ast.js.map +1 -0
  27. package/dist/bdef/lexer.d.ts +105 -0
  28. package/dist/bdef/lexer.js +148 -0
  29. package/dist/bdef/lexer.js.map +1 -0
  30. package/dist/bdef/parser.d.ts +22 -0
  31. package/dist/bdef/parser.js +1012 -0
  32. package/dist/bdef/parser.js.map +1 -0
  33. package/dist/build.d.ts +26 -0
  34. package/dist/build.js +62 -0
  35. package/dist/build.js.map +1 -0
  36. package/dist/cds/adapter.d.ts +91 -0
  37. package/dist/cds/adapter.js +381 -0
  38. package/dist/cds/adapter.js.map +1 -0
  39. package/dist/cds/expression-ast.d.ts +65 -0
  40. package/dist/cds/expression-ast.js +330 -0
  41. package/dist/cds/expression-ast.js.map +1 -0
  42. package/dist/cli.d.ts +9 -0
  43. package/dist/cli.js +485 -0
  44. package/dist/cli.js.map +1 -0
  45. package/dist/dcl/parser.d.ts +59 -0
  46. package/dist/dcl/parser.js +338 -0
  47. package/dist/dcl/parser.js.map +1 -0
  48. package/dist/ddic/catalog.d.ts +17 -0
  49. package/dist/ddic/catalog.js +211 -0
  50. package/dist/ddic/catalog.js.map +1 -0
  51. package/dist/ddic/cds-types.d.ts +47 -0
  52. package/dist/ddic/cds-types.js +131 -0
  53. package/dist/ddic/cds-types.js.map +1 -0
  54. package/dist/ddic/table-source.d.ts +70 -0
  55. package/dist/ddic/table-source.js +500 -0
  56. package/dist/ddic/table-source.js.map +1 -0
  57. package/dist/ddic/xml.d.ts +65 -0
  58. package/dist/ddic/xml.js +201 -0
  59. package/dist/ddic/xml.js.map +1 -0
  60. package/dist/dev.d.ts +128 -0
  61. package/dist/dev.js +434 -0
  62. package/dist/dev.js.map +1 -0
  63. package/dist/diagnostics.d.ts +33 -0
  64. package/dist/diagnostics.js +53 -0
  65. package/dist/diagnostics.js.map +1 -0
  66. package/dist/gen/abap/derived-types.d.ts +26 -0
  67. package/dist/gen/abap/derived-types.js +323 -0
  68. package/dist/gen/abap/derived-types.js.map +1 -0
  69. package/dist/gen/abap/eml.d.ts +118 -0
  70. package/dist/gen/abap/eml.js +362 -0
  71. package/dist/gen/abap/eml.js.map +1 -0
  72. package/dist/gen/abap/enums.d.ts +13 -0
  73. package/dist/gen/abap/enums.js +74 -0
  74. package/dist/gen/abap/enums.js.map +1 -0
  75. package/dist/gen/abap/framework-http.d.ts +14 -0
  76. package/dist/gen/abap/framework-http.js +1049 -0
  77. package/dist/gen/abap/framework-http.js.map +1 -0
  78. package/dist/gen/abap/framework-query.d.ts +14 -0
  79. package/dist/gen/abap/framework-query.js +225 -0
  80. package/dist/gen/abap/framework-query.js.map +1 -0
  81. package/dist/gen/abap/framework-utils.d.ts +29 -0
  82. package/dist/gen/abap/framework-utils.js +3707 -0
  83. package/dist/gen/abap/framework-utils.js.map +1 -0
  84. package/dist/gen/abap/framework.d.ts +12 -0
  85. package/dist/gen/abap/framework.js +480 -0
  86. package/dist/gen/abap/framework.js.map +1 -0
  87. package/dist/gen/abap/generate.d.ts +40 -0
  88. package/dist/gen/abap/generate.js +238 -0
  89. package/dist/gen/abap/generate.js.map +1 -0
  90. package/dist/gen/abap/rewrite.d.ts +82 -0
  91. package/dist/gen/abap/rewrite.js +957 -0
  92. package/dist/gen/abap/rewrite.js.map +1 -0
  93. package/dist/gen/abap/shadow.d.ts +25 -0
  94. package/dist/gen/abap/shadow.js +175 -0
  95. package/dist/gen/abap/shadow.js.map +1 -0
  96. package/dist/gen/abap/tokens.d.ts +33 -0
  97. package/dist/gen/abap/tokens.js +87 -0
  98. package/dist/gen/abap/tokens.js.map +1 -0
  99. package/dist/gen/app/index.d.ts +25 -0
  100. package/dist/gen/app/index.js +322 -0
  101. package/dist/gen/app/index.js.map +1 -0
  102. package/dist/gen/csdl.d.ts +11 -0
  103. package/dist/gen/csdl.js +562 -0
  104. package/dist/gen/csdl.js.map +1 -0
  105. package/dist/gen/deploy.d.ts +12 -0
  106. package/dist/gen/deploy.js +53 -0
  107. package/dist/gen/deploy.js.map +1 -0
  108. package/dist/gen/docs/index.d.ts +35 -0
  109. package/dist/gen/docs/index.js +267 -0
  110. package/dist/gen/docs/index.js.map +1 -0
  111. package/dist/gen/docs/markdown.d.ts +23 -0
  112. package/dist/gen/docs/markdown.js +187 -0
  113. package/dist/gen/docs/markdown.js.map +1 -0
  114. package/dist/gen/docs/site.d.ts +17 -0
  115. package/dist/gen/docs/site.js +106 -0
  116. package/dist/gen/docs/site.js.map +1 -0
  117. package/dist/gen/keycloak.d.ts +21 -0
  118. package/dist/gen/keycloak.js +90 -0
  119. package/dist/gen/keycloak.js.map +1 -0
  120. package/dist/gen/launchpad/index.d.ts +48 -0
  121. package/dist/gen/launchpad/index.js +302 -0
  122. package/dist/gen/launchpad/index.js.map +1 -0
  123. package/dist/gen/launchpad/ui.d.ts +14 -0
  124. package/dist/gen/launchpad/ui.js +350 -0
  125. package/dist/gen/launchpad/ui.js.map +1 -0
  126. package/dist/gen/mcp.d.ts +23 -0
  127. package/dist/gen/mcp.js +244 -0
  128. package/dist/gen/mcp.js.map +1 -0
  129. package/dist/gen/sql-expressions.d.ts +12 -0
  130. package/dist/gen/sql-expressions.js +136 -0
  131. package/dist/gen/sql-expressions.js.map +1 -0
  132. package/dist/gen/sql.d.ts +38 -0
  133. package/dist/gen/sql.js +612 -0
  134. package/dist/gen/sql.js.map +1 -0
  135. package/dist/gen/ui/index.d.ts +7 -0
  136. package/dist/gen/ui/index.js +27 -0
  137. package/dist/gen/ui/index.js.map +1 -0
  138. package/dist/gen/ui/layout.d.ts +230 -0
  139. package/dist/gen/ui/layout.js +511 -0
  140. package/dist/gen/ui/layout.js.map +1 -0
  141. package/dist/gen/ui/static.d.ts +55 -0
  142. package/dist/gen/ui/static.js +2901 -0
  143. package/dist/gen/ui/static.js.map +1 -0
  144. package/dist/gen/ui/views.d.ts +10 -0
  145. package/dist/gen/ui/views.js +511 -0
  146. package/dist/gen/ui/views.js.map +1 -0
  147. package/dist/gen/vendor.d.ts +23 -0
  148. package/dist/gen/vendor.js +68 -0
  149. package/dist/gen/vendor.js.map +1 -0
  150. package/dist/gen/xml.d.ts +9 -0
  151. package/dist/gen/xml.js +42 -0
  152. package/dist/gen/xml.js.map +1 -0
  153. package/dist/index.d.ts +20 -0
  154. package/dist/index.js +20 -0
  155. package/dist/index.js.map +1 -0
  156. package/dist/input/project.d.ts +50 -0
  157. package/dist/input/project.js +128 -0
  158. package/dist/input/project.js.map +1 -0
  159. package/dist/ir/access-controls.d.ts +11 -0
  160. package/dist/ir/access-controls.js +148 -0
  161. package/dist/ir/access-controls.js.map +1 -0
  162. package/dist/ir/authorizations.d.ts +25 -0
  163. package/dist/ir/authorizations.js +178 -0
  164. package/dist/ir/authorizations.js.map +1 -0
  165. package/dist/ir/behaviors.d.ts +16 -0
  166. package/dist/ir/behaviors.js +548 -0
  167. package/dist/ir/behaviors.js.map +1 -0
  168. package/dist/ir/builder.d.ts +20 -0
  169. package/dist/ir/builder.js +505 -0
  170. package/dist/ir/builder.js.map +1 -0
  171. package/dist/ir/config.d.ts +25 -0
  172. package/dist/ir/config.js +130 -0
  173. package/dist/ir/config.js.map +1 -0
  174. package/dist/ir/dictionary.d.ts +54 -0
  175. package/dist/ir/dictionary.js +328 -0
  176. package/dist/ir/dictionary.js.map +1 -0
  177. package/dist/ir/entities.d.ts +58 -0
  178. package/dist/ir/entities.js +676 -0
  179. package/dist/ir/entities.js.map +1 -0
  180. package/dist/ir/expressions.d.ts +33 -0
  181. package/dist/ir/expressions.js +327 -0
  182. package/dist/ir/expressions.js.map +1 -0
  183. package/dist/ir/handlers.d.ts +63 -0
  184. package/dist/ir/handlers.js +398 -0
  185. package/dist/ir/handlers.js.map +1 -0
  186. package/dist/ir/number-ranges.d.ts +24 -0
  187. package/dist/ir/number-ranges.js +74 -0
  188. package/dist/ir/number-ranges.js.map +1 -0
  189. package/dist/ir/seed.d.ts +23 -0
  190. package/dist/ir/seed.js +78 -0
  191. package/dist/ir/seed.js.map +1 -0
  192. package/dist/ir/service.d.ts +21 -0
  193. package/dist/ir/service.js +96 -0
  194. package/dist/ir/service.js.map +1 -0
  195. package/dist/launchpad/config.d.ts +47 -0
  196. package/dist/launchpad/config.js +321 -0
  197. package/dist/launchpad/config.js.map +1 -0
  198. package/dist/launchpad/workspace.d.ts +48 -0
  199. package/dist/launchpad/workspace.js +166 -0
  200. package/dist/launchpad/workspace.js.map +1 -0
  201. package/dist/srvd/parser.d.ts +36 -0
  202. package/dist/srvd/parser.js +193 -0
  203. package/dist/srvd/parser.js.map +1 -0
  204. package/dist/standard/data.d.ts +18 -0
  205. package/dist/standard/data.js +534 -0
  206. package/dist/standard/data.js.map +1 -0
  207. package/dist/standard/index.d.ts +36 -0
  208. package/dist/standard/index.js +507 -0
  209. package/dist/standard/index.js.map +1 -0
  210. package/dist/version.d.ts +2 -0
  211. package/dist/version.js +4 -0
  212. package/dist/version.js.map +1 -0
  213. package/docs/AGENTS.md +79 -0
  214. package/docs/ANNOTATIONS.md +76 -0
  215. package/docs/DECISIONS.md +1554 -0
  216. package/docs/DEPLOYMENT.md +113 -0
  217. package/docs/DIAGNOSTICS.md +116 -0
  218. package/docs/FEATURES.md +287 -0
  219. package/docs/GUIDE.md +560 -0
  220. package/docs/KNOWN_GAPS.md +216 -0
  221. package/docs/MCP.md +84 -0
  222. package/docs/PLAN.md +690 -0
  223. package/docs/README.md +148 -0
  224. package/docs/REMOTE_APIS.md +121 -0
  225. package/docs/ROADMAP.md +35 -0
  226. package/docs/UTILITIES.md +95 -0
  227. package/package.json +49 -3
@@ -0,0 +1,1554 @@
1
+ # Technical decisions
2
+
3
+ Decisions taken on 2026-11-09, after the owner of the project approved the principle ("take the
4
+ technical decisions you think are the best; all the features of a non-draft RAP BO must be
5
+ respected"). Each entry gives the decision, the reason and the consequence. Numbers D1 to D13
6
+ correspond to points 1 to 13 of §11 of PLAN.md. The milestones M1 to M6 are those of PLAN.md §8.
7
+
8
+ ## D1 — BDEF grammar written with Chevrotain
9
+
10
+ - **Decision**: Chevrotain grammar for the BDEFs (managed behavior and projection); the BDEF
11
+ parser of abaplint is used as an oracle in the tests.
12
+ - **Why**: typed AST, positioned error messages, full control over the handling of the
13
+ out-of-scope constructs. The parser of abaplint only produces a generic tree.
14
+ - **Consequence**: two parsers to keep aligned; a test compares the entities, aliases and
15
+ operations each one finds.
16
+
17
+ ## D2 — ABP: generated derived types and source-to-source rewrite
18
+
19
+ - **Decision**: for each managed BDEF, the compiler generates an ABAP interface
20
+ (`ZRAP_DT_<BDEF>`) containing the RAP derived types (`%tky`, `%control`, `FOR …` tables,
21
+ `FAILED/REPORTED/MAPPED` responses, authorization and feature control requests and results),
22
+ then rewrites the behavior pool before giving it to the transpiler: signatures of the handlers,
23
+ `TYPE TABLE|STRUCTURE|RESPONSE FOR …` references, removal of `FOR BEHAVIOR OF`, and **all** the
24
+ EML statements (`READ`, `MODIFY`, `COMMIT`, `ROLLBACK ENTITIES`, long and short forms) lowered
25
+ into calls of the kernel class `ZRAP_EML` (`begin`, operations, `end`). The `#` of the operands
26
+ become the expected derived type; the inline declarations (`RESULT DATA(x)`) are hoisted as
27
+ `DATA x TYPE …`.
28
+ - **Adjustment (M4)**: the EML is parsed by a token-based parser of the project
29
+ (`gen/abap/eml.ts`), and no longer left to the lowering of the transpiler: abaplint does not
30
+ recognize all the forms (`CREATE AUTO FILL CID SET FIELDS WITH` is an unknown statement for
31
+ it), and the lowering of the transpiler lost `IN LOCAL MODE`, `BY \_assoc`, `LINK` and
32
+ `CREATE BY`. The complete ABAP check (types included) runs on the rewritten sources, with the
33
+ configuration of the transpiler; each replacement keeps the number of lines of the replaced
34
+ text, so that the errors (ABP004) point to the original line.
35
+ - **Adjustment (feature control)**: as on SAP, the feature control requests and results
36
+ (`FOR INSTANCE FEATURES`, `FOR GLOBAL FEATURES`) only contain what the BDEF declares at the
37
+ matching level: `%create`, `%update`, `%delete` of the `( features : … )` operations,
38
+ `%action-x` of the `( features : … )` actions, `%assoc-_x` of the `create ( features : … )`,
39
+ `%field-x` of the `field ( features : instance )` fields. A pool that writes `%update` without
40
+ `update ( features : instance )` is refused (ABP004) instead of passing `rapt check` and then
41
+ failing on activation on SAP.
42
+ - **Why**: abaplint types these constructs as `void` and the transpiler then refuses the ABP.
43
+ The PoC (PLAN.md, V10) proved the approach with the published packages; M4 generalized it.
44
+ - **Consequence**: the transpiler and the runtime are never patched nor monkeypatched; they are
45
+ given standard ABAP. The methods of `ZRAP_EML`, a "clean-room" class of the project, are
46
+ replaced at runtime by the JavaScript kernel (`runtime/src/rap/kernel.ts`). Any construct the
47
+ pass cannot handle produces a diagnostic (ABP005 to ABP007) and an entry in KNOWN_GAPS.md. An
48
+ upstream contribution (derived types in abaplint) remains desirable; it is public and will
49
+ only be proposed with the agreement of the owner of the project.
50
+
51
+ ## D3 — Fastify rather than Express
52
+
53
+ - **Why**: `inject()` lets the server be tested without a network; encapsulated plugins; good
54
+ TypeScript typing; performance.
55
+
56
+ ## D4 — OData reads through generated SQL views
57
+
58
+ - **Decision**: each CDS view becomes a PostgreSQL view (`zr_*` on the tables, then `zc_*` on
59
+ `zr_*`). The OData reads query the `zc_*` views; the writes go through the RAP runtime, to the
60
+ tables.
61
+ - **Why**: it is the separation of RAP (query through the CDS projection, transaction through
62
+ the BO). Future CDS expressions (CASE, CAST…) will translate directly into SQL.
63
+
64
+ ## D5 — OData scope extended to what a V4 List Report / Object Page sends
65
+
66
+ - **Decision**: in addition to the initial list: navigation `Entity(…)/_Assoc` (read and
67
+ creation), `ge`, `le`, `not`, `startswith`, `endswith`, `/$count` segment, simple `$search`,
68
+ CSRF token, `IEEE754Compatible=true`, one-level deep insert, `sap-messages` header,
69
+ `$<Content-ID>` references in a changeset, `sap-*` parameters ignored.
70
+ - **Why**: without them, a Fiori Elements V4 application (or our OpenUI5 UI, which uses the same
71
+ ODataModel) does not fully work.
72
+
73
+ ## D6 — Path of the service modeled on RAP
74
+
75
+ - **Decision**: by default `/sap/opu/odata4/sap/<binding>/srvd/sap/<srvd>/0001/`, configurable.
76
+ - **Why**: an existing Fiori Elements application can point to the service without changing its
77
+ manifest.
78
+
79
+ ## D7 — Fixture activatable as is on a real system
80
+
81
+ - **Decision**: the `fixtures/travel` fixture is a complete and consistent abapGit export
82
+ (strict(2), global authorization, managed numbering…). The authorization is no longer ignored:
83
+ it is part of the supported non-draft features (D14).
84
+ - **Why**: the reference behavior can be checked on an SAP system.
85
+
86
+ ## D8 — Timestamps as `timestamptz(6)`
87
+
88
+ - **Decision**: `TIMESTAMPL`/`UTCLONG` → `timestamptz(6)`, `TIMESTAMP` → `timestamptz(0)`.
89
+ - **Why**: natural type in SQL and for the OData filters. The timestamps generated by the runtime
90
+ are to the microsecond, which keeps the ETags stable.
91
+ - **Consequence**: the seventh ABAP decimal is lost (KNOWN_GAPS.md).
92
+ - **Adjustment (M4)**: `@abaplint/runtime` 2.14.24 stores the packed numbers in an exact form
93
+ (scaled `BigInt` integer). The conversions between the buffer and ABAP go through text
94
+ (`toFixed`): a `TIMESTAMPL` reaches the ABP intact and comes back intact. Only the arithmetic
95
+ operations of the runtime on packed numbers go through JavaScript numbers (KNOWN_GAPS.md). The
96
+ ETags are canonical values of the buffer, never recomputed by the ABP.
97
+
98
+ ## D9 — Client removed
99
+
100
+ - **Decision**: the `CLNT` column (`CLIENT`, `MANDT`) is removed from the tables; single-tenant
101
+ application.
102
+
103
+ ## D10 — Freestyle OpenUI5 1.136 LTS UI
104
+
105
+ - **Decision**: `sap.m`, `sap.f`, `sap.uxap`, `sap.ui.layout` controls, `sap_horizon` theme,
106
+ ODataModel V4 with `autoExpandSelect`. `sap.ui.mdc` is a possible evolution.
107
+ - **Why**: Apache-2.0 libraries; the ODataModel V4 is the one of Fiori Elements V4.
108
+ - **Implementation (M5)**: `rapt build` writes `ui/`: one List Report XML view per root entity
109
+ set and one Object Page view per entity set, generated from the `@UI` annotations (same
110
+ derivation as the CSDL), and generic controllers driven by `ui/model/layout.json` (filters,
111
+ search, non-draft creation, edit and save with `submitBatch`, deletion, actions with a
112
+ parameter dialog, factory actions, editable composition tables, messages). The feature control
113
+ goes through `__OperationControl`, `__EntityControl` and `__FieldControl`, which the ODataModel
114
+ selects itself.
115
+
116
+ ## D11 — Serialized ABP execution, reentrant within a LUW
117
+
118
+ - **Decision**: a lock per process wraps the ABP calls; it is reentrant for the LUW that holds
119
+ it (a determination that runs an internal action does not block itself).
120
+ - **Why**: the abaplint runtime has a global state (`sy-*`, static attributes).
121
+ - **Consequence**: scaling with processes or containers.
122
+ - **Implementation (M4)**: `RapRuntime.transaction` chains the LUWs on a promise (one LUW at a
123
+ time per process); the current LUW is carried by `AsyncLocalStorage`, which makes the EML calls
124
+ of the handlers reentrant. Between processes, the locks are PostgreSQL row locks
125
+ (`SELECT … FOR UPDATE NOWAIT` on the instance of the `lock master`, cause `locked` when refused).
126
+
127
+ ## D12 — Naming and license
128
+
129
+ - **Decision**: packages `@open-rap/model`, `@open-rap/compiler`,
130
+ `@open-rap/runtime`; `rapt` CLI; MIT license for this repository.
131
+
132
+ ## D13 — Test corpus of the parsers
133
+
134
+ - **Initial decision**: an extract of BDEF and CDS sources of the SAP-samples repositories
135
+ (Apache-2.0).
136
+ - **Adjustment (M1)**: the SAP-samples repositories were reorganized and their paths can no
137
+ longer be found by code search. The corpus is therefore written for the project
138
+ (`packages/compiler/test/bdef-parser.test.ts`): namespaced names, operation options, static,
139
+ factory and internal actions, functions, side effects, `control` mappings, and the
140
+ out-of-scope constructs (draft, late numbering, events…). The same sources are compared with
141
+ the BDEF parser of abaplint (D1). An SAP-samples extract may be added later.
142
+
143
+ ## D14 — Functional scope: a complete non-draft managed RAP BO
144
+
145
+ - **Decision**: the target is the set of features of a managed BO without draft, tracked in
146
+ [FEATURES.md](FEATURES.md). It adds to the initial MVP: authorizations (global and instance),
147
+ static and dynamic feature control, external numbering, static, factory, internal actions and
148
+ actions with a parameter, on save determinations, precheck, side effects (CSDL), T100 messages,
149
+ reads and creations by association in the ABP.
150
+ - **Still out of scope**, with their reason in FEATURES.md: draft (excluded by the
151
+ specification, then added by D54), unmanaged implementation, late numbering, events, change
152
+ documents, extensions, analytics. `with additional save` and `with unmanaged save`, excluded at
153
+ first, are supported since D30.
154
+
155
+ ## D15 — Generic runtime driven by the IR
156
+
157
+ - **Decision**: the generated application contains a generic runtime (`@open-rap/runtime`)
158
+ that reads `model.json`. Only the SQL, the CSDL, the JavaScript of the ABP, the UI and the
159
+ packaging are generated. The runtime is bundled as a tarball in `vendor/`.
160
+ - **Why**: a single runtime tested for all BOs; a standalone output without a private registry.
161
+ - **Adjustment (M6)**: rather than a tarball, `vendor/` contains the compiled `runtime` and
162
+ `model` packages (`dist/` and a production `package.json`); their version carries a hash of
163
+ their content, so that `npm install` updates them after a new `rapt build`. The `.npmrc`
164
+ (`install-links=true`) installs them as ordinary packages. `server.mjs` calls `start()` of the
165
+ runtime, configured by environment variables (`.env.example`).
166
+
167
+ ## D16 — Canonical representation of the values
168
+
169
+ - **Decision**: in the transactional buffer, the values are canonical JavaScript values: strings
170
+ for CHAR, NUMC and STRING (trailing blanks removed); strings for decimals and INT8 (no loss of
171
+ precision); `YYYY-MM-DD` for dates (initial = `null`); `HH:MM:SS` for times; ISO 8601 UTC to
172
+ the microsecond for timestamps; lower-case UUIDs with dashes; booleans for `ABAP_BOOLEAN`.
173
+ - **Why**: a single pivot form between ABAP, SQL and OData; the conversions are isolated and
174
+ tested.
175
+
176
+ ## D17 — Toolchain
177
+
178
+ - **Decision**: TypeScript 7.0 (`strict`, `noUncheckedIndexedAccess`,
179
+ `exactOptionalPropertyTypes`, `verbatimModuleSyntax`), ESM `NodeNext` with `.js` specifiers,
180
+ `tsc` compilation to `dist/`, Vitest (aliases to the sources), pnpm workspaces.
181
+
182
+ ## D18 — Database of the tests
183
+
184
+ - **Decision**: PGlite (PostgreSQL in WASM, in process) for the integration tests; a real
185
+ PostgreSQL for the e2e.
186
+
187
+ ## D19 — "Shadow" DDIC for the transpiler
188
+
189
+ - **Decision**: the registry given to the transpiler receives the tables of the model with
190
+ predefined types (DATATYPE, LENG, DECIMALS), the data elements of the standard catalog and of
191
+ the project; no CDS views.
192
+ - **Why**: the SQL layer of the transpiler fails on associations and does not know the standard
193
+ SAP data elements; the types of the ABP come from the derived interface (D2); our SQL comes
194
+ from the IR.
195
+ - **Consequence**: a `TYPE zr_travel` type (CDS view) was not available in the ABP; the derived
196
+ types and the tables were.
197
+ - **Adjusted (D26)**: the CDS entities are given as "shadow" tables with the same name and
198
+ elements (predefined types, without associations), for ABAP SQL; `TYPE zr_travel` is therefore
199
+ typed. The shadow tables no longer have a client column (D9). The CDS views themselves stay out
200
+ of the registry of the transpiler.
201
+
202
+ ## D20 — `field ( mandatory )` without a check at runtime
203
+
204
+ - **Decision**: as in RAP, no runtime check; `Common.FieldControl` Mandatory in the CSDL and
205
+ required field in the UI. `mandatory : create`, on the other hand, is checked on creation, as
206
+ in RAP.
207
+
208
+ ## D21 — Triggering of the determinations and validations
209
+
210
+ - **Decision**: the on modify determinations run at the end of each modifying request, in
211
+ successive rounds: a round handles the instances whose trigger was touched since the previous
212
+ run of this determination; a determination is not triggered again by its own changes; the loop
213
+ is bounded (error beyond 20 rounds). The validations and the on save determinations run in the
214
+ save sequence, on the accumulated changes of the LUW.
215
+
216
+ ## D22 — RAP failure causes and HTTP statuses
217
+
218
+ - **Decision**: the first entry of `failed` gives the status of the OData error: `unspecific`
219
+ 400, `unauthorized` 403, `not_found` 404, `locked` and `conflict` 409, `readonly` 400,
220
+ `disabled` 409, `dependency` 424; a missing `If-Match` on PATCH, PUT and DELETE of an entity
221
+ with an ETag gives 428, a stale ETag 412. The main message is the first error message of
222
+ `reported`; the other messages go into `details`. Without a message, a generic text describes
223
+ the cause ("Travel(…) is locked by another user").
224
+ - **Why**: SAP does not publish a mapping table; these statuses are those of the HTTP protocol
225
+ for each cause, and Fiori elements shows the message whatever the status.
226
+
227
+ ## D23 — One LUW per request or per change set; responses rendered after the commit
228
+
229
+ - **Decision**: a modifying request (or a change set of a `$batch`) is a LUW: the operations, the
230
+ on modify determinations at the end of each request (D21), then the save sequence, in a
231
+ PostgreSQL transaction. The responses (created entity, result of an action…) are read after the
232
+ commit in the projection views; in a change set, they are deferred until the end of the change
233
+ set. A failure rolls back the whole change set, which receives a single error response.
234
+ - **Why**: the response shows the saved state (determinations, administrative fields, ETag), as
235
+ in RAP, and the reads never have to see the unsaved buffer. The navigations of a write
236
+ (`$1/_Booking`, `Travel(…)/_Booking(…)`) are resolved in the LUW, which sees the instances
237
+ created earlier in the change set.
238
+
239
+ ## D24 — Extended "clean-room" ABAP base
240
+
241
+ - **Decision**: in addition to `if_abap_behv`, `cl_abap_behavior_handler` and
242
+ `cl_abap_context_info`, the compiler provides `if_message`, `if_t100_message`,
243
+ `if_t100_dyn_msg`, `if_abap_behv_message`, `cx_root` (and `cx_static_check`,
244
+ `cx_dynamic_check`, `cx_no_check`, the classes of the runtime errors), `cl_system_uuid`, the
245
+ `scx_t100key` structure and the standard data elements of the DDIC catalog. The text of an
246
+ exception is its T100 message, its attributes resolved by name (native method
247
+ `ZRAP_EML=>exception_text`).
248
+ - **Why**: real behavior pools use message exception classes (pattern
249
+ `/DMO/CM_FLIGHT_MESSAGES`), `cl_system_uuid` and these types. The transpiler turns any class
250
+ named `CX_ROOT` into a subclass of the JavaScript `Error`: it is enough to provide it.
251
+ - **Consequence**: these objects are written from the public signatures of SAP (LICENSES.md); an
252
+ object of the project with the same name takes precedence.
253
+
254
+ ## D25 — Checks of the OData interface of the writes
255
+
256
+ - **Decision**: the `readonly` properties, the managed numbering properties and the
257
+ administrative fields sent by an OData client are ignored (as `Core.Computed` provides); an
258
+ unknown property gives 400. The operations that are not exposed (`use create` missing,
259
+ `internal` operation) give 405. The keys of an entity do not change with PATCH (400). The
260
+ feature control (`__EntityControl`, `__OperationControl`, `__FieldControl`) is only computed
261
+ when the request selects these properties (or has no `$select`). The global feature control of
262
+ `update`, `delete` and the instance actions is also there, with the same value for all the
263
+ instances: RAP presents it like the instance feature control ("consumer hint"). In the
264
+ generated views, these values are read with `%{…}` (raw value): `${…}` would give the text
265
+ formatted by the OData type (`"1"` for an Edm.Byte), and the comparison would fail.
266
+ - **Why**: it is the behavior the ODataModel V4 of Fiori elements expects; computing the feature
267
+ control calls the ABP and must not slow down the reads that do not use it.
268
+
269
+ ## D26 — Read ABAP SQL in the ABP
270
+
271
+ - **Initial decision**: any ABAP SQL statement in a behavior pool was a compile error (ABP007),
272
+ for lack of a database connection for the abaplint runtime.
273
+ - **Revised decision**: `SELECT` (simple, `SINGLE`, `INTO TABLE`, `FOR ALL ENTRIES`, joins,
274
+ aggregates, subqueries, `SELECT … ENDSELECT` with `SELECT *`) reads the tables and the CDS
275
+ entities of the project. The writes (`INSERT`, `UPDATE`, `MODIFY`, `DELETE` on a table) are
276
+ refused in the handlers (ABP007): a managed BO is written by the RAP runtime, the code changes
277
+ it with EML. They are allowed in the saver (`save_modified`) of a BO with additional or
278
+ unmanaged save (D30). Common table expressions (`WITH`) stay refused.
279
+ - **Implementation**:
280
+ - the SQL `@abaplint/transpiler` generates assumes columns in ABAP format (its own schemas store
281
+ dates as `NCHAR(8)`, UUIDs as 32 hexadecimal digits). The generated schema therefore adds a
282
+ `rapt_abap` schema of views, one per table (without a client column, D9) and per CDS entity,
283
+ lower-case column names: dates `YYYYMMDD`, times `HHMMSS`, UUIDs and binaries in upper-case
284
+ hexadecimal, booleans `X`/space, timestamps as packed numbers (`YYYYMMDDhhmmss.fffffff`), ABAP
285
+ initial values instead of `NULL`s. Fixed-length character types become `bpchar`, whose
286
+ comparisons ignore trailing blanks like ABAP. `abap.dbo.schemaPrefix` (option of the abaplint
287
+ runtime) makes the transpiled code read these views;
288
+ - the runtime registers a read-only database client (`DatabaseClient` of the abaplint runtime)
289
+ that runs the query in the transaction of the current LUW, after a translation to PostgreSQL:
290
+ `UP TO n ROWS` → `LIMIT`, `ORDER BY PRIMARY KEY`, `ASCENDING`, operators `EQ`/`GE`…,
291
+ `alias~field`, lower-case identifiers. Only the views of `rapt_abap` are readable. Each query
292
+ runs in a savepoint: an SQL error leaves the transaction of the LUW usable.
293
+ `COMMIT WORK`/`ROLLBACK WORK` are a runtime error, as in RAP;
294
+ - the CDS entities are given to the transpiler as "shadow" tables with the same name and
295
+ elements (D19): `SELECT FROM zr_travel` and `DATA ls TYPE zr_travel` are typed;
296
+ - abaplint does not type the inline declaration of the result of a literal, of an aggregate
297
+ other than `COUNT(*)` or of an expression: the transpiled method would fail on start. The
298
+ rewriting pass declares the two most common forms (existence check
299
+ `SELECT SINGLE @abap_true … INTO @DATA(x)`: `abap_boolean`; `MAX( col )`/`MIN( col )` of a
300
+ single source, numbering pattern of the RAP100 tutorial: the type of the column); the others
301
+ are reported at compile time (ABP009);
302
+ - `MESSAGE … INTO` takes its texts from the message classes of the project.
303
+ - **Semantics**: as in RAP, `SELECT` reads the database, not the transactional buffer: the
304
+ unsaved changes of the LUW are not visible in it (read the BO with EML for that).
305
+ - **Why**: reading control tables with SQL, the uniqueness of an identifier or numbering with
306
+ `MAX` are common in real behavior pools; views in ABAP format leave the SQL of the transpiler
307
+ almost intact and give the ABAP semantics of the comparisons. Trade-off: a condition on a
308
+ converted column (date, UUID, timestamp) cannot use an index (KNOWN_GAPS.md).
309
+ - **Related fix**: the failure of a lock (`FOR UPDATE NOWAIT`) left the PostgreSQL transaction
310
+ of the LUW in an error state; it is also isolated in a savepoint (test `rap-locks.test.ts` on a
311
+ real PostgreSQL).
312
+
313
+ ## D27 — OpenUI5 resources served from the npm packages
314
+
315
+ - **Decision**: the runtime serves `/resources/` from the npm packages of OpenUI5 (`@openui5/*`
316
+ 1.136, Apache-2.0): the sources of the libraries as they are, and the themes compiled on the
317
+ first request with `less-openui5` (Apache-2.0), then kept in memory. `/ui/` serves the
318
+ generated application.
319
+ - **Why**: the OpenUI5 CDN is not always reachable (it is not from the development environment
320
+ of this project) and a standalone application must not depend on it; npm is enough, without
321
+ UI5 tooling on startup. The first load requests about 400 modules: a bundle built by the UI5
322
+ tooling (`ui5 build`) is planned for the Docker image (M6).
323
+
324
+ ## D28 — OIDC sessions without an additional dependency
325
+
326
+ - **Decision**: the "authorization code" flow with PKCE is written with `jose` (already used for
327
+ the bearer tokens): sign-in state and session in signed cookies (HMAC-SHA256, HttpOnly,
328
+ SameSite=Lax), without storage on the server; CSRF token bound to the session; discovery
329
+ possible on an internal URL (`OIDC_DISCOVERY_URL`) distinct from the public issuer. The tests
330
+ use an OIDC provider written for them (discovery, authorization, token, JWKS).
331
+ - **Adjustment of PLAN.md**: `openid-client`, `@fastify/secure-session` and `oauth2-mock-server`
332
+ are not added. `openid-client` requires the issuer to be the discovery URL, which does not suit
333
+ Keycloak in docker-compose (the browser sees `localhost:8180`, the container of the
334
+ application `keycloak:8080`).
335
+ - **Consequence**: the session lasts 8 hours (`maxAge`); there is neither token refresh nor
336
+ revocation on the server before expiry (KNOWN_GAPS.md).
337
+
338
+ ## D29 — End-to-end test and CI
339
+
340
+ - **Decision**: the e2e test (`packages/runtime/test/e2e.test.ts`) starts from the compiled CLI:
341
+ `rapt build` of the fixture, `npm install` of the generated application, `node server.mjs` on a
342
+ real PostgreSQL (the one of `RAPT_E2E_DATABASE_URL`, otherwise a server started for the test,
343
+ otherwise PGlite), then the OData service and the UI in Chromium. The CI provides a
344
+ PostgreSQL 16 service and Chromium. A second job builds the image and starts the generated
345
+ `docker-compose.yml` (`--wait`, HTTP checks of the service and of the UI). Also checked in the
346
+ development environment: image built, PostgreSQL and application started by compose, creation,
347
+ creation by association, action and UI served by the container.
348
+ - **Why**: it is the delivered artifact that is tested, not the sources of the runtime.
349
+ - **Note**: TypeScript 7.0.2 compiles `new (x as T).Builder()` without the parentheses; only the
350
+ compiled JavaScript (dist) shows it, Vitest compiling the sources differently. The e2e test,
351
+ which runs the dist, revealed it; the code avoids this form.
352
+
353
+ ## D30 — Additional and unmanaged save in a managed BO
354
+
355
+ - **Decision**: `managed with additional save` and `managed with unmanaged save` (header of the
356
+ BDEF, or `with additional save` / `with unmanaged save` per entity, `with full data` accepted)
357
+ are supported, with the local saver class of the behavior pool (`lsc_…` inheriting from
358
+ `cl_abap_behavior_saver` or `cl_abap_behavior_saver_failed`): `save_modified` (mandatory) and
359
+ `cleanup`. Additional save: the runtime writes the persistent tables, then calls
360
+ `save_modified`. Unmanaged save: the entity has no persistent table, the runtime reads and
361
+ locks the instances through the CDS view and `save_modified` writes them.
362
+ - **Implementation**:
363
+ - derived types `TYPE TABLE FOR CHANGE`, `TYPE REQUEST FOR CHANGE` and
364
+ `TYPE REQUEST FOR DELETE`; the rewriting pass declares `save_modified` with its parameters
365
+ (`create`, `update`, `delete`, `reported`, and `failed` for `cl_abap_behavior_saver_failed`);
366
+ `adjust_numbers` (late numbering) is refused (ABP007);
367
+ - ABAP SQL writes are only allowed in the saver class (ABP007 elsewhere) and, at runtime,
368
+ during `save_modified` only. The RAP-specific forms
369
+ (`… FROM [TABLE] @source [INDICATORS SET STRUCTURE %control] MAPPING FROM ENTITY`), which the
370
+ transpiler does not know, are lowered to the kernel, which applies the mapping of the BDEF
371
+ and writes with the types of the database, exact values;
372
+ - the other writes go through the transpiler and the views of the `rapt_abap` schema (D26): an
373
+ `INSTEAD OF` trigger per table writes the table, values converted back from their ABAP form
374
+ (functions `rapt_abap.to_date`, `to_uuid`, `to_timestamp`…);
375
+ - the rows of `update` carry all the values (like `with full data`), `%control` the changed
376
+ fields; `delete` also contains the compositions deleted in cascade;
377
+ - a `failed` of the saver (`cl_abap_behavior_saver_failed`) rolls back the LUW, like a
378
+ validation; the messages of `reported` are returned to the client;
379
+ - all the transparent tables of the project enter the model (database and types of the
380
+ transpiler), not only those of the CDS views and of the persistent tables: a log or control
381
+ table can be read and written by ABAP SQL. A table no entity uses and that cannot be built is
382
+ left aside (information DDIC005).
383
+ - **Transpiler defect worked around**: `DELETE dbtab FROM @wa` makes it lose the row, and the
384
+ query would delete the whole table: refused at compile time (ABP007) and, for safety, a
385
+ `DELETE` without a condition is refused at runtime.
386
+ - **Why**: it is the RAP way to write existing tables or to log the changes of a BO, frequent in
387
+ real projects; the ABAP SQL of the saver reuses the views of D26.
388
+
389
+ ## D31 — Consuming remote APIs in the ABP (HTTP, OData client proxy, SCM and SCM2)
390
+
391
+ - **Decision**: a behavior pool can call remote APIs with the APIs of ABAP Cloud, rewritten in
392
+ clean-room from their public signatures:
393
+ - HTTP client: `cl_http_destination_provider` (`create_by_url`,
394
+ `create_by_comm_arrangement`, `create_by_cloud_destination`), `cl_web_http_client_manager`,
395
+ `if_web_http_client`, `if_web_http_request` (path, query, headers, text or binary body, form,
396
+ basic or bearer authentication), `if_web_http_response`, `if_web_http_header`,
397
+ `if_a4c_cp_service`;
398
+ - OData client proxy of the service consumption model: `/iwbep/cl_cp_factory_remote` and
399
+ `cl_web_odata_client_factory` (`create_v2_remote_proxy`, `create_v4_remote_proxy`); list read
400
+ (`create_by_range` of the filters combined with `and`/`or`/`not`, `$select`, `$orderby`,
401
+ `$top`, `$skip`, `request_count`, `request_no_business_data`), read by key
402
+ (`navigate_with_key`), create, update (`PATCH`/`PUT`, provided properties, `set_if_match`),
403
+ delete;
404
+ - exceptions `cx_http_dest_provider_error`, `cx_web_http_client_error`,
405
+ `cx_web_message_error`, `/iwbep/cx_gateway`, `/iwbep/cx_cp_remote` (`http_status_code`,
406
+ `http_error_body`), with the text of the error for `get_text( )`.
407
+ - **Destinations**: on SAP, they come from the destination service of BTP or from a
408
+ communication arrangement; the generated application reads them from its environment:
409
+ `DESTINATION_<NAME>_URL`, then `_USER` and `_PASSWORD` (basic), `_TOKEN` (bearer) or
410
+ `_HEADERS` (JSON object: API key…). `<NAME>` is the name of `create_by_cloud_destination`, or
411
+ `<SCENARIO>_<SERVICE_ID>` then `<SCENARIO>` for `create_by_comm_arrangement`
412
+ (`comm_system_id` ignored). The credentials stay in the kernel: the ABAP only sees the URL.
413
+ `rapt build` lists the destinations named by literals in the `.env.example` and the README of
414
+ the application.
415
+ - **Service consumption model**:
416
+ - SCM2: the consumption model class (`INHERITING FROM /iwbep/cl_v4_abs_pm_model_prov`) is kept
417
+ for its types (`tys_…`, `tyt_…`) and its constants (`gcs_entity_set`, `gcs_entity_type`); the
418
+ rewriting pass removes the definition of the model (`define`, `def_…`), written with the
419
+ `/iwbep/if_v4_pm_*` interfaces of SAP Gateway;
420
+ - SCM: the generated abstract entities (`_vc` value control fields included) type the data;
421
+ they are structures for the ABAP (shadow DDIC). The service definition generated for the
422
+ client proxy has no binding: when there are several definitions, the one with a binding is
423
+ built (SRVD007);
424
+ - the proxy reads the `$metadata` of the remote service once: entity sets, keys, types of the
425
+ properties. The internal ABAP names (`TRAVEL_ID`, `'TRAVEL'`) are matched with the EDM names
426
+ ignoring case and `_` (`TravelID`). The values are converted according to the EDM type and
427
+ the ABAP type: dates, times, timestamps (`/Date(ms)/` in V2, ISO in V4 with the precision of
428
+ the property), UUIDs, exact decimals as text (`IEEE754Compatible=true` in V4), booleans,
429
+ binary; the trailing blanks of the `c` fields are not sent; the ETag (`@odata.etag`,
430
+ `__metadata.etag`) fills the `etag` component of the SCM2 classes;
431
+ - modifying requests: CSRF token requested with `HEAD` on the root of the service
432
+ (`X-CSRF-Token: Fetch`, can be disabled with `iv_do_fetch_csrf_token`), session cookies
433
+ kept, retry with a new token after `403` + `X-CSRF-Token: Required`, as SAP Gateway requires.
434
+ - **Implementation**: the ABAP keeps the state of the objects (request, headers, filters); the
435
+ kernel (`ZRAP_EML`) makes the network calls (`fetch` of Node.js, 60 s by default or
436
+ `i_timeout`), reads the `$metadata` (fast-xml-parser, already used by the compiler) and
437
+ converts the values.
438
+ - **Why**: calling an API from an action, a determination or a validation is a common need
439
+ (side-by-side extension, enrichment, check against another system); the developer keeps the
440
+ code they write on SAP. The SAP Gateway classes do not exist outside an SAP system: they are
441
+ reimplemented, without SAP code.
442
+ - **Tests**: `rap-variant-remote.test.ts` (REST API: destination with basic auth, headers,
443
+ status, missing destination, refused connection; OData V4 with an SCM2 class to the
444
+ application of the fixture served on a port: filter, `$select`, `$orderby`, `$top`, `$count`,
445
+ read by key; V4 writes to a simulated service: CSRF, cookies, ETags, token renewal, 409 error;
446
+ OData V2 with the abstract entity of an SCM: `d.results`, `__count`, `/Date()/`),
447
+ `http-client.test.ts`, `app-scaffold.test.ts`.
448
+
449
+ ## D32 — Dependent ETag and ETag in a change set
450
+
451
+ - **Decision**: an `etag dependent by _Assoc` entity has the ETag of its master: the ETag is the
452
+ value of the ETag field of the master instance reached through the `by` associations (one or
453
+ several, up to an `etag master`). The service exposes it (`@odata.etag` of the instances,
454
+ `ETag` header, `Core.OptimisticConcurrency` with the path `_Travel/LocalLastChangedAt`) and
455
+ checks `If-Match` on the changes of the dependent entity (428 without the header, 412 when
456
+ stale).
457
+ - **The master changes with its dependents**: the SAP rule is that the ETag field is updated on
458
+ each change of the instance it covers; when a dependent instance is created, changed or
459
+ deleted, the runtime updates the ETag field of the master when it manages it (administrative
460
+ timestamp `localInstanceLastChangedAt` or `lastChangedAt`, master written by the runtime), once
461
+ per LUW, at save time. A master with unmanaged save or with another ETag field must be updated
462
+ by the application.
463
+ - **`If-Match` in a change set**: it is compared with the ETag of the instance at the beginning
464
+ of the LUW, the state the client read. The requests of a change set carry the ETags read before
465
+ it: an earlier change in the same change set (the previous request, or a determination that
466
+ recomputes the travel when a booking changes) does not make them stale. The changes of the
467
+ other clients are still detected: the LUW locks the instances it changes.
468
+ - **Why**: without it, the Object Page of a non-draft BO that saves a travel and its bookings in
469
+ one change set would fail with 412 as soon as a determination touches the travel; and a
470
+ dependent entity would have no concurrency protection.
471
+ - **Implementation**: OData model (`etagPath`), CSDL, read (scalar subqueries along the
472
+ navigations, in the lists, the single reads and `$expand`), write check
473
+ (`Transaction.etagValue`), update of the master (`refreshEtagMasters`, before the write).
474
+ Tests: `rap-variant-etag.test.ts`.
475
+
476
+ ## D33 — PGlite and Keycloak proven end to end
477
+
478
+ - **PGlite** (`PGLITE_DIR`): the e2e test starts the generated application on its embedded
479
+ database, creates a travel, stops it and starts it again: the schema is applied again
480
+ (idempotent) and the data is there. The test revealed that the application did not start on
481
+ PGlite: the schema was sent as one query of several statements, which the extended protocol of
482
+ PGlite refuses (`cannot insert multiple commands into a prepared statement`). It is now split
483
+ into statements (`splitStatements`: literals, identifiers, `$f$…$f$` bodies and comments
484
+ respected) and applied in a transaction.
485
+ - **Keycloak**: `keycloak.test.ts` plays the browser against a real Keycloak with the generated
486
+ realm: redirect to Keycloak, submission of the sign-in form of `alice`, return to
487
+ `/auth/callback` (authorization code with PKCE), session on the UI and on the OData service
488
+ (CSRF token, creation attributed to `ALICE`), sign-out to the end-session endpoint of Keycloak;
489
+ then a bearer token issued by Keycloak (public client created through the admin API, password
490
+ grant) verified by the JWKS, and a forged token refused. Run against the Keycloak 26.8.2
491
+ distribution (Maven Central, Java 21) in the development environment, and in CI against the
492
+ image of the generated `docker-compose.yml` (`--profile oidc`).
493
+ - **CI**: the health check of the PostgreSQL service uses `pg_isready -U postgres`.
494
+
495
+ ## D34 — Authorizations: authorization objects, OIDC roles, AUTHORITY-CHECK
496
+
497
+ - **Problem**: the ABAP environment splits the authorizations between the IAM (IAM apps,
498
+ business catalogs, business roles with their restrictions, assigned to business users) and the
499
+ code (`AUTHORITY-CHECK` in the handlers, DCL access controls). Outside SAP, there is no IAM;
500
+ OIDC authenticates and carries roles, but does not say which values of authorization objects a
501
+ role grants. The transpiler translates `AUTHORITY-CHECK` into a call to a kernel class
502
+ `KERNEL_AUTHORITY_CHECK` the application did not provide: the statement compiled but failed at
503
+ runtime (500).
504
+ - **Decision**:
505
+ - the authorization objects come from the abapGit sources (SUSO: fields `FIEL1…`, activities
506
+ `TACTZ`), or from the `objects` section of the roles file;
507
+ - the roles are in `rapt/authorizations.json`, at the root of the abapGit project, versioned
508
+ with the sources (outside the `src/` folder, abapGit ignores it): name of the role of the
509
+ IdP → authorizations, instances of objects with their values, like a business role and its
510
+ restrictions;
511
+ - values in the PFCG way: value, pattern `07*`, `*`, range `{ "from", "to" }`, and values from
512
+ a claim of the token (`{claim:agency}`: the agency of the user, attribute of the IdP), so as
513
+ not to create one role per value;
514
+ - `AUTHORITY-CHECK` is evaluated by the kernel on the roles of the user of the request:
515
+ `sy-subrc` 0 when an authorization of the object accepts all the checked fields (`DUMMY` is
516
+ not checked), 4 otherwise, 12 without an authorization for the object, 40 for `FOR USER` of
517
+ another user; the ABAP code stays the one of SAP;
518
+ - the IdP (Keycloak, Entra ID, SAP Cloud Identity Services…) assigns the roles to the users
519
+ (`OIDC_ROLES_CLAIM`) and carries the attributes; the application keeps the claims read by the
520
+ roles (session included); `OIDC_REQUIRED_ROLE` stays the right to access the application;
521
+ - in development (`AUTH_MODE=none`), the user has all the authorizations (like SAP_ALL in a
522
+ development system), unless `DEV_ROLES` (and `DEV_ATTRIBUTES`) are given;
523
+ `AUTHORIZATIONS_FILE` replaces the roles on deployment;
524
+ - the compiler checks the roles against the objects (AUTH001…AUTH004) and the
525
+ `AUTHORITY-CHECK`s of the code (ABP010, ABP011); the generated Keycloak realm contains the
526
+ roles (given to `alice`), a user attribute declared in the profile and a mapper to the token
527
+ for each claim read.
528
+ - **Why**: the same behavior pool runs on SAP and here; the definition of the roles stays
529
+ reviewed and versioned like the IAM objects; the administration of the users stays in the IdP.
530
+ - **Fixture**: the reference BO checks the `ZAO_TRAVL` object (activity, agency) in its global
531
+ and instance authorizations, like the /DMO/ reference scenario of SAP; roles `travel_manager`,
532
+ `travel_agent` (agency from the `agency` claim), `travel_viewer`.
533
+ - **Tests**: `rap-authorizations.test.ts` (locally signed tokens), `authorizations.test.ts`
534
+ (semantics of `sy-subrc`), `authorizations.test.ts` of the compiler (diagnostics),
535
+ `keycloak.test.ts` (an agent created in Keycloak with their agency attribute).
536
+
537
+ ## D35 — Access controls (DCL) on the reads
538
+
539
+ - **Decision**: the CDS roles of the abapGit sources (DCLS objects: `define role R { grant select
540
+ on E where … ; }`) restrict the rows the OData service returns to the user of the request, in
541
+ the lists, `$count`, the single reads, `$expand` and the navigations. A row is granted when a
542
+ condition of a role of the entity is true; an entity without a role is fully readable (as in
543
+ ABAP CDS).
544
+ - **Evaluated conditions**: `( E1, E2 ) = aspect pfcg_auth( OBJ, F1, F2, ACTVT = '03' )` on the
545
+ authorizations of the user (D34: values, patterns, ranges, claims; no authorization → no row),
546
+ `E ?= aspect user` / `= aspect user` (ABAP name of the user), comparisons with literals (ABAP
547
+ format: dates `YYYYMMDD`), `like`, `is [not] null`, `and`, `or`, `not`, parentheses, paths of a
548
+ to-one association (`_Travel.AgencyID`, read by a subquery on the target view) and
549
+ `inheriting conditions from entity B`: a projection takes the conditions of its base view, its
550
+ elements renamed as it exposes them.
551
+ - **Security**: what cannot be evaluated (other aspects, longer paths, element not exposed by the
552
+ projection) grants nothing (deny by default), with a diagnostic (DCL002, DCL003). A read outside
553
+ a request grants nothing. The development user without `DEV_ROLES` is not restricted.
554
+ - **What is not filtered**: the EML and the ABAP SQL of the behavior pool (like
555
+ `WITH PRIVILEGED ACCESS`); the changes go through the instance authorization, as in RAP.
556
+ - **Implementation**: DCL parser (`dcl/parser.ts`), resolution for the exposed entities
557
+ (`ir/access-controls.ts`, conditions in `model.accessControls`), SQL condition computed per
558
+ request (`odata/access.ts`, user carried by an `AsyncLocalStorage`) and added to the `WHERE` of
559
+ the read engine.
560
+ - **Fixture**: roles `ZRT_R_TRAVEL` (agency through
561
+ `pfcg_auth( ZAO_TRAVL, ZAGENCY, ACTVT = '03' )`), `ZRT_C_TRAVEL` (inherits from the base view),
562
+ `ZRT_C_BOOKING` (agency of the travel, `_Travel.AgencyID`).
563
+
564
+ ## D36 — Authorizations in the consumer hints (UI)
565
+
566
+ - **Decision**: like RAP, which presents the feature control and the authorization check as
567
+ consumer hints, the entities whose BO has authorization handlers (master with a global or
568
+ instance handler, or entity dependent on such a master) expose `__EntityControl` (`Updatable`,
569
+ `Deletable`) and `__OperationControl` (instance actions, create by association); their values
570
+ combine, for the user of the request, the feature controls and the global and instance
571
+ authorizations. The `$metadata` announces them (`UpdateRestrictions`, `DeleteRestrictions`,
572
+ `NavigationRestrictions` per path, `Core.OperationAvailable`), usable by a Fiori elements
573
+ application as well.
574
+ - **Creation**: it depends on no instance; the service answers `GET <root>$permissions` (creations
575
+ allowed per entity set: global authorization and global feature control) and the generated UI
576
+ hides Create when it is refused.
577
+ - **Create by association**: for the master entity, it has its own indicator
578
+ (`%assoc-_Booking`), as on SAP; for a dependent entity, the update authorization of the master
579
+ applies. The fixture grants the creation of bookings with the update of the travel.
580
+ - **Tests**: `rap-authorizations.test.ts` (checks per profile, `$permissions`, creation of
581
+ bookings), `ui-authorizations.test.ts` (browser: a viewer does not see Create, Edit, Delete and
582
+ the action are disabled).
583
+
584
+ ## D37 — Launchpad of the applications (OpenUI5)
585
+
586
+ - **Decision**: `rapt launchpad <launchpad.json> --out <folder>` generates a launchpad, the
587
+ equivalent of the SAP Fiori launchpad built with OpenUI5. The SAP shell (`sap.ushell`) only
588
+ exists in SAPUI5, under an SAP license, and the rule of the project excludes it. The launchpad
589
+ is made of a `sap.f.ShellBar` (home, tile search, user menu with sign-out) and of groups of
590
+ `sap.m.GenericTile`. A dynamic tile (`NumericContent`) shows the `$count` of an entity set,
591
+ with an optional filter. Navigation is by intent (`/#Travel-manage`, `/go/Travel-manage?…`).
592
+ - **Content**: `launchpad.json` is versioned with the sources, like the content of the launchpad
593
+ (catalogs, groups, tiles, target mappings). It lists the applications (folder generated by
594
+ `rapt build`, internal URL) and the groups of tiles. A tile is assigned to roles of the IdP, the
595
+ equivalent of the business catalog of a business role; without a role, every user sees it. The
596
+ compiler resolves the service paths and checks the entity sets of the counters, the intents and
597
+ the roles (`LPD001` to `LPD007`); it takes the roles and the claims of the applications for the
598
+ realm and the session.
599
+ - **Architecture, a single origin**: the SAP front-end server serves the launchpad, the
600
+ applications and their services under one domain. Here, the server of the launchpad mounts the
601
+ UI of each application under `/apps/<id>/` and relays its services at their own paths
602
+ (`/sap/opu/odata4/…`, unique per service); the redirects of the applications are rewritten
603
+ under their mount point. The browser sees a single origin: no CORS, the UI reads the counters
604
+ directly, the cookies are shared.
605
+ - **Single sign-on**: the launchpad opens the OIDC session (code + PKCE). The applications behind
606
+ it have the same `SESSION_SECRET` (and the client and `PUBLIC_URL` of the launchpad) and read
607
+ this session: user, roles, claims read by their authorizations. Each application keeps its
608
+ authorizations (D34), its access controls (D35) and its required role, now checked on each
609
+ request: a session opened by the launchpad for a user without this role receives a 403. The
610
+ bearer tokens of the API clients are relayed as is.
611
+ - **Sign-out**: the ID token of the sign-in is kept in a separate HttpOnly cookie (`rapt_idt`)
612
+ and passed as `id_token_hint` to the end-session endpoint. The provider then ends its session
613
+ without a confirmation page, even with an expired token, as OIDC RP-Initiated Logout provides
614
+ (checked with Keycloak). This also applies to a standalone application.
615
+ - **Why not the relayed token (BFF)**: relaying the access token of the user would have required
616
+ keeping the tokens on the server and refreshing them (5 minutes by default with Keycloak). The
617
+ shared signed session stays stateless, like the one of the applications (D28).
618
+ - **In the applications**: the generated UI, served behind a launchpad (`/launchpad/config`
619
+ answers), shows the shell bar of the launchpad above its pages (home, user, sign-out). Alone,
620
+ it is unchanged. Each application stays deployable on its own.
621
+ - **Operations**: the generated folder contains `server.mjs`, a Dockerfile and a
622
+ `docker-compose.yml`: the launchpad, each application built from its folder with its PostgreSQL
623
+ database, and Keycloak in the `oidc` profile (client `rapt-launchpad`). Plus `.env.example`
624
+ (`APP_<ID>_URL`) and a README.
625
+ - **Tests**: `runtime/test/launchpad.test.ts` covers the tiles per role and the sign-in against a
626
+ test OIDC provider, then the reading of the session by the application. It also checks,
627
+ through the gateway, the DCL, the authorizations, the CSRF and the bearer tokens, as well as
628
+ the intents, the rewriting of the redirects, the 502 and the required role.
629
+ `runtime/test/launchpad-ui.test.ts` tests in the browser the tiles of a viewer, the counter,
630
+ the search, the application opened with the shell bar, the way back and the intent.
631
+ `compiler/test/launchpad.test.ts` covers the diagnostics, the generated files and the CLI.
632
+
633
+ ## D38 — Development loop (`rapt dev`)
634
+
635
+ - **Context**: building, installing and starting the generated application takes four commands
636
+ (`rapt build`, `npm install`, a PostgreSQL database, `npm start`) at each change of a source.
637
+ The standalone application is the target: the editing loop must be immediate.
638
+ - **Decision**: `rapt dev <src>` builds the application without its scaffold (no `vendor/`, no
639
+ `package.json`, no Docker) into `<src>/.rapt/dev`, starts it, watches the sources and starts
640
+ again on each change (grouped over 150 ms, one build at a time).
641
+ - **Dependencies**: the `node_modules` of the build is a link (a junction on Windows) to the one
642
+ of the `@open-rap/runtime` package, which contains the abaplint runtime, PGlite and
643
+ OpenUI5. The transpiled ABAP module and the runtime thus load the same `@abaplint/runtime`. No
644
+ npm installation.
645
+ - **Child process**: the server runs in a separate process (`dev-server.mjs`), stopped by an
646
+ IPC message (PGlite closes its files) then started again. The transpiled ABAP code sets a
647
+ global state (the `abap` object) and ES modules cannot be unloaded: a restart in the same
648
+ process would keep the old behavior pool.
649
+ - **Errors**: a build in error does not touch the running application; the diagnostics keep the
650
+ format of `rapt check`.
651
+ - **Database**: PGlite in `.rapt/dev/data`, kept between builds. The applied schema is
652
+ remembered; when it changes, the database is deleted and created again (development data, no
653
+ migration). With `DATABASE_URL`, the given database is used as is.
654
+ - **Watching**: one `fs.watch` per folder, not the recursive mode. On Linux, the recursive mode
655
+ of Node.js loses the files replaced by a rename, which most editors do on save. Folders
656
+ starting with a dot and `node_modules` are ignored, except `.abapgit.xml`.
657
+ - **Defaults**: `AUTH_MODE=none`, `HOST=127.0.0.1`, `LOG_LEVEL=warn`; each environment
658
+ variable overrides them.
659
+ - **Tests**: `compiler/test/dev.test.ts` covers the build, the behavior pool run, the data kept,
660
+ the build in error, the reset on a schema change and the watching of a file replaced by a
661
+ rename.
662
+
663
+ ## D39 — JSON output of the CLI and VS Code extension
664
+
665
+ - **Context**: everything must be possible on the command line, or visually in VS Code. Two
666
+ implementations of the compiler would diverge.
667
+ - **Decision**: the CLI stays the only engine; the VS Code extension
668
+ ([packages/vscode](../packages/vscode)) calls it and shows its results.
669
+ - **`--json`**: `rapt check` and `rapt build` write a JSON object (`command`, `ok`, `root`,
670
+ `counts`, `diagnostics` with, in addition to `file`, the absolute path `path`); `rapt build`
671
+ adds `out` and `files`. `rapt dev` writes one JSON event per line (`building`, `changed`,
672
+ `diagnostics`, `ready`, `failed`, `database-reset`, `server-failed`, `server-stopped`,
673
+ `error`, `log` for the logs of the server). The text of `rapt dev` is derived from the same
674
+ events (`formatDevEvent`).
675
+ - **Extension**: diagnostics in the Problems tab (on each save, or from `rapt dev` when it runs
676
+ on the project), commands *Check the BO*, *Build the application*, *Start / Stop rapt dev*,
677
+ *Open the application*, state in the status bar, menus of the explorer and of the title bar.
678
+ A project is the folder of `.abapgit.xml` (otherwise, the parent of `src`).
679
+ - **Standalone extension**: the `.vsix` (~31 MB, ~9,100 files) contains `rapt/`: the packages
680
+ of the repository (`pnpm pack`) installed by npm, flat, with their production dependencies
681
+ and what `rapt dev` serves (OpenUI5, `less-openui5`, PGlite, at the versions of the runtime),
682
+ without source maps, types, documentation, tests of the packages, OpenUI5 design time nor
683
+ PGlite extensions (`scripts/stage-rapt.mjs`). A download on first start would have made the
684
+ `.vsix` lighter, but required access to the npm registry (often filtered in companies) and
685
+ cache code; the `.vsix` goes through VS Code, offline once installed, and rapt always has the
686
+ version of the extension.
687
+ - **Node.js of VS Code**: rapt runs on the executable of VS Code with `ELECTRON_RUN_AS_NODE=1`
688
+ (Node.js 22 since VS Code 1.101, minimum version of the extension); the server of `rapt dev`
689
+ inherits the variable. Checked with Electron 37: `check`, `dev`, PGlite, behavior pool, stop
690
+ of the server when `rapt dev` is killed.
691
+ - **`rapt dev` installed flat**: the `node_modules` linked in `.rapt/dev` is the first folder,
692
+ from the runtime to the root, that contains `@abaplint/runtime` (the one of the runtime in
693
+ the pnpm repository, the one of `rapt/` in the extension).
694
+ - **CLI found**: `rapt.cliPath` setting, otherwise the bundled rapt, otherwise an Open RAP Runtime
695
+ clone or a `node_modules` of the workspace, otherwise `rapt` from the PATH; Node.js from
696
+ `rapt.nodePath`, otherwise the one of VS Code.
697
+ - **CI**: the `test` job builds the `.vsix`, checks the fixture with the bundled rapt and
698
+ publishes the `.vsix` as an artifact (`open-rap-runtime-vscode`).
699
+ - **Stopping `rapt dev`**: SIGINT. On Windows, where this signal terminates the process, the
700
+ server stops when its IPC channel with `rapt dev` closes (D38).
701
+ - **Build**: ESM sources checked by `tsc`, delivered as one CommonJS file by esbuild
702
+ (`dist/extension.cjs`, `vscode` external), without a runtime dependency; `.vsix` by
703
+ `vsce package`.
704
+ - **Tests**: `vscode/test/rapt.test.ts` covers the search of the project and of the CLI, the
705
+ reading of the JSON lines, `rapt check --json` and `rapt dev --json` really started.
706
+ `compiler/test/cli.test.ts` and `dev.test.ts` cover the JSON output and the events. The
707
+ interface of the extension (commands, status bar) is not tested in VS Code.
708
+
709
+ ## D40 — ABAP Cloud utility classes in the behavior pool
710
+
711
+ - **Context**: after the HTTP client (D31), the behavior pools need the other commonly used
712
+ released classes: JSON for the APIs, encodings, numbering, dates, application log, hashing,
713
+ random numbers, regular expressions, RTTS. Without them, ordinary ABAP Cloud code does not pass
714
+ the check.
715
+ - **Decision**: a clean-room implementation in ABAP of these classes
716
+ (`gen/abap/framework-utils.ts`), written from the public signatures documented by SAP, with
717
+ methods of the `ZRAP_EML` kernel for what ABAP cannot do alone (`runtime/src/abap/utils.ts`).
718
+ Guide: [UTILITIES.md](UTILITIES.md).
719
+ - **Closure**: only the objects named by the sources of the project, then those they name, are
720
+ given to the transpiler. A project that uses none of them keeps the same build and the same
721
+ check time; an object of the project with the same name wins.
722
+ - **The kernel**: JSON (parser that keeps the numbers as written, traversal of the data of
723
+ `@abaplint/runtime` by type, names of the members according to the mode), code pages, base64,
724
+ gzip (zlib), hashing and HMAC (crypto), random seed, regular expressions (RegExp with `d` for
725
+ the submatches, extended mode and POSIX classes translated), calendar and timestamps in
726
+ tenths of a microsecond (BigInt), description of a data object for the RTTS. The RTTS are
727
+ built in ABAP, one level at a time: `ASSIGN COMPONENT` for the structures,
728
+ `CREATE DATA … LIKE LINE OF` for the tables.
729
+ - **Numbering**: intervals of `rapt/number-ranges.json` (diagnostic NR001), inserted by
730
+ `db/schema.sql` without touching the current number; table `rapt_number_range`, written in the
731
+ LUW (the LUWs are serialized: no conflict). An interval that is not configured starts at 1, to
732
+ develop without configuration.
733
+ - **Application log**: tables `rapt_application_log` and `rapt_application_log_item`, in the
734
+ LUW; read by filter.
735
+ - **Tables**: generated only when the code uses `cl_numberrange_runtime` or `cl_bali_*`
736
+ (`model.services`), so as not to change the schema of the other applications.
737
+ - **Global types**: `string_table`, `xstring_table`, `match_result(_tab)`,
738
+ `submatch_result(_tab)` as DDIC objects (TABL, TTYP), and an `ABAP` type pool
739
+ (`abap_compdescr_tab`, `abap_component_tab`…), which the analysis of abaplint resolves.
740
+ - **Class constructors**: they run when the modules are loaded, before the kernel; those of the
741
+ utilities therefore do not call `ZRAP_EML` (`cl_abap_char_utilities` writes its control
742
+ characters as literals).
743
+ - **Tests**: `runtime/test/utilities.test.ts` runs a self-test written in ABAP in a behavior pool
744
+ (`test/utilities/selftest.abap`: more than seventy checks on all the classes, including the
745
+ reference values of SHA-256, MD5 and HMAC), draws numbers until an interval is exhausted and
746
+ writes then reads a log. `compiler/test/utilities.test.ts` covers the closure, the check of all
747
+ the classes and `rapt/number-ranges.json`.
748
+
749
+ ## D41 — Background processing (bgPF) and e-mail
750
+
751
+ - **Context**: `cl_bgmc_process_factory` and `cl_bcs_mail_message` are commonly used in RAP BOs
752
+ (deferred work after the save, notifications). Both rely on the LUW: the process only starts
753
+ and the mail only leaves after the `COMMIT WORK`.
754
+ - **Decision**: the runtime keeps, for each LUW, the work to do after its commit
755
+ (`LuwContext.afterCommit`). A rolled-back LUW forgets it; a committed LUW starts it after the
756
+ response (`setImmediate`), each item in a LUW of its own, serialized with the others.
757
+ - **bgPF**: `save_for_execution` writes the process (`rapt_background_process`, `NEW`) in the
758
+ LUW and keeps the reference of the ABAP operation. The execution calls `execute` in a LUW
759
+ marked "background": controlled (`if_bgmc_op_single`), the EML is saved at the end and ABAP
760
+ SQL writes after `cl_abap_tx=>save( )`; uncontrolled, ABAP SQL always writes,
761
+ `COMMIT ENTITIES` starts the save sequence (once) and `ROLLBACK ENTITIES` rolls back the LUW.
762
+ Outside the background, `COMMIT ENTITIES` stays refused in a behavior pool. The final state
763
+ (`SUCCESSFUL`, `ERRONEOUS` with the text of the exception) is written in a separate LUW, to
764
+ survive the rollback of the one of the operation.
765
+ - **Why in memory**: SAP serializes the operation object to run it later, elsewhere.
766
+ Serializing an object of the abaplint runtime (references, nested objects) would require a
767
+ general mechanism; the application being a single process, the reference is enough. Limit: a
768
+ process not run at shutdown is not resumed.
769
+ - **E-mail**: `send( )` composes the MIME message with nodemailer (MIT-0), keeps it in
770
+ `rapt_mail` (`WAITING`), then sends it after the commit through the server of
771
+ `MAIL_SMTP_URL` (`SENT` / `ERROR`). Without a server, the mail stays in the table (`STORED`)
772
+ with its complete message: development needs no server. `MAIL_FROM` gives the default sender;
773
+ both variables are in the `.env.example` of the applications that send mails.
774
+ - **Tables**: generated only if the code uses `cl_bgmc_process_factory` or
775
+ `cl_bcs_mail_message` (`model.services`, D40).
776
+ - **Tests**: `runtime/test/background.test.ts`: an action starts three processes (controlled
777
+ with EML, uncontrolled with `COMMIT ENTITIES`, failing) and checks their states and effects;
778
+ another sends a mail (HTML, copy, text and binary attachments), kept without a server, sent to
779
+ a test SMTP server with `MAIL_SMTP_URL`, and nothing is sent when the LUW is rolled back.
780
+
781
+ ## D42 — Writing only ABAP: tables as source, `rapt abapgit`, `rapt new`, initial data
782
+
783
+ - **Context**: an abapGit export requires, for each object, an XML metadata file, and the tables
784
+ only exist as XML (DD02V, DD09L, DD03P). Writing these files by hand discourages a developer
785
+ who wants to write their BO in VS Code without an SAP system.
786
+ - **Decision**:
787
+ - **Tables as source**: `name.tabl.astabl`, with the syntax of the table editor of the ABAP
788
+ Development Tools (`define table … { key client : abap.clnt not null; … }`, annotations
789
+ `@EndUserText.label`, `@AbapCatalog.*`, `@Semantics.amount.currencyCode` /
790
+ `@Semantics.quantity.unitOfMeasure` for the references, `include`, `define structure`).
791
+ `loadProject` derives the abapGit XML from it in memory: the rest of the compiler only reads
792
+ the XML, and the diagnostics of the tables carry the line of the field in the source. The
793
+ source wins over an XML next to it. Foreign keys (`with foreign key`) are reported (TABL002)
794
+ and not carried over.
795
+ - **`rapt abapgit <src>`** writes what is missing for abapGit to import the repository into an
796
+ SAP system: `.abapgit.xml` (which ignores `*.astabl`, unknown to abapGit),
797
+ `package.devc.xml`, the XML of the tables written as source (regenerated each time), and the
798
+ missing XML of the CDS entities (source type derived: view, projection, abstract entity,
799
+ custom entity…), metadata extensions, access controls, BDEF, SRVD, classes and interfaces
800
+ (description from `@EndUserText.label` or from the ABAP Doc short text). An existing XML is
801
+ never changed. `--check` only reports what is missing (exit code 1), for the CI.
802
+ - **`rapt new <src> <Entity> [--prefix ZRT] [--description …]`** writes a complete managed BO,
803
+ activatable on SAP: table as source, root view and projection, metadata extension (List
804
+ Report and Object Page), BDEF and projection, behavior pool (global authorization, one
805
+ determination), SRVD and SRVB OData V4, `rapt/seed.json`, `.gitignore` (`.rapt/`,
806
+ `node_modules/`), then the XML files of `rapt abapgit`. Existing files are never overwritten.
807
+ Command *RAP: New business object* in the extension.
808
+ - **Initial data**: `rapt/seed.json` (table → rows, field names of the dictionary, in lower
809
+ case, without the client) and `rapt/seed.sql`, checked at compile time (SEED001, SEED002) and
810
+ copied into `db/` of the application. With `SEED=auto` (default of `rapt dev`), the
811
+ application loads them on its first start, when all its tables are empty: a missing UUID key
812
+ is drawn at random, a NUMC receives its leading zeros. Without `SEED=auto` (in production by
813
+ default), nothing is loaded.
814
+ - **Why not a "source-less" XML**: abapGit can only serialize tables as XML; the `.astabl` source
815
+ is what an ADT developer already reads and writes, and the conversion to XML is deterministic.
816
+ Keeping both files in the repository (the XML regenerated by `rapt abapgit`) allows the import
817
+ into SAP without a tool.
818
+ - **Tests**: `compiler/test/authoring.test.ts` (the source of `zrt_booking` gives the same XML
819
+ and the same model as the fixture; positioned errors; `rapt abapgit` recreates the XML files
820
+ removed from the fixture identically; `rapt new` produces a BO that compiles without a
821
+ diagnostic) and `runtime/test/seed.test.ts`.
822
+
823
+ ## D43 — Custom entities (unmanaged query) and managed BO on a custom entity
824
+
825
+ - **Context**: the data of a BO sometimes comes from APIs, not tables. ABAP Cloud exposes it with
826
+ a custom entity (`define custom entity`) whose query class
827
+ (`@ObjectModel.query.implementedBy: 'ABAP:<class>'`, interface `if_rap_query_provider`)
828
+ provides the rows.
829
+ - **Decision**:
830
+ - **Custom entity** (activatable on SAP): the model keeps it without an SQL view, with its
831
+ query class, its typed elements and its associations (`_X : association … to … on …`). The
832
+ OData reads (collection, instance, navigation, `$count`, one-level `$expand`, to or from a
833
+ custom entity) call `select` with the request of ABAP Cloud: filter as an ABAP SQL string
834
+ (`get_as_sql_string`) and as ranges (`get_as_ranges`, `cx_rap_query_filter_no_range` when the
835
+ filter does not fit), sorting, paging (`get_paging`), requested elements,
836
+ `is_total_numb_of_rec_requested`, `is_data_requested`, search. `set_data` and
837
+ `set_total_number_of_records` give the result back to the runtime (kernel
838
+ `zrap_eml=>query_data`, `query_total`). As in SAP, the class applies the filter, the sorting
839
+ and the paging itself; the runtime only guarantees the keys and the navigation conditions
840
+ (extra rows dropped) and cuts the page at `$top`. The interfaces and classes of the query are
841
+ written from their public signatures (D24).
842
+ - **Managed BO with unmanaged save on a custom entity** (specific to Open RAP Runtime, warning
843
+ BDEF027): the RAP runtime reads the instances (READ ENTITIES, existence check, ETag,
844
+ associations) through the query class, with a filter on the keys, in the current LUW;
845
+ `save_modified` writes the changes to the API. No database lock: the LUWs of the application
846
+ are serialized. On SAP, the same need is handled by an unmanaged BO.
847
+ - **Why**: it is the ABAP Cloud way to read an API; the managed BO on the custom entity avoids
848
+ writing by hand the transactional buffer of an unmanaged BO, at the price of an accepted and
849
+ reported gap.
850
+ - **Tests**: fixture `fixtures/custom-entity` (products read from an "API" simulated by a table,
851
+ reviews as constants); `runtime/test/custom-entity.test.ts` (ranges, sorting, paging, count,
852
+ navigation, `$expand`, refused filter, creation, duplicate, validation, change, deletion) and
853
+ `compiler/test/custom-entity.test.ts`.
854
+
855
+ ## D44 — SAP standard views (I_Currency…) and value helps
856
+
857
+ - **Context**: on SAP (ABAP Cloud as well as S/4HANA), the released CDS views `I_Currency`,
858
+ `I_UnitOfMeasure`, `I_Country`, `I_Language` and their text views exist in every system, with
859
+ content delivered and maintained by SAP (ISO currencies, units, countries, languages). A RAP BO
860
+ uses them for its value helps (`@Consumption.valueHelpDefinition`) and its associations.
861
+ Without them, a source activatable on SAP failed here (CDS005) and the value help was ignored.
862
+ - **Decision**:
863
+ - **Standard views**: a source of the project that names one of these views without defining
864
+ it receives it, with its text view (`_Text`, category `#TEXT`) and its tables
865
+ (`rapt_currency`, `rapt_currencyt`, `rapt_uom`, `rapt_uomt`, `rapt_country`,
866
+ `rapt_countryt`, `rapt_language`, `rapt_languaget`). The elements have the SAP names; the
867
+ tables are specific to Open RAP Runtime (ABAP Cloud code does not read the SAP tables). A view
868
+ defined by the project wins. The files are virtual (`<standard>/…`): `rapt abapgit` never
869
+ writes them.
870
+ - **Reference data**: ISO 4217 currencies (with their decimals), ISO 3166 countries
871
+ (three-letter code, currency, EU membership), common units (SAP code, ISO code, dimension),
872
+ languages (one-character SAP key, ISO code); texts in English. `db/schema.sql` inserts them
873
+ (`ON CONFLICT DO NOTHING`): a row added or changed by the application stays. The standard
874
+ tables do not count for "all the tables are empty" of the initial data (D42).
875
+ - **Value helps**: as in RAP, the entity of a value help and its text view are exposed
876
+ implicitly by the service, read-only, without a page in the UI. `$metadata` carries
877
+ `Common.ValueList` (`CollectionPath`, `InOut` parameter, `DisplayOnly` columns). The UI opens
878
+ a selection dialog (search on the text columns and on the descriptions) from the fields of the
879
+ Object Page and the filters of the List Report.
880
+ - **Answer to the question "must the currencies be maintained as on BTP?"**: on BTP, the content
881
+ of these views is delivered by SAP and is not to be entered; here it is delivered with
882
+ Open RAP Runtime the same way. The other released views useful next: `I_Region`,
883
+ `I_CalendarDate` (calendar), `I_Timezone`, `I_BusinessUserBasic` (users, to link to the IdP),
884
+ `I_ExchangeRate` (exchange rates, to link to a source of rates).
885
+ - **Views added afterwards**: `I_Region` and `I_RegionText` (regions of the United States,
886
+ Canada, Germany and Australia), `I_CalendarDate` (calendar from 1990 to 2060 computed by
887
+ `generate_series`: year, quarter, month, ISO week, day of the week), `I_TimeZone` and
888
+ `I_TimeZoneText` (common IANA time zones), `I_BusinessUserBasic` (a user of the IdP is recorded
889
+ in it on each authenticated request: identifier, name, e-mail) and `I_ExchangeRate` (empty
890
+ table `rapt_exchrate`: the rates are data of the application). A service definition that
891
+ exposes a standard view receives it too.
892
+ - **Tests**: `compiler/test/standard.test.ts`, `runtime/test/standard-views.test.ts` (read,
893
+ texts, read-only, value help in the browser, calendar, users).
894
+
895
+ ## D45 — `standard` and `enhanced` modes
896
+
897
+ - **Decision**: `rapt/config.json` (`{ "mode": "enhanced" }`) or `--mode` chooses the mode of
898
+ the project. `standard` (default): the sources behave as on SAP and what SAP refuses is
899
+ refused; a project that builds in this mode imports and activates on SAP. `enhanced`: the
900
+ extensions of Open RAP Runtime, which do not exist on SAP:
901
+ - managed BO with unmanaged save on a custom entity (D43): error BDEF027 in `standard`,
902
+ information in `enhanced`;
903
+ - `field ( mandatory )` checked on save without a validation: each created or changed instance
904
+ with an initial mandatory field fails, with one message per field (`%element`). In
905
+ `standard`, as in RAP (D20), only `mandatory : create` is checked.
906
+ - **Why a file**: the mode is a property of the project, versioned with its sources; the CLI,
907
+ `rapt dev` and the VS Code extension (command *Choose the project mode*) read it.
908
+ - **Tests**: `compiler/test/config.test.ts`, `runtime/test/custom-entity.test.ts`.
909
+
910
+ ## D46 — `rapt generate`: one object at a time, derived from the existing objects
911
+
912
+ - **Decision**: `rapt generate <src> <type> <name> [--from X] [--for Y] [--expose A,B] [--root]`
913
+ writes an object with the file names of abapGit, then its XML: table, view (from a table,
914
+ elements in CamelCase, administrative annotations), projection (associations redirected by the
915
+ same renaming: `ZC_Travel ← ZR_Travel` gives `ZC_Booking`), metadata extension, access control,
916
+ BDEF (managed with its compositions: locks, authorizations and ETag of the children, UUID
917
+ numbering, read-only administrative fields, mapping; or projection BDEF), behavior pool (one
918
+ handler per entity, global authorization), abstract entity, custom entity and its query class,
919
+ class, interface, service definition and binding. An existing file is never overwritten.
920
+ - **Why**: the creation wizards of ADT derive an object from the previous one; `rapt new` writes
921
+ a whole BO, `rapt generate` accompanies the construction step by step.
922
+ - **VS Code**: *New RAP object…* (type, source object picked in a list, proposed name).
923
+ - **Tests**: `compiler/test/generate.test.ts` (a complete BO built command by command, without a
924
+ diagnostic), `vscode/test/rapt.test.ts`.
925
+
926
+ ## D47 — MCP server of the service
927
+
928
+ - **Decision**: `rapt mcp <src>` writes `rapt/mcp.json`, one tool per operation of the service
929
+ (`<Set>_list`, `_get`, `_create`, `_update`, `_delete`, `_create<_Composition>`, `_<action>`),
930
+ each with a description to complete to explain to an agent what the tool does and when to use
931
+ it; `false` removes a tool. Running the command again keeps the written descriptions, adds the
932
+ new tools and removes those the service no longer has. The application built with this file
933
+ serves the tools on `/mcp` (Model Context Protocol, "Streamable HTTP" transport, JSON
934
+ responses, without an SSE stream).
935
+ - **Execution**: each call is an OData request run by the service, for the user of the MCP
936
+ request: authorizations, DCL, feature control, validations and messages are those of the
937
+ OData. The errors of the BO come back as text (`isError`), the result as `structuredContent`.
938
+ - **Authentication**: bearer token (OIDC) or development user; never the session cookie of the
939
+ browser (a web page could otherwise call the tools on behalf of the user).
940
+ - **Tests**: `compiler/test/mcp.test.ts`, `runtime/test/mcp.test.ts` (protocol, read, write with
941
+ messages, actions, create by composition).
942
+
943
+ ## D48 — Spaces and pages of the launchpad
944
+
945
+ - **Decision**: `launchpad.json` accepts `spaces` (title, roles) → `pages` → `sections` of tiles,
946
+ like the SAP Fiori launchpad in spaces mode. A user sees the spaces of their roles as tabs, the
947
+ pages of a space in a list; the last space is remembered (local storage of the browser); the
948
+ search covers all the spaces. A file with `groups` only gives a space of one page. `groups` of
949
+ the model stays the list of all the sections (intents, search).
950
+ - **Tests**: `compiler/test/launchpad.test.ts`, `runtime/test/launchpad-ui.test.ts`.
951
+
952
+ ## D49 — Development launchpad and documentation application
953
+
954
+ - **Decision**: `rapt dev` serves at `/` a development launchpad: the application (with the
955
+ number of instances of its list), the documentation of Open RAP Runtime, the `$metadata` and,
956
+ with `rapt/mcp.json`, the MCP server. The application and the documentation show the shell bar
957
+ there. The documentation is an OpenUI5 application (list of the pages, search, links between
958
+ pages) whose pages are rendered from the Markdown of the repository at build time (our own
959
+ renderer, without a dependency); it is copied next to the compiler (`packages/compiler/docs`)
960
+ so that the packaged rapt (npm, VS Code extension) has it too. None of this is in
961
+ `rapt build`: it is a development tool.
962
+ - **Tests**: `compiler/test/docs.test.ts`, `compiler/test/dev.test.ts`.
963
+
964
+ ## D50 — Deployment outside localhost
965
+
966
+ - **Decision**: the generated application and launchpad deploy as is anywhere Node.js 22 or a
967
+ container runs. In addition to the Dockerfile, they have a Cloud Foundry `manifest.yml` (SAP
968
+ BTP) and a `.cfignore`. The runtime reads the environment of Cloud Foundry: `PUBLIC_URL` from
969
+ the route (`VCAP_APPLICATION`), `DATABASE_URL` and `DATABASE_CA` from a bound PostgreSQL
970
+ service (`VCAP_SERVICES`); a variable given explicitly wins. `DATABASE_CA` gives the
971
+ certificate of the PostgreSQL server (verified TLS connection). `rapt dev --host 0.0.0.0` opens
972
+ the development server to the network. Guide: [DEPLOYMENT.md](DEPLOYMENT.md).
973
+ - **Tests**: `runtime/test/platform.test.ts`, `compiler/test/app-scaffold.test.ts`.
974
+
975
+ ## D51 — UI: filter bar, user variants, refresh
976
+
977
+ - **Filter bar**: the *Go*, *Clear* and *Adapt Filters* buttons are aligned with the fields. Each
978
+ filter accepts conditions as in SAP Fiori (`=x`, `!=x`, `>x`, `>=x`, `<x`, `<=x`, `a...b`,
979
+ `*x*`, `x*`, `*x`; a single value: "contains" for a text, "equal" otherwise), several per field
980
+ (tokens: included values combined with OR, exclusions with AND); a string of digits (NUMC) is
981
+ entered without its leading zeros; dates as ranges, date-times by whole days (UTC). *Adapt
982
+ Filters* adds the columns of the list as filters. The "Filtered By" summary shows when the
983
+ header is collapsed.
984
+ - **Variants**: `sap.m.VariantManagement` (OpenUI5): filters, visible filters, visible columns
985
+ and sorting (*table settings*) saved per user by the service (`$variants?page=…`, table
986
+ `rapt_ui_variant` created on first use), default variant, favorites, renaming and deletion; in
987
+ the local storage of the browser when the service does not keep them.
988
+ - **Refresh**: the list reloads when you come back to it after a change saved on an Object Page
989
+ (deletion, save, action); the title of the table gives the number of rows.
990
+ - **Object Page**: breadcrumbs to the list; unsaved changes are discarded (with a confirmation)
991
+ when leaving the page, and are no longer sent with the next save.
992
+ - **Tests**: `compiler/test/ui.test.ts` (conditions), `runtime/test/ui.test.ts` (in the browser).
993
+
994
+ ## D52 — `$apply`: aggregation in the OData runtime
995
+
996
+ - **Context**: the Analytical List Page and the Overview Page (D53) need totals and groups;
997
+ Fiori Elements requests them with `$apply` (OData Data Aggregation).
998
+ - **Decision**: the runtime reads `$apply` with `filter(…)`, then a `groupby((P,…))`,
999
+ `groupby((P,…),aggregate(…))` or `aggregate(…)` transformation; methods `sum`, `min`, `max`,
1000
+ `average`, `countdistinct` and `$count as Alias`. The SQL query is a `GROUP BY` on the view of
1001
+ the entity, with the access controls (DCL) of the user; `$orderby` on the dimensions and the
1002
+ aliases, `$top`, `$skip`, `$count`. The other transformations (`topcount`, `compute`, a
1003
+ transformation after the grouping…) answer 501.
1004
+ - **Why**: it is the part of OData Data Aggregation the floorplans use; the rest requires the
1005
+ `@Aggregation` annotation transactional BOs do not have.
1006
+ - **Tests**: `runtime/test/odata-apply.test.ts`.
1007
+
1008
+ ## D53 — Floorplans: Worklist, Analytical List Page, Overview Page, Form entry
1009
+
1010
+ - **Context**: Fiori Elements chooses the floorplan in the descriptor of the application; the
1011
+ RAP sources say nothing about it. The floorplan is therefore a setting of the project.
1012
+ - **Decision**: `ui` of `rapt/config.json`:
1013
+ `{ "floorplans": { "<EntitySet>": "listReport" | "worklist" | "analyticalListPage" | "formEntry" },
1014
+ "overview": { "title": "…", "cards": [ … ] } }`.
1015
+ - *Worklist*: list without a filter bar, one tab per qualified `@UI.selectionVariant` of the
1016
+ entity, with the number of instances of each tab;
1017
+ - *Analytical List Page*: the first `@UI.chart` of the entity (dimension, measures, `#COLUMN`,
1018
+ `#BAR`, `#LINE`, `#PIE`, `#DONUT`) above the table, read with `$apply` (D52); a value chosen
1019
+ in the chart filters the table; chart view, table view or both;
1020
+ - *Form entry*: the Object Page for creation only; after the save, a new empty form;
1021
+ - *Overview Page*: home page of cards (`kpi`: aggregate of a measure or count; `list`,
1022
+ `table`: filtered and sorted instances; `chart`: a `@UI.chart`), each card opens the list or
1023
+ the instance.
1024
+ The chart is our own OpenUI5 control in SVG (`sap.viz` only exists in SAPUI5).
1025
+ - **Why**: the same annotations as Fiori Elements (`UI.chart`, `UI.selectionVariant`), without a
1026
+ proprietary library; a checked setting (CFG003) rather than a descriptor to write.
1027
+ - **Tests**: `compiler/test/config.test.ts`, `compiler/test/ui.test.ts`,
1028
+ `runtime/test/ui-floorplans.test.ts` (browser).
1029
+
1030
+ ## D54 — Draft (`with draft`)
1031
+
1032
+ - **Context**: draft is the standard way to edit in Fiori Elements V4; a managed RAP BO declares
1033
+ it with `with draft;` and one `draft table` per entity.
1034
+ - **Decision**:
1035
+ - **BDEF**: `with draft`, `draft table`, `total etag`, `draft action Edit`, `Activate
1036
+ optimized`, `Discard`, `Resume`, `draft determine action Prepare`; an entity without a
1037
+ `draft table`: BDEF028.
1038
+ - **Database**: one draft table per entity (the elements of the entity, `DraftUUID`,
1039
+ `HasActiveEntity`, dates of the draft) and `rapt_draft_admin` (user, creation, last change);
1040
+ a `<projection>_all` view combines active instances and drafts.
1041
+ - **OData**: `IsActiveEntity` key, properties `HasActiveEntity`, `HasDraftEntity`, navigation
1042
+ `DraftAdministrativeData`, actions `Edit`, `Activate`, `Prepare`, `Resume`, `Discard`,
1043
+ `Common.DraftRoot` / `DraftNode`. A user only reads their own drafts.
1044
+ - **Runtime**: creation, change and deletion in the draft without determinations nor
1045
+ validations; `Edit` copies the instance and its compositions (one draft per instance,
1046
+ exclusive lock of 15 minutes as SAP); `Activate` replays the draft in the managed runtime in
1047
+ the LUW of the request (determinations, validations, saver), keys kept, then deletes the
1048
+ draft; an active instance that has a draft cannot be changed directly (409).
1049
+ - **UI**: Create opens a draft, each field is saved in it when leaving it, Save activates,
1050
+ Cancel and *Discard Draft* discard; the list shows the editing status.
1051
+ - **Why**: the observable behavior of SAP (URLs, keys, actions, lock) so that the sources and the
1052
+ clients stay those of an SAP system.
1053
+ - **Tests**: `runtime/test/draft.test.ts`, `runtime/test/ui-draft.test.ts` (browser),
1054
+ `compiler/test/ir.test.ts` (BDEF028).
1055
+
1056
+ ## D55 — Table entities, CDS simple types and enumerated types
1057
+
1058
+ - **Context**: recent ABAP Cloud adds CDS table entities (`define table entity`: a table and its
1059
+ entity in one object, persistence of a RAP BO) and CDS types (`define type`, DRTY objects):
1060
+ simple types and enumerated types (`enum`). SAP does not expose enumerated types in RAP OData
1061
+ services ("Development Constraints" of RAP).
1062
+ - **Decision**:
1063
+ - **CDS types** (`<name>.drty.acds`, read by Open RAP Runtime: abaplint knows the object without
1064
+ parsing it): a simple type is worth its base type (built-in type, data element or another CDS
1065
+ type), with its label; an enumerated type has a base type (`abap.int1`, `int2`, `int4`,
1066
+ `char(1..8)`, `numc(1..8)`), exactly one `initial` constant, unique constants and values
1067
+ (TYPE001 to TYPE003). A type typed by an enumerated type takes its constants. They type the
1068
+ elements of the table entities, of the abstract and custom entities.
1069
+ - **Table entity**: a table with the same name (elements in lower case; `NOT NULL` unless
1070
+ `null`; keys first; `abap.string` declared `null`, CDS019-CDS020) and a view entity on this
1071
+ table. A managed BDEF on the table entity does not write `persistent table`: the table entity
1072
+ is its persistence. Views and projections read it as an entity; the ABAP SQL of the behavior
1073
+ pool reads and writes its table.
1074
+ - **Enumerated values**: stored in the database with their base value. In the behavior pool,
1075
+ the type is given to the transpiler as a data element of its base type and each
1076
+ `type-constant` constant becomes the literal of its value (same line, same meaning).
1077
+ - **OData**: in `standard` mode, an enumerated element in an exposed entity is an error
1078
+ (CDS021), as on SAP. In `enhanced` mode, it becomes an `EnumType` of the `$metadata`: members
1079
+ named like the constants, integer value (the base value when it is an integer, the position
1080
+ otherwise), label as `Core.Description`. The JSON carries the name of the member
1081
+ (`"Status": "OPEN"`); `$filter` accepts `Status eq 'OPEN'`, `Ns.Type'OPEN'` and the integer
1082
+ value; a value outside the type is refused (400). The UI shows the labels and offers a choice
1083
+ list (OpenUI5 cannot write enumeration literals: the filter goes through `$filter`).
1084
+ - `rapt generate tableentity|type|enum`, DDLS of type `T` for `rapt abapgit`. A CDS type is an
1085
+ object in the AFF format of abapGit (SAP/abap-file-formats repository): the source
1086
+ `<name>.drty.acds` and its metadata `<name>.drty.json` (`formatVersion`, description,
1087
+ language), written by `rapt abapgit` (`.drty.asdrty` is still read).
1088
+ - **Why**: the table entity is a complete BO in two files; enumerated types give named values to
1089
+ the API without changing the storage. The `standard` mode stays activatable on SAP, the
1090
+ exposure as an enumeration is an extension (D45).
1091
+ - **Tests**: `compiler/test/table-entity.test.ts`, `runtime/test/table-entity.test.ts` (OData,
1092
+ ABAP, browser), fixture `fixtures/table-entity`.
1093
+
1094
+ ## D56 — Value helps: bindings, validation, fixed values, action parameters
1095
+
1096
+ - **Context**: D44 gave the simple value help (`entity: { name, element }`); RAP BOs also use
1097
+ `additionalBinding`, `useForValidation` and drop-down lists.
1098
+ - **Decision**:
1099
+ - `additionalBinding: [{ localElement, element, usage }]`: `#FILTER` restricts the list by the
1100
+ local value (`Common.ValueListParameterIn`), `#RESULT` copies the element of the chosen row
1101
+ into the local element (`ValueListParameterOut`), without a usage both (`InOut`);
1102
+ - `useForValidation: true`: `Common.ValueListForValidation`; the UI reports an entered value
1103
+ missing from the list (error state of the field);
1104
+ - `@ObjectModel.resultSet.sizeCategory: #XS` on the entity of the value help:
1105
+ `Common.ValueListWithFixedValues`, and a drop-down list (value and description) in the UI;
1106
+ - the action parameters (abstract entity) have their value help in the dialog, if its entity is
1107
+ exposed by the service (as in RAP, which does not add it implicitly).
1108
+ - **Why**: they are the value help forms of common RAP BOs, and Fiori Elements interprets them
1109
+ from the `$metadata`.
1110
+ - **Tests**: `compiler/test/table-entity.test.ts` (CSDL), `runtime/test/table-entity.test.ts`
1111
+ (drop-down list that fills the currency, validation, in the browser),
1112
+ `runtime/test/standard-views.test.ts`.
1113
+
1114
+ ## D57 — Display annotations: criticality, texts, header, links, filters
1115
+
1116
+ - **Context**: the generated UI followed the structure annotations (`lineItem`,
1117
+ `identification`, `facet`…) but not the common display annotations of RAP BOs; the complete
1118
+ table is in [ANNOTATIONS.md](ANNOTATIONS.md).
1119
+ - **Decision**:
1120
+ - `criticality` (line item, identification, data point): colored `ObjectStatus` or
1121
+ `ObjectNumber` (1 negative, 2 critical, 3 positive);
1122
+ - `@ObjectModel.text.element` + `@UI.textArrangement`: "Text (ID)" (default, as in Fiori
1123
+ Elements), "ID (Text)", text only, separate;
1124
+ - `@UI.dataPoint` (`UI.DataPoint`, qualified by its `qualifier` or the name of the element) and
1125
+ `@UI.facet` purpose `#HEADER` (`#DATAPOINT_REFERENCE`, `#FIELDGROUP_REFERENCE`): header of
1126
+ the Object Page, progress bar (`#PROGRESS`, `targetValue`), rating (`#RATING`), number;
1127
+ - `@UI.multiLineText`: text area; `@Semantics.email.address`, `@Semantics.telephone.type`:
1128
+ `mailto:` / `tel:` links (`Communication` vocabulary); `#WITH_URL` line item: link;
1129
+ - `@Consumption.filter.defaultValue` (initial filter, applied by the standard variant),
1130
+ `mandatory` (mandatory filter, `FilterRestrictions`), `selectionType` /
1131
+ `multipleSelections` (one value);
1132
+ - `@Consumption.hidden`: element not exposed, except a key or the element of an association.
1133
+ - Fix: `@UI.facet: […]` then `@UI: { … }` on the same element keep both (the second
1134
+ annotation replaced the first one).
1135
+ - **Why**: they are the annotations the SAP examples (RAP100, Flight) use on each BO; they change
1136
+ what the user sees without changing the BO.
1137
+ - **Tests**: `compiler/test/table-entity.test.ts` (CSDL), `runtime/test/table-entity.test.ts`
1138
+ (browser), fixture `fixtures/table-entity`.
1139
+
1140
+ ## D58 — Draft UI as in Fiori Elements
1141
+
1142
+ - **Context**: the first draft UI (D54) edited and activated, but without the landmarks of Fiori
1143
+ Elements: the list showed the active instance even when the user had a draft, without an
1144
+ editing status filter nor a saving indicator.
1145
+ - **Decision**:
1146
+ - **List Report**: *Editing Status* filter (*All*, *Own Draft*, *Locked by Another User*,
1147
+ *No Changes*); *All* shows the drafts of the user instead of their active instances
1148
+ (`IsActiveEntity eq false or HasDraftEntity eq false or
1149
+ DraftAdministrativeData/InProcessByUser ne '<user>'`, like `SiblingEntity` at SAP); *Draft*
1150
+ and *Locked by X* markers (`ObjectMarker`). The active instance points to the administrative
1151
+ data of its draft (`DraftUUID`), as at SAP.
1152
+ - **Object Page**: *Draft* marker in the header, *Locked by X* on an instance another user is
1153
+ editing, `DraftIndicator` in the footer ("Saving draft…", "Draft updated"); *Discard Draft*
1154
+ asks "Discard all changes?"; leaving the page in draft offers *Keep Draft* / *Discard Draft*
1155
+ (the draft stays in the list).
1156
+ - The current user is given to the UI by `$permissions` (`$user`).
1157
+ - **Why**: a Fiori Elements user finds the same gestures; no SAP library (OpenUI5 controls
1158
+ `ObjectMarker`, `DraftIndicator`).
1159
+ - **Tests**: `runtime/test/ui-draft.test.ts` (browser).
1160
+
1161
+ ## D59 — Collaborative draft
1162
+
1163
+ - **Context**: `with collaborative draft;` and `draft action Share;` (ABAP Cloud) let several
1164
+ users edit the same draft; the users invited with `Share` or who join the draft change it,
1165
+ activate it or discard it.
1166
+ - **Decision**:
1167
+ - BDEF: `with collaborative draft;` in the header, `draft action Share;` mandatory (BDEF029),
1168
+ `use action Share;` in the projection.
1169
+ - Database: `rapt_draft_user` (draft, user, role `O` owner / `E` editor).
1170
+ - OData: action `Share(Users: Collection(DraftAdministrativeUserInput{UserID, UserAccessRole}),
1171
+ ShareAll)`, `Common.DraftRoot/ShareAction`, entity `DraftAdministrativeUser` (navigation
1172
+ `DraftAdministrativeData/DraftAdministrativeUser`).
1173
+ - Runtime: the creator is the owner; `Edit` of an instance whose draft another user has makes
1174
+ the user join the draft (instead of 409); every user of the draft reads it, changes it,
1175
+ activates it, discards it; the others receive 409 / 404 as for an exclusive draft.
1176
+ - UI: in draft, the users (avatars) and *Invite* (`Share`); the page reads the draft again
1177
+ every 5 seconds to show the changes of the others.
1178
+ - **Why**: the behavior of SAP (actions, entities, annotation) so that Fiori Elements and the
1179
+ OData clients find their way; the periodic refresh replaces the WebSockets (ABAP Channels) of
1180
+ SAP.
1181
+ - **Tests**: `runtime/test/draft-collaborative.test.ts` (OData, browser, BDEF029).
1182
+
1183
+ ## D60 — Web APIs: OData V4 - Web API binding, OAuth 2.0 (client credentials, PKCE)
1184
+
1185
+ - **Context**: a RAP service is also exposed as an API for other systems (*OData V4 - Web API*
1186
+ binding). The callers authenticate with OAuth 2.0: *client credentials* (machine to machine)
1187
+ or *authorization code* with PKCE (application on behalf of a user).
1188
+ - **Decision**:
1189
+ - **Binding**: `BINDING_TYPE_CATEGORY` 1 of the SRVB, or `--binding-type webapi`, or
1190
+ `rapt generate binding … --binding-type webapi`. As at SAP: no `UI.*` annotation, nor value
1191
+ help, nor side effects, nor feature control (`__EntityControl`…) in the service (the checks
1192
+ are still applied by the runtime); no generated UI; a draft BO only exposes its active
1193
+ instances (direct writes, without `IsActiveEntity`).
1194
+ - **Production** (`AUTH_MODE=oidc`): the service accepts the tokens of the IdP (Keycloak, SAP
1195
+ Cloud Identity Services, Entra ID…) obtained with either flow; a *client credentials* token
1196
+ without `preferred_username` designates its client (`client_id`) as a technical user; its
1197
+ roles can come from `scope` (`OIDC_ROLES_CLAIM=scope`). The browser sessions already used the
1198
+ PKCE flow.
1199
+ - **Development**: `rapt dev` (and an application started with `AUTH_MODE=none` and
1200
+ `DEV_OAUTH=on`) serves a development authorization server under `/oauth`: OIDC discovery,
1201
+ JWKS, `/oauth/token` (`client_credentials` with `rapt-client` / `rapt-secret`,
1202
+ `authorization_code` with S256 PKCE verification), `/oauth/authorize` (page to choose the user
1203
+ and their roles); configurable clients (`DEV_OAUTH_CLIENTS`). Its tokens (RS256, key generated
1204
+ on start) give a development user.
1205
+ - **Why**: test an API exactly as its clients will call it (Postman, Bruno, another system)
1206
+ without installing an IdP; in production, the IdP stays the only source of tokens.
1207
+ - **Tests**: `runtime/test/web-api.test.ts` (binding, client credentials, PKCE),
1208
+ `runtime/test/auth.test.ts` (client token).
1209
+
1210
+ ## D61 — Guided creation of the objects, floorplan, CLI / extension parity
1211
+
1212
+ - **Context**: ADT guides the creation of an object (*New ABAP Repository Object*: types by
1213
+ category, description, reference object); the extension only offered a series of quick picks,
1214
+ and the floorplan of the UI could only be chosen by editing `rapt/config.json`.
1215
+ - **Decision**:
1216
+ - **Catalog**: `authoring/catalog.ts` describes each object type (category, label,
1217
+ description, files, length of the name, fields: source object, exposed entities, binding type,
1218
+ built-in type); `rapt generate <src> --json` without a type returns it. The extension builds
1219
+ its wizard from it: the CLI stays the only source.
1220
+ - **Wizard** (*New RAP object* webview): filter, types by category, form of the type (the
1221
+ objects of the project offered as source), *Create* runs `rapt generate` (or `rapt new` for a
1222
+ complete BO) and opens the file. New types: data element and domain (`--type`).
1223
+ - ***RAP Objects* view**: the objects of the project grouped like the project explorer of ADT.
1224
+ - **Floorplan**: `rapt floorplan <src> [<EntitySet> <floorplan>]` reads and writes
1225
+ `ui.floorplans` of `rapt/config.json`; the VS Code command describes each floorplan (use,
1226
+ required annotations).
1227
+ - **Parity**: each command of the CLI has its VS Code command (`check`, `ir`, `build`,
1228
+ `launchpad`, `dev`, `new`, `abapgit`, `mcp`, `generate`, `floorplan`).
1229
+ - **Why**: an ABAP developer finds the gestures of ADT; the choice of the floorplan becomes
1230
+ explicit and documented at the moment it is made.
1231
+ - **Tests**: `vscode/test/rapt.test.ts` (catalog, arguments of the wizard, `rapt floorplan`,
1232
+ groups), wizard checked in Chromium.
1233
+
1234
+ ## D62 — Website of the project and download of the extension
1235
+
1236
+ - **Decision**: `site/` (static HTML and CSS, without a dependency nor a build tool) presents the
1237
+ project; the `site.yml` workflow publishes it on GitHub Pages with the screenshots of
1238
+ `docs/images` and the `.vsix` built from the same commit (*Download* button). The tagged
1239
+ versions are still published by `release.yml` (GitHub Release, Marketplace and Open VSX when
1240
+ their tokens are configured). To enable once: *Settings → Pages → Source: GitHub Actions*.
1241
+ - **Why**: one address to share, always up to date, without a server to maintain.
1242
+ - **Tests**: rendering checked in Chromium (desktop, mobile without horizontal scrolling, dark
1243
+ mode).
1244
+
1245
+ ## D63 — English everywhere; documentation pages on the website
1246
+
1247
+ - **Context**: the project was documented in French, and the website linked to pages of the
1248
+ GitHub repository, which is private: visitors could not read the guide.
1249
+ - **Decision**:
1250
+ - **English**: documentation, website, VS Code extension (commands, wizard, settings), object
1251
+ and floorplan catalog, messages and comments are in English (AGENTS.md rule 8). The documents
1252
+ with French names were renamed (`DEPLOYMENT.md`, `UTILITIES.md`, `REMOTE_APIS.md`).
1253
+ - **Documentation on the website**: `scripts/build-site.mjs` renders each Markdown document of
1254
+ the documentation app (same list and order as `rapt dev`, D49) into a static page
1255
+ `docs/<name>.html` of the website, with the Markdown renderer of the compiler, a navigation
1256
+ between the pages and the style of the website. Links between documents point to the rendered
1257
+ pages; links to files of the repository that are not published (sources, fixtures) are shown
1258
+ as plain text. The `site.yml` workflow runs the script after `pnpm build`. The website no
1259
+ longer links to GitHub.
1260
+ - **Why**: a private repository cannot be the documentation of a public website; rendering from
1261
+ the same Markdown keeps a single source, already shown in `rapt dev`.
1262
+ - **Tests**: `compiler/test/docs.test.ts` (rendering of the links for the website), website
1263
+ checked in Chromium (pages, navigation, mobile, dark mode).
1264
+
1265
+ ## D64 — rapt for AI coding agents: `rapt agent`, `rapt` in the terminals of VS Code
1266
+
1267
+ - **Context**: AI agents (GitHub Copilot in agent mode, Claude Code, Cursor…) write more and more
1268
+ of the RAP sources. With a shell, they can run `rapt check --json`; but in VS Code `rapt` was
1269
+ only inside the extension (not on the PATH), and the documentation of Open RAP Runtime (codes,
1270
+ annotations, known gaps and their workarounds) is not in the project of the user, so an agent
1271
+ could not find it. Also, the `.vsix` lost this documentation: the staging of the extension
1272
+ removed the `docs` folders and the `.md` files of the packages, and the documentation app of
1273
+ `rapt dev` was empty when started from the extension.
1274
+ - **Decision**:
1275
+ - **`rapt agent [<workspace>]`**: an MCP server over stdio (newline-delimited JSON-RPC) with the
1276
+ tools `rapt_projects`, `rapt_check` (diagnostics, without writing), `rapt_object_types` (the
1277
+ catalog of D61), `rapt_generate`, `rapt_new_business_object`, `rapt_floorplan`,
1278
+ `rapt_abapgit`, `rapt_docs_search` (sections of the documentation by relevance) and
1279
+ `rapt_docs_read` (a page or a section, as Markdown), and instructions given on
1280
+ `initialize` (check after each change, standard mode, read the documentation). The paths are
1281
+ relative to the workspace; a path outside it is refused. The project is given, or the only
1282
+ one of the workspace. Distinct from `rapt mcp` (D47), the MCP server of a *service*.
1283
+ - **VS Code**: the extension registers `rapt agent` for the agent mode of VS Code
1284
+ (`vscode.lm.registerMcpServerDefinitionProvider`, contribution `mcpServerDefinitionProviders`,
1285
+ one server per workspace folder, setting `rapt.agent.enabled`); and it puts `rapt` on the
1286
+ PATH of its integrated terminals (`environmentVariableCollection`): shims `rapt` (sh) and
1287
+ `rapt.cmd` (Windows) in the storage of the extension start the CLI the extension uses, with
1288
+ the Node.js of VS Code (setting `rapt.terminal.addToPath`). An agent of the terminal (Claude
1289
+ Code) then runs `rapt` or `claude mcp add rapt -- rapt agent .` without installing anything.
1290
+ - **Documentation in the `.vsix`**: the staging keeps `@open-rap/compiler/docs` and fails
1291
+ when it is missing; the documentation app shows "Documentation not found" instead of an empty
1292
+ list if the documentation is ever missing.
1293
+ - **Why an MCP server and not only the CLI**: the CLI with `--json` and the AGENTS.md of the
1294
+ project are enough for an agent with a shell; the MCP server adds what the CLI does not give:
1295
+ discovery without configuration in VS Code, typed inputs (the catalog of the objects as enums),
1296
+ and the documentation of Open RAP Runtime, searchable, wherever the extension or the npm package
1297
+ is installed. It reuses the functions of the CLI: one engine (D39).
1298
+ - **Tests**: `compiler/test/agent.test.ts` (protocol, business object created, checked, object
1299
+ generated, floorplan, abapGit, paths outside the workspace, several projects, documentation,
1300
+ stdio of the built CLI), `vscode/test/rapt.test.ts` (shims run `rapt check` through the PATH,
1301
+ server definitions), `compiler/test/docs.test.ts` (empty documentation); `.vsix` built and its
1302
+ `rapt dev` checked to serve the 14 documentation pages.
1303
+
1304
+ ## D65 — npm package `open-rap-runtime`
1305
+
1306
+ - **Context**: outside VS Code (CI pipelines, a terminal agent, another editor), rapt needed a
1307
+ clone of the repository.
1308
+ - **Decision**: the package `open-rap-runtime` (`packages/cli`) gives the `rapt` command
1309
+ (`npm install -g open-rap-runtime`, `npx open-rap-runtime check .`, or a `devDependency` of the
1310
+ project and `npx rapt …`). It depends on `@open-rap/compiler` and
1311
+ `@open-rap/runtime` and on what `rapt dev` serves (OpenUI5, PGlite, less-openui5, at the
1312
+ versions of the runtime, checked by a test), like the `rapt/` of the extension (D39). Its
1313
+ `dist/rapt.js` calls `main` of `@open-rap/compiler/cli` (new export); `cli.js` runs
1314
+ itself only when it is the program (`isEntryPoint`: real paths compared, symbolic links and
1315
+ Windows paths included), so the command runs once. The compiler no longer declares a `bin`:
1316
+ one package gives `rapt`. `COMPILER_VERSION` is read from the `package.json` of the compiler.
1317
+ Each package has a README for npm; `@open-rap/model`, `compiler` and `runtime` are also
1318
+ usable as libraries.
1319
+ - **Release**: the `npm` job of `release.yml` sets the version of the tag on the four packages,
1320
+ builds them, packs them (`pnpm pack`), installs the archives with npm in an empty folder and
1321
+ runs `rapt --help`, `rapt check` and `rapt build` of the fixture; then, when the secret
1322
+ `NPM_TOKEN` is set, publishes them (`workspace:*` becomes the version of the tag). npm
1323
+ provenance is added when the repository is public. Once: create the `open-rap-runtime`
1324
+ organization on npmjs.com and the `NPM_TOKEN` secret ([DEPLOYMENT.md](DEPLOYMENT.md)). The name
1325
+ `rapt` is taken on npm, hence `open-rap-runtime`.
1326
+ - **Why**: the same CLI everywhere; CI can check RAP sources without VS Code; a terminal agent
1327
+ can install it in one command.
1328
+ - **Note**: publishing makes the compiled code public on npm, as the `.vsix` of the website
1329
+ already does.
1330
+ - **Tests**: `cli/test/package.test.ts` (dependencies, a single run), `cli/test/cli.test.ts`
1331
+ (`rapt check`, `rapt agent` in the help), `compiler/test/cli.test.ts` (`isEntryPoint`); the four
1332
+ packages packed and installed with npm in an empty folder: `rapt check`, `rapt dev` (UI,
1333
+ OpenUI5, OData, documentation) work; `pnpm publish --dry-run`.
1334
+
1335
+ ## D66 — Menus of the extension: two ways to create, greyed-out commands, required floorplan
1336
+
1337
+ - **Context**: the right click of the explorer showed three creation entries (*New business
1338
+ object*, *New RAP object (wizard)…*, *New RAP object… (table, view…)*), all the commands were
1339
+ active even outside a project, and the application was generated as a List Report without the
1340
+ floorplan having been chosen.
1341
+ - **Decision**:
1342
+ - **Create**: two entries. *New complete business object…* (`rapt new`, possible in an empty
1343
+ folder) builds the whole BO; *New RAP object…* (the wizard, D61) creates object by object.
1344
+ The quick picks of D46 (`rapt.generate`) stay a command, outside the menus and the palette.
1345
+ - **Groups** of the menu: create, project (check, abapGit), application (floorplan, rapt dev,
1346
+ build); the *RAP Objects* view offers the same on a project, and a *New complete business
1347
+ object* button when the workspace has no project.
1348
+ - **Greyed out**: the extension computes the folders of the projects (`rapt.projects`) and
1349
+ those of the projects that have a service definition (`rapt.apps`) from the sources of the
1350
+ workspace, updated when a file is created or deleted. The commands have an `enablement`:
1351
+ `resourcePath in rapt.projects` (or `rapt.apps`) in the explorer, a project (or a service) in
1352
+ the workspace elsewhere; *Stop* and *Open the application* when rapt dev runs.
1353
+ - **Required floorplan**: *Build the application…* and *Start rapt dev* ask for the floorplan
1354
+ of each root entity set that has none; cancelling writes nothing and builds nothing.
1355
+ `rapt floorplan --json` gives `chosen` per entity set, and `rapt floorplan . Set listReport`
1356
+ writes the List Report in `rapt/config.json` (instead of removing it) to keep the choice. The
1357
+ CLI keeps the List Report as the default.
1358
+ - **Why**: one way to do each gesture, an impossible command is not offered, and the floorplan is
1359
+ a design choice of the application, not a default one suffers.
1360
+ - **Tests**: `vscode/test/rapt.test.ts` (folders of the projects and of the applications,
1361
+ `chosen`, List Report kept).
1362
+
1363
+ ## D67 — Expressions in the elements of the CDS views
1364
+
1365
+ - **Context**: a calculated element of the select list (`CASE`, `CAST`, calculation, function,
1366
+ literal) was refused (CDS002), while RAP views often have some (status text, total amount,
1367
+ duration).
1368
+ - **Decision**: the adapter turns the expression parsed by abaplint into a tree; the IR resolves
1369
+ and types it (`FieldModel.expression` instead of `column`); the SQL generator translates it to
1370
+ PostgreSQL in the view (`CAST(… AS type)` around the result, for a stable column type).
1371
+ - **Supported**: literals (string, integer, decimal, `abap.xxx'…'`), columns of the source,
1372
+ `$projection.Element`, `+ - * /` and parentheses, simple and searched `CASE`
1373
+ (`= <> < > <= >=`, `[NOT] LIKE`, `IS [NOT] NULL`, `AND`, `OR`, `NOT`), `CAST( x AS abap.xxx )`
1374
+ or to a data element, functions `concat`, `concat_with_space`, `substring`, `upper`, `lower`,
1375
+ `length`, `left`, `right`, `ltrim`, `rtrim`, `replace`, `coalesce`, `div`, `division`, `mod`,
1376
+ `abs`, `round`, `ceil`, `floor`, `dats_days_between`, `dats_add_days`, and
1377
+ `$session.system_date` (`current_date`). A projection takes the calculated elements of its
1378
+ view and accepts expressions itself on the elements of the projected entity.
1379
+ - **Typing**: the explicit `CAST` wins; otherwise, calculation on numbers (integers: the
1380
+ largest; decimals: precision and scale combined, 31 digits at most; `/`: `decfloat34`),
1381
+ string for `concat` (lengths added), common type of the branches of a `CASE`. When the type
1382
+ cannot be derived (`NULL` alone, `+` on a string), CDS022 asks for a `CAST`.
1383
+ - **ABAP semantics**: `CHAR` strings lose their trailing blanks in `concat` and `length`; `div`
1384
+ is an integer division, `division( a, b, d )` rounds to `d` decimals; `/` gives a decimal (no
1385
+ integer division of PostgreSQL).
1386
+ - **Read-only**: a calculated element cannot be a key (CDS022); it is neither in the mapping of
1387
+ the BDEF, nor in the draft table, nor in the writes. In OData, it is `Core.Computed` and
1388
+ ignored in the body of the requests (OData convention of computed properties).
1389
+ - **Drafts**: the draft view computes the expression on the values of the draft; a column of
1390
+ the source that the entity does not expose is `NULL` there. In the behavior pool (EML), the
1391
+ value read is the one of the active view: it reflects the last save, not the current changes
1392
+ of the transaction; a draft instance has the initial value there.
1393
+ - **Still CDS002** (message naming the construct): `$session.user`, `$session.client`,
1394
+ `$session.system_language` (the SQL views do not have the session of the request),
1395
+ association paths (`_Assoc.field`), associations in an expression, `BETWEEN`, aggregates, SQL
1396
+ functions not listed, parameters and path filters. Joins and `WHERE` unchanged.
1397
+ - **Why**: PostgreSQL computes on read, like the HANA database: `$filter` and `$orderby` on a
1398
+ calculated element work without code at runtime, and the value stays consistent with the data.
1399
+ - **Tests**: tree of the adapter, inferred types and diagnostics (CDS002, CDS004, CDS022), schema
1400
+ applied on PGlite and values read in the view and the projection, `$metadata`
1401
+ (`Core.Computed`), OData read, `PATCH` ignored and values computed on a draft then after
1402
+ activation.
1403
+
1404
+ ## D68 — Filter value help: *Search and Select* and *Define Conditions*
1405
+
1406
+ - **Context**: the filter fields of the List Report (and of the ALP) accepted typed conditions
1407
+ (D51) and opened, for a field with a value help, a list of values (D44); the dialog of SAP
1408
+ Fiori elements where conditions are composed (operator, values, exclusions) next to the
1409
+ selection in the value help was missing. The dialog of `sap.ui.comp` / `sap.ui.mdc` is SAPUI5
1410
+ only.
1411
+ - **Decision**:
1412
+ - the icon of each token filter field (all but booleans and enumerated types, which stay choice
1413
+ lists) opens a dialog built with `sap.m` (`controller/conditions.js`): a *Search and Select*
1414
+ tab (when the field has a value help: search, table with multiple or single selection,
1415
+ descriptions of the text view), a *Define Conditions* tab (one row per condition: operator,
1416
+ value(s), value help on the value, *Add*), and at the bottom the *Selected Items and
1417
+ Conditions* tokens;
1418
+ - operators as in Fiori elements: *equal to*, *contains*, *starts with*, *ends with*,
1419
+ *between*, *less than*, *less than or equal to*, *greater than*, *greater than or equal to*,
1420
+ *empty*, and their exclusions (*not equal to*, *does not contain*, *not between*, *not
1421
+ empty*…), filtered by type: text all of them, numbers and NUMC comparisons, dates and times
1422
+ comparisons and *empty* (date / time picker), UUID equal / not equal;
1423
+ - `$filter`: included conditions of a field with `or`, exclusions with `and not`, fields with
1424
+ `and`; *empty* of a text = `''` or `null`; date-time by whole days (UTC);
1425
+ - tokens written as in Fiori elements: value alone (with its description: "Euro (EUR)"),
1426
+ `!(=x)`, `*x*`, `a...b`, `<empty>`, `!(…)`; the key of the token is the condition (`=x`,
1427
+ `!(*x*)`) read again for the search and kept in the variants (earlier variants are still
1428
+ read, including the date ranges `{ from, to }`);
1429
+ - direct input: `!(…)`, `<empty>` and `a - b` (dates) in addition to D51; a single value is
1430
+ *contains* for a free text, *equal to* for a key or a field with a value help; dates are
1431
+ entered in the format of the language or in ISO;
1432
+ - `@Consumption.filter.selectionType` (`AllowedExpressions` already in the CSDL, D57):
1433
+ `#SINGLE` equal only, `#INTERVAL` comparisons and ranges (without patterns), a single
1434
+ condition when `multipleSelections: false` (without exclusions for `SingleRange`).
1435
+ - **Why**: it is the "match code" of the Fiori elements filters; keeping the selection in the
1436
+ value help and the conditions in the same dialog avoids two different gestures for one field.
1437
+ - **Limits**: no relative date operators (*today*, *last week*…); booleans and enumerated types
1438
+ keep their choice list; a value entered without a value help is shown without a description.
1439
+ - **Tests**: `compiler/test/ui.test.ts` (reading and writing of the conditions, operators by type
1440
+ and `selectionType`, `$filter`), `runtime/test/table-entity.test.ts` (dialog in Chromium:
1441
+ selection in the value help, exclusion, typed conditions, filtered rows).
1442
+
1443
+ ## D69 — One launchpad per workspace, applications added when they are created
1444
+
1445
+ - **Context**: `launchpad.json` was written by hand (D37) and only referenced applications
1446
+ already built (`build`). In a workspace of several applications (`rap-apps/helpdesk-tickets`,
1447
+ `rap-apps/project-tracker`…), you had to write it, build each application then the launchpad,
1448
+ and nothing said that only one was needed.
1449
+ - **Decision**:
1450
+ - **One launchpad per workspace**, titled "Launchpad". The CLI looks for `launchpad.json` in
1451
+ the folder of the project then its parents (4 levels, stopping after a folder that contains
1452
+ `.git`, never in the root of the disk, the home folder or the temporary folder); without it,
1453
+ it creates it in the **parent folder of the project** (the workspace), or in the project when
1454
+ it is the root of a git repository or when its parent is one of these system folders.
1455
+ `--launchpad <file>` designates another file. The extension always passes `--launchpad`: next
1456
+ to the saved `.code-workspace`, otherwise in the **first folder** of the workspace — a single
1457
+ launchpad, even for a multi-root workspace.
1458
+ - **Added on creation**: `rapt new` (complete BO) and `rapt floorplan <src> <Set> <floorplan>`
1459
+ (choosing the floorplan creates the Fiori application) add the application; `--no-launchpad`
1460
+ refrains from it; `rapt launchpad add <src>` adds it on demand (`--json`). The `apps` entry
1461
+ references the **sources** (`"source": "helpdesk-tickets"`, relative to the launchpad.json),
1462
+ its `id` comes from the name of the folder, its `url` (local start) takes the first free port
1463
+ from 8081. The tile goes into the "Applications" group (or the "Applications" section of the
1464
+ first page of the first space): title of the application, intent `<EntitySet>-manage`
1465
+ (`process` for a Worklist, `analyze` for an Analytical List Page, `create` for a Form Entry),
1466
+ icon and counter of the starting entity set. **Idempotent**: an application already present
1467
+ (same sources, or same id without sources) is not changed, nothing the user wrote is
1468
+ rewritten; an unreadable file is not overwritten.
1469
+ - **Build**: `rapt launchpad launchpad.json --out out/launchpad` also builds each application
1470
+ referenced by `source` in `out/launchpad/apps/<id>`; `docker-compose.yml` builds them from
1471
+ this folder (one PostgreSQL each) and the image of the launchpad ignores it. `source` and
1472
+ `build` together are refused (LPD009); `build` stays possible for an application built
1473
+ separately.
1474
+ - **Two deployment modes**: the **launchpad with all its applications** (`rapt launchpad`), or
1475
+ **one application alone** (`rapt build`), which does not depend on the launchpad: its shell
1476
+ bar only shows if `/launchpad/config` answers. The extension asks for the mode in *Build the
1477
+ application…* ("Launchpad (all the applications)" / "Application alone (standalone)");
1478
+ *Build the launchpad…* builds the one of the workspace (`rapt.hasLaunchpad`). `rapt dev` keeps
1479
+ its development launchpad (D49).
1480
+ - **Why**: the user creates applications, not configuration files; a single entry point per
1481
+ workspace, like the Fiori Launchpad of a system, and the choice of shipping one application
1482
+ alone stays open.
1483
+ - **Tests**: `compiler/test/launchpad.test.ts` (creation titled "Launchpad", idempotent addition
1484
+ without rewriting a changed tile, two projects of one folder in the same launchpad, `rapt new`,
1485
+ `--no-launchpad`, `--launchpad`, build of the applications with the launchpad, LPD009);
1486
+ `vscode/test/rapt.test.ts` (file of the workspace, `rapt floorplan` and `rapt launchpad add` in
1487
+ JSON).
1488
+
1489
+ ## D70 — Name: Open RAP Runtime
1490
+
1491
+ - **Context**: "rap-transpiler" described one step of the tool (abaplint transpiles the ABAP);
1492
+ the project is a runtime for RAP business objects outside SAP (OData V4, RAP managed runtime,
1493
+ UI, launchpad, MCP), with a compiler and a VS Code extension around it.
1494
+ - **Decision**: the project is named **Open RAP Runtime**. The npm package of the command line is
1495
+ `open-rap-runtime`, the libraries `@open-rap/model`, `@open-rap/compiler`, `@open-rap/runtime`
1496
+ (npm organization `open-rap`), the VS Code extension `open-rap-runtime-vscode` ("Open RAP
1497
+ Runtime"). Unchanged, so that existing projects keep working: the `rapt` command, the `rapt/`
1498
+ folder of the projects, the `.rapt/` build folder, the `rapt_*` tables, the `ZRAP_*` ABAP
1499
+ objects. The URL of the website follows the name of the GitHub repository, renamed
1500
+ `Open-Rap-Runtime` (https://clementringot.github.io/Open-Rap-Runtime/).
1501
+ - **Why**: "Open" says open source and without proprietary SAP libraries; "RAP Runtime" says
1502
+ what the application runs. The website states that the project is not affiliated with SAP SE.
1503
+ - **Tests**: the whole suite (package names, generated applications with `vendor/open-rap-*`,
1504
+ extension), the `.vsix` built and its `rapt dev` checked.
1505
+
1506
+ ## D71 — Feedback: a public feedback repository and a form on the website
1507
+
1508
+ - **Context**: the repository is private, so its issues are not open to the users; bug reports
1509
+ and feature requests had no place.
1510
+ - **Decision**: a public repository, `ClementRingot/open-rap-runtime-feedback`, holds the issues,
1511
+ with GitHub issue forms (`bug_report.yml`: component, description, steps, logs, version,
1512
+ environment; `feature_request.yml`: area, description, why, example, version; blank issues
1513
+ disabled). The website has a `feedback.html` page: a form (type, component, title,
1514
+ description, steps or use case, version, environment) that opens a new issue of the right form
1515
+ with its fields filled in (GitHub fills an issue form field from the query parameter of its
1516
+ id); the author reviews it and submits it on GitHub. The page reads `kind`, `version`,
1517
+ `environment`, `component`, `title` and `description` from its own query string: the VS Code
1518
+ command *RAP: Report a bug or request a feature…* opens it with the version of the extension
1519
+ and the environment (VS Code, operating system). The help of `rapt` and the footer of every page
1520
+ of the website link to it.
1521
+ - **Why**: no server to run (a static page and GitHub), issues that everyone can follow and
1522
+ answer, and reports that come with the version and the environment.
1523
+ - **Tests**: `vscode/test/rapt.test.ts` (URL of the form); the page checked in Chromium (required
1524
+ fields, prefill from the query string, URL of the issue with the fields of the bug form,
1525
+ fields of the feature form, mobile without horizontal scrolling).
1526
+
1527
+
1528
+ ## D72 — Extension builds: progress, floorplan prompts; the website serves the released .vsix
1529
+
1530
+ - **Context**: in VS Code, *Build the application* kept its progress notification spinning after
1531
+ the build: the message of the result (with its *Open folder* button) was awaited inside the
1532
+ progress, so the progress ended only once the message was answered; the user saw a build that
1533
+ never ended. Building the launchpad asks the floorplan of each application that has none, but
1534
+ the prompt named only the entity set, not the application. The website served a `.vsix` built
1535
+ from `main`, whose version (that of `packages/vscode/package.json`) was not the released one.
1536
+ - **Decision**: the progress covers the call of `rapt` only; the result is shown after it, without
1537
+ being awaited. Every application of the VS Code workspace (each project with a service
1538
+ definition) goes into the one launchpad of the workspace when the launchpad is built: they are
1539
+ added to `launchpad.json` (what is there is kept) before the build; only an application built
1540
+ alone (standalone) stays out of it. The choice reads "Launchpad of the workspace (3
1541
+ applications)" with the names of the applications, never the name of a folder that could be
1542
+ taken for one of them, and "<application> alone (standalone)". The floorplan prompt names the
1543
+ application: the name of its workspace folder,
1544
+ with the folder when they differ ("Helpdesk (Worklist) (helpdesk) — Ticket"); building the
1545
+ launchpad still asks, application by application, the floorplan of every root entity set
1546
+ without one (all of them when no application has a floorplan). The website serves the `.vsix`
1547
+ of the latest GitHub release under its stable name, with its version shown on the page
1548
+ (`%VSIX_VERSION%` of `scripts/build-site.mjs`); the Release workflow runs the Site workflow once
1549
+ the release exists (a release created with the `GITHUB_TOKEN` does not trigger the `release`
1550
+ event). Without a release, the `.vsix` is built from the commit. The website and the links of
1551
+ the project follow the name of the repository, `Open-Rap-Runtime`.
1552
+ - **Tests**: `vscode/test/rapt.test.ts` (name of a project in the prompts, applications of the
1553
+ workspace, URL of the feedback form); a launchpad of three projects (one already built, two without floorplan) under a folder
1554
+ with an accent built by `rapt launchpad` in 2 s.