@voxgig/sdkgen 0.44.0 → 1.0.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 (160) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/ReadmeEntity.js +9 -153
  3. package/dist/cmp/ReadmeEntity.js.map +1 -1
  4. package/dist/cmp/ReadmeIntro.js +9 -14
  5. package/dist/cmp/ReadmeIntro.js.map +1 -1
  6. package/dist/cmp/ReadmeModel.js +6 -4
  7. package/dist/cmp/ReadmeModel.js.map +1 -1
  8. package/dist/cmp/ReadmeOptions.js +9 -61
  9. package/dist/cmp/ReadmeOptions.js.map +1 -1
  10. package/dist/cmp/ReadmeRef.js +10 -1328
  11. package/dist/cmp/ReadmeRef.js.map +1 -1
  12. package/dist/sdkgen.d.ts +2 -2
  13. package/dist/sdkgen.js +2 -1
  14. package/dist/sdkgen.js.map +1 -1
  15. package/dist/utility.d.ts +2 -1
  16. package/dist/utility.js +9 -0
  17. package/dist/utility.js.map +1 -1
  18. package/package.json +3 -3
  19. package/project/.sdk/src/cmp/go/Config_go.ts +9 -4
  20. package/project/.sdk/src/cmp/go/Entity_go.ts +2 -2
  21. package/project/.sdk/src/cmp/go/Main_go.ts +8 -4
  22. package/project/.sdk/src/cmp/go/Package_go.ts +2 -2
  23. package/project/.sdk/src/cmp/go/ReadmeEntity_go.ts +138 -0
  24. package/project/.sdk/src/cmp/go/ReadmeExplanation_go.ts +2 -2
  25. package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +8 -5
  26. package/project/.sdk/src/cmp/go/ReadmeInstall_go.ts +2 -2
  27. package/project/.sdk/src/cmp/go/ReadmeIntro_go.ts +18 -0
  28. package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +8 -5
  29. package/project/.sdk/src/cmp/go/ReadmeOptions_go.ts +58 -0
  30. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +13 -9
  31. package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +354 -0
  32. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +8 -6
  33. package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +2 -2
  34. package/project/.sdk/src/cmp/go/TestDirect_go.ts +222 -41
  35. package/project/.sdk/src/cmp/go/TestEntity_go.ts +142 -60
  36. package/project/.sdk/src/cmp/go/Test_go.ts +2 -2
  37. package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +21 -4
  38. package/project/.sdk/src/cmp/js/Config_js.ts +18 -0
  39. package/project/.sdk/src/cmp/js/ReadmeEntity_js.ts +138 -0
  40. package/project/.sdk/src/cmp/js/ReadmeHowto_js.ts +11 -6
  41. package/project/.sdk/src/cmp/js/ReadmeIntro_js.ts +18 -0
  42. package/project/.sdk/src/cmp/js/ReadmeModel_js.ts +6 -3
  43. package/project/.sdk/src/cmp/js/ReadmeOptions_js.ts +58 -0
  44. package/project/.sdk/src/cmp/js/ReadmeQuick_js.ts +6 -4
  45. package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +384 -0
  46. package/project/.sdk/src/cmp/js/ReadmeTopQuick_js.ts +6 -4
  47. package/project/.sdk/src/cmp/js/TestDirect_js.ts +23 -12
  48. package/project/.sdk/src/cmp/js/TestEntity_js.ts +107 -74
  49. package/project/.sdk/src/cmp/js/fragment/Config.fragment.js +1 -5
  50. package/project/.sdk/src/cmp/lua/Config_lua.ts +9 -4
  51. package/project/.sdk/src/cmp/lua/Package_lua.ts +9 -2
  52. package/project/.sdk/src/cmp/lua/ReadmeEntity_lua.ts +138 -0
  53. package/project/.sdk/src/cmp/lua/ReadmeHowto_lua.ts +6 -3
  54. package/project/.sdk/src/cmp/lua/ReadmeIntro_lua.ts +18 -0
  55. package/project/.sdk/src/cmp/lua/ReadmeModel_lua.ts +6 -3
  56. package/project/.sdk/src/cmp/lua/ReadmeOptions_lua.ts +58 -0
  57. package/project/.sdk/src/cmp/lua/ReadmeQuick_lua.ts +6 -4
  58. package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +360 -0
  59. package/project/.sdk/src/cmp/lua/ReadmeTopQuick_lua.ts +6 -4
  60. package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +172 -29
  61. package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +120 -52
  62. package/project/.sdk/src/cmp/lua/fragment/Main.fragment.lua +20 -4
  63. package/project/.sdk/src/cmp/php/Config_php.ts +10 -8
  64. package/project/.sdk/src/cmp/php/Package_php.ts +7 -1
  65. package/project/.sdk/src/cmp/php/ReadmeEntity_php.ts +138 -0
  66. package/project/.sdk/src/cmp/php/ReadmeHowto_php.ts +6 -3
  67. package/project/.sdk/src/cmp/php/ReadmeIntro_php.ts +18 -0
  68. package/project/.sdk/src/cmp/php/ReadmeModel_php.ts +6 -3
  69. package/project/.sdk/src/cmp/php/ReadmeOptions_php.ts +58 -0
  70. package/project/.sdk/src/cmp/php/ReadmeQuick_php.ts +6 -4
  71. package/project/.sdk/src/cmp/php/ReadmeRef_php.ts +358 -0
  72. package/project/.sdk/src/cmp/php/ReadmeTopQuick_php.ts +6 -4
  73. package/project/.sdk/src/cmp/php/TestDirect_php.ts +171 -28
  74. package/project/.sdk/src/cmp/php/TestEntity_php.ts +126 -55
  75. package/project/.sdk/src/cmp/php/fragment/Main.fragment.php +17 -3
  76. package/project/.sdk/src/cmp/py/Config_py.ts +9 -4
  77. package/project/.sdk/src/cmp/py/Package_py.ts +8 -1
  78. package/project/.sdk/src/cmp/py/ReadmeEntity_py.ts +138 -0
  79. package/project/.sdk/src/cmp/py/ReadmeHowto_py.ts +6 -3
  80. package/project/.sdk/src/cmp/py/ReadmeIntro_py.ts +18 -0
  81. package/project/.sdk/src/cmp/py/ReadmeModel_py.ts +6 -3
  82. package/project/.sdk/src/cmp/py/ReadmeOptions_py.ts +58 -0
  83. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +9 -6
  84. package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +356 -0
  85. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +9 -6
  86. package/project/.sdk/src/cmp/py/TestDirect_py.ts +164 -27
  87. package/project/.sdk/src/cmp/py/TestEntity_py.ts +125 -51
  88. package/project/.sdk/src/cmp/py/fragment/Main.fragment.py +19 -4
  89. package/project/.sdk/src/cmp/rb/Config_rb.ts +9 -4
  90. package/project/.sdk/src/cmp/rb/Package_rb.ts +9 -2
  91. package/project/.sdk/src/cmp/rb/ReadmeEntity_rb.ts +138 -0
  92. package/project/.sdk/src/cmp/rb/ReadmeHowto_rb.ts +6 -3
  93. package/project/.sdk/src/cmp/rb/ReadmeIntro_rb.ts +18 -0
  94. package/project/.sdk/src/cmp/rb/ReadmeModel_rb.ts +6 -3
  95. package/project/.sdk/src/cmp/rb/ReadmeOptions_rb.ts +58 -0
  96. package/project/.sdk/src/cmp/rb/ReadmeQuick_rb.ts +6 -4
  97. package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +361 -0
  98. package/project/.sdk/src/cmp/rb/ReadmeTopQuick_rb.ts +6 -4
  99. package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +172 -29
  100. package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +120 -52
  101. package/project/.sdk/src/cmp/rb/fragment/Main.fragment.rb +19 -3
  102. package/project/.sdk/src/cmp/ts/Config_ts.ts +18 -0
  103. package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
  104. package/project/.sdk/src/cmp/ts/ReadmeEntity_ts.ts +138 -0
  105. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +11 -6
  106. package/project/.sdk/src/cmp/ts/ReadmeIntro_ts.ts +18 -0
  107. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +9 -5
  108. package/project/.sdk/src/cmp/ts/ReadmeOptions_ts.ts +58 -0
  109. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +6 -4
  110. package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +384 -0
  111. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +6 -4
  112. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +213 -42
  113. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +168 -75
  114. package/project/.sdk/src/cmp/ts/fragment/Config.fragment.ts +1 -5
  115. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +8 -1
  116. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +8 -2
  117. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +21 -1
  118. package/project/.sdk/tm/go/feature/test_feature.go +51 -3
  119. package/project/.sdk/tm/go/test/runner_test.go +106 -6
  120. package/project/.sdk/tm/go/test/sdk-test-control.json +19 -0
  121. package/project/.sdk/tm/go/utility/fetcher.go +10 -0
  122. package/project/.sdk/tm/go/utility/make_url.go +12 -0
  123. package/project/.sdk/tm/go/utility/prepare_auth.go +15 -1
  124. package/project/.sdk/tm/js/src/utility/PrepareAuthUtility.js +7 -1
  125. package/project/.sdk/tm/lua/feature/test_feature.lua +41 -3
  126. package/project/.sdk/tm/lua/test/runner.lua +74 -0
  127. package/project/.sdk/tm/lua/test/sdk-test-control.json +19 -0
  128. package/project/.sdk/tm/lua/utility/fetcher.lua +13 -0
  129. package/project/.sdk/tm/lua/utility/make_url.lua +16 -0
  130. package/project/.sdk/tm/lua/utility/prepare_auth.lua +9 -1
  131. package/project/.sdk/tm/php/feature/TestFeature.php +185 -43
  132. package/project/.sdk/tm/php/test/Runner.php +62 -0
  133. package/project/.sdk/tm/php/test/sdk-test-control.json +19 -0
  134. package/project/.sdk/tm/php/utility/Fetcher.php +132 -9
  135. package/project/.sdk/tm/php/utility/MakeUrl.php +16 -0
  136. package/project/.sdk/tm/php/utility/PrepareAuth.php +11 -1
  137. package/project/.sdk/tm/py/feature/test_feature.py +35 -3
  138. package/project/.sdk/tm/py/test/runner.py +60 -0
  139. package/project/.sdk/tm/py/test/sdk-test-control.json +19 -0
  140. package/project/.sdk/tm/py/utility/fetcher.py +13 -0
  141. package/project/.sdk/tm/py/utility/make_url.py +13 -0
  142. package/project/.sdk/tm/py/utility/prepare_auth.py +10 -1
  143. package/project/.sdk/tm/rb/feature/test_feature.rb +36 -3
  144. package/project/.sdk/tm/rb/test/runner.rb +46 -0
  145. package/project/.sdk/tm/rb/test/sdk-test-control.json +19 -0
  146. package/project/.sdk/tm/rb/utility/fetcher.rb +49 -28
  147. package/project/.sdk/tm/rb/utility/make_url.rb +16 -0
  148. package/project/.sdk/tm/rb/utility/prepare_auth.rb +8 -1
  149. package/project/.sdk/tm/ts/src/utility/MakeUrlUtility.ts +7 -8
  150. package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +7 -1
  151. package/project/.sdk/tm/ts/test/sdk-test-control.json +19 -0
  152. package/project/.sdk/tm/ts/test/utility.ts +120 -2
  153. package/src/cmp/ReadmeEntity.ts +11 -178
  154. package/src/cmp/ReadmeIntro.ts +11 -25
  155. package/src/cmp/ReadmeModel.ts +7 -5
  156. package/src/cmp/ReadmeOptions.ts +12 -74
  157. package/src/cmp/ReadmeRef.ts +11 -1372
  158. package/src/sdkgen.ts +2 -1
  159. package/src/utility.ts +12 -0
  160. /package/project/.sdk/tm/go/utility/{make_target.go → make_point.go} +0 -0
@@ -1,5 +1,8 @@
1
1
 
2
2
  import {
3
+ Model,
4
+ ModelEntity,
5
+ nom,
3
6
  depluralize,
4
7
  } from '@voxgig/apidef'
5
8
 
@@ -8,6 +11,7 @@ import {
8
11
  File,
9
12
  cmp,
10
13
  snakify,
14
+ isAuthActive,
11
15
  } from '@voxgig/sdkgen'
12
16
 
13
17
 
@@ -51,12 +55,20 @@ function normalizePathParams(
51
55
 
52
56
  const TestDirect = cmp(function TestDirect(props: any) {
53
57
  const ctx$ = props.ctx$
54
- const model = ctx$.model
58
+ const model: Model = ctx$.model
55
59
 
56
60
  const target = props.target
57
- const entity = props.entity
61
+ const entity: ModelEntity = props.entity
58
62
 
59
- const PROJECTNAME = model.Name.toUpperCase().replace(/[^A-Z_]/g, '_')
63
+ const PROJECTNAME = nom(model, 'Name').toUpperCase().replace(/[^A-Z_]/g, '_')
64
+
65
+ const authActive = isAuthActive(model)
66
+ const apikeyEnvEntry = authActive
67
+ ? `\n "${PROJECTNAME}_APIKEY": "NONE",`
68
+ : ''
69
+ const apikeyLiveField = authActive
70
+ ? `\n "apikey": env.get("${PROJECTNAME}_APIKEY"),`
71
+ : ''
60
72
 
61
73
  const opnames = Object.keys(entity.op)
62
74
  const hasLoad = opnames.includes('load')
@@ -71,13 +83,52 @@ const TestDirect = cmp(function TestDirect(props: any) {
71
83
 
72
84
  const loadPoint = loadOp?.points?.[0]
73
85
  const loadPath = loadPoint ? normalizePathParams(loadPoint.parts || [], loadPoint?.args?.params || [], loadPoint?.rename?.param) : ''
74
- const loadParams = loadPoint?.args?.params || []
86
+ const allLoadParams = loadPoint?.args?.params || []
87
+ // Some upstream OpenAPI specs declare a parameter as `in: path` even when
88
+ // that path has no `{name}` placeholder for it. Only path params that
89
+ // actually appear in the URL template should drive direct-test path-param
90
+ // setup and URL-substitution asserts; otherwise the SDK silently drops
91
+ // them and the URL-includes assert fails.
92
+ const _pathPlaceholders = new Set<string>()
93
+ for (const part of (loadPoint?.parts || [])) {
94
+ if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
95
+ _pathPlaceholders.add(part.slice(1, -1))
96
+ }
97
+ }
98
+ const _renameMap = (loadPoint?.rename?.param || {}) as Record<string, string>
99
+ const _renamedPlaceholders = new Set<string>()
100
+ for (const ph of _pathPlaceholders) {
101
+ _renamedPlaceholders.add(ph)
102
+ for (const [orig, renamed] of Object.entries(_renameMap)) {
103
+ if (renamed === ph) _renamedPlaceholders.add(orig)
104
+ }
105
+ }
106
+ const loadParams = allLoadParams.filter((p: any) =>
107
+ _renamedPlaceholders.has(p.name) || _renamedPlaceholders.has(p.orig))
75
108
 
76
109
  const listPoint = listOp?.points?.[0]
77
110
  const listPath = listPoint ? normalizePathParams(listPoint.parts || [], listPoint?.args?.params || [], listPoint?.rename?.param) : ''
78
111
  const listParams = listPoint?.args?.params || []
79
112
 
80
- const entidEnvVar = `${PROJECTNAME}_TEST_${entity.Name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID`
113
+ // Required query params with spec-provided examples — needed in live mode
114
+ // to satisfy API contracts (e.g. /v2018/history requires city/start/end).
115
+ const loadQuery = loadPoint?.args?.query || []
116
+ const loadLiveQueryEntries = loadQuery
117
+ .filter((q: any) => q.reqd && undefined !== q.example && null !== q.example)
118
+ const loadLiveQueryLines = loadLiveQueryEntries
119
+ .map((q: any) => ` query["${q.name}"] = ${JSON.stringify(q.example)}`)
120
+ .join('\n')
121
+
122
+ // Path params with spec-provided examples — when ALL load params have
123
+ // spec examples, prefer them over list-bootstrap.
124
+ const loadAllHaveExamples =
125
+ loadParams.length > 0 &&
126
+ loadParams.every((p: any) => undefined !== p.example && null !== p.example)
127
+ const loadExampleLines = loadAllHaveExamples
128
+ ? loadParams.map((p: any) => ` params["${p.name}"] = ${JSON.stringify(p.example)}`).join('\n')
129
+ : ''
130
+
131
+ const entidEnvVar = `${PROJECTNAME}_TEST_${nom(entity, 'NAME').replace(/[^A-Z_]/g, '_')}_ENTID`
81
132
 
82
133
  File({ name: 'test_' + entity.name + '_direct.' + target.ext }, () => {
83
134
 
@@ -97,12 +148,33 @@ class Test${entity.Name}Direct:
97
148
  `)
98
149
 
99
150
  if (hasList && listPoint) {
151
+ // Track idmap keys this list test consumes in live mode.
152
+ const listLiveIdKeys: string[] = listParams.map((lp: any) => {
153
+ return lp.name === 'id'
154
+ ? entity.name + '01'
155
+ : lp.name.replace(/_id$/, '') + '01'
156
+ })
157
+ const listSkipBlock = listLiveIdKeys.length > 0
158
+ ? ` if setup["live"]:
159
+ for _live_key in [${listLiveIdKeys.map(k => `"${k}"`).join(', ')}]:
160
+ if setup["idmap"].get(_live_key) is None:
161
+ # pytest already imported at module scope
162
+ pytest.skip(f"live test needs {_live_key} via *_ENTID env var (synthetic IDs only)")
163
+ return
164
+
165
+ `
166
+ : ''
100
167
  Content(` def test_should_direct_list_${entity.name}(self):
101
168
  setup = _${entity.name}_direct_setup([
102
169
  {"id": "direct01"},
103
170
  {"id": "direct02"},
104
171
  ])
105
- client = setup["client"]
172
+ _skip, _reason = runner.is_control_skipped("direct", "direct-list-${entity.name}", "live" if setup["live"] else "unit")
173
+ if _skip:
174
+ # pytest already imported at module scope
175
+ pytest.skip(_reason or "skipped via sdk-test-control.json")
176
+ return
177
+ ${listSkipBlock} client = setup["client"]
106
178
 
107
179
  `)
108
180
 
@@ -136,11 +208,24 @@ class Test${entity.Name}Direct:
136
208
  `)
137
209
  }
138
210
 
139
- Content(` assert err is None
140
- assert result["ok"] is True
141
- assert helpers.to_int(result["status"]) == 200
142
-
143
- if not setup["live"]:
211
+ Content(` if setup["live"]:
212
+ # Live mode is lenient: synthetic IDs frequently 4xx and the
213
+ # list-response shape varies wildly across public APIs. Skip
214
+ # rather than fail when the call doesn't return a usable list.
215
+ if err is not None:
216
+ pytest.skip(f"list call failed (likely synthetic IDs against live API): {err}")
217
+ return
218
+ if not result.get("ok"):
219
+ pytest.skip("list call not ok (likely synthetic IDs against live API)")
220
+ return
221
+ status = helpers.to_int(result["status"])
222
+ if status < 200 or status >= 300:
223
+ pytest.skip(f"expected 2xx status, got {status}")
224
+ return
225
+ else:
226
+ assert err is None
227
+ assert result["ok"] is True
228
+ assert helpers.to_int(result["status"]) == 200
144
229
  assert isinstance(result["data"], list)
145
230
  assert len(result["data"]) == 2
146
231
  assert len(setup["calls"]) == 1
@@ -149,18 +234,58 @@ class Test${entity.Name}Direct:
149
234
  }
150
235
 
151
236
  if (hasLoad && loadPoint) {
237
+ // Python's direct-load test has no list-bootstrap, so when load path
238
+ // params can't be filled (no spec examples + no override), skip cleanly.
239
+ const loadSkipBlock = (loadParams.length > 0 && !loadAllHaveExamples)
240
+ ? ` if setup["live"]:
241
+ # pytest already imported at module scope
242
+ pytest.skip("live direct-load needs real ID — set *_ENTID env var with real IDs to run")
243
+ return
244
+
245
+ `
246
+ : ''
152
247
  Content(` def test_should_direct_load_${entity.name}(self):
153
248
  setup = _${entity.name}_direct_setup({"id": "direct01"})
154
- client = setup["client"]
249
+ _skip, _reason = runner.is_control_skipped("direct", "direct-load-${entity.name}", "live" if setup["live"] else "unit")
250
+ if _skip:
251
+ # pytest already imported at module scope
252
+ pytest.skip(_reason or "skipped via sdk-test-control.json")
253
+ return
254
+ ${loadSkipBlock} client = setup["client"]
155
255
 
156
256
  `)
157
257
 
158
- if (loadParams.length > 0) {
258
+ const needsQuery = loadParams.length > 0 || loadLiveQueryLines !== ''
259
+ if (needsQuery) {
159
260
  Content(` params = {}
160
- if not setup["live"]:
261
+ query = {}
262
+ `)
263
+ if (loadAllHaveExamples) {
264
+ // Use spec-provided path-param examples in live mode.
265
+ Content(` if setup["live"]:
266
+ ${loadLiveQueryLines ? loadLiveQueryLines + '\n' : ''}${loadExampleLines}
267
+ else:
161
268
  `)
162
- for (let i = 0; i < loadParams.length; i++) {
163
- Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
269
+ for (let i = 0; i < loadParams.length; i++) {
270
+ Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
271
+ `)
272
+ }
273
+ } else if (loadParams.length > 0) {
274
+ if (loadLiveQueryLines) {
275
+ Content(` if setup["live"]:
276
+ ${loadLiveQueryLines}
277
+ `)
278
+ }
279
+ Content(` if not setup["live"]:
280
+ `)
281
+ for (let i = 0; i < loadParams.length; i++) {
282
+ Content(` params["${loadParams[i].name}"] = "direct0${i + 1}"
283
+ `)
284
+ }
285
+ } else if (loadLiveQueryLines) {
286
+ // Required-query only, no path params.
287
+ Content(` if setup["live"]:
288
+ ${loadLiveQueryLines}
164
289
  `)
165
290
  }
166
291
  }
@@ -170,20 +295,34 @@ class Test${entity.Name}Direct:
170
295
  "path": "${loadPath}",
171
296
  "method": "GET",
172
297
  `)
173
- if (loadParams.length > 0) {
298
+ if (needsQuery) {
174
299
  Content(` "params": params,
300
+ "query": query,
175
301
  `)
176
302
  } else {
177
303
  Content(` "params": {},
178
304
  `)
179
305
  }
180
306
  Content(` })
181
- assert err is None
182
- assert result["ok"] is True
183
- assert helpers.to_int(result["status"]) == 200
184
- assert result["data"] is not None
185
-
186
- if not setup["live"]:
307
+ if setup["live"]:
308
+ # Live mode is lenient: synthetic IDs frequently 4xx. Skip
309
+ # rather than fail when the load endpoint isn't reachable
310
+ # with the IDs we can construct from setup.idmap.
311
+ if err is not None:
312
+ pytest.skip(f"load call failed (likely synthetic IDs against live API): {err}")
313
+ return
314
+ if not result.get("ok"):
315
+ pytest.skip("load call not ok (likely synthetic IDs against live API)")
316
+ return
317
+ status = helpers.to_int(result["status"])
318
+ if status < 200 or status >= 300:
319
+ pytest.skip(f"expected 2xx status, got {status}")
320
+ return
321
+ else:
322
+ assert err is None
323
+ assert result["ok"] is True
324
+ assert helpers.to_int(result["status"]) == 200
325
+ assert result["data"] is not None
187
326
  if isinstance(result["data"], dict):
188
327
  assert result["data"]["id"] == "direct01"
189
328
  assert len(setup["calls"]) == 1
@@ -200,15 +339,13 @@ def _${entity.name}_direct_setup(mockres):
200
339
 
201
340
  env = runner.env_override({
202
341
  "${entidEnvVar}": {},
203
- "${PROJECTNAME}_TEST_LIVE": "FALSE",
204
- "${PROJECTNAME}_APIKEY": "NONE",
342
+ "${PROJECTNAME}_TEST_LIVE": "FALSE",${apikeyEnvEntry}
205
343
  })
206
344
 
207
345
  live = env.get("${PROJECTNAME}_TEST_LIVE") == "TRUE"
208
346
 
209
347
  if live:
210
- merged_opts = {
211
- "apikey": env.get("${PROJECTNAME}_APIKEY"),
348
+ merged_opts = {${apikeyLiveField}
212
349
  }
213
350
  client = ${model.const.Name}SDK(merged_opts)
214
351
  return {
@@ -7,7 +7,12 @@ import {
7
7
 
8
8
  import {
9
9
  KIT,
10
+ Model,
11
+ ModelEntity,
12
+ ModelEntityFlow,
13
+ ModelEntityFlowStep,
10
14
  getModelPath,
15
+ nom,
11
16
  } from '@voxgig/apidef'
12
17
 
13
18
 
@@ -18,34 +23,47 @@ import {
18
23
  each,
19
24
  buildIdNames,
20
25
  getMatchEntries,
26
+ isAuthActive,
21
27
  } from '@voxgig/sdkgen'
22
28
 
23
29
 
24
- type OpGen = (ctx: GenCtx, step: any, index: any) => void
25
-
30
+ // See TestEntity_ts.ts for the GenCtx/OpGen contract.
26
31
  type GenCtx = {
27
- model: any
28
- entity: any
29
- flow: any
32
+ model: Model
33
+ entity: ModelEntity
34
+ flow: ModelEntityFlow
30
35
  PROJUPPER: string
31
36
  }
32
37
 
38
+ type OpGen = (ctx: GenCtx, step: ModelEntityFlowStep, index: number) => void
39
+
33
40
 
34
41
  const TestEntity = cmp(function TestEntity(props: any) {
35
42
  const ctx$ = props.ctx$
36
- const model = ctx$.model
43
+ const model: Model = ctx$.model
37
44
 
38
45
  const target = props.target
39
- const entity = props.entity
46
+ const entity: ModelEntity = props.entity
40
47
 
41
- const basicflow = getModelPath(model, `main.${KIT}.flow.Basic${entity.Name}Flow`)
42
- const dobasic = basicflow && true === basicflow.active
48
+ const basicflow: ModelEntityFlow | undefined =
49
+ getModelPath(model, `main.${KIT}.flow.Basic${nom(entity, 'Name')}Flow`)
43
50
 
44
- if (!dobasic) {
51
+ // No flow or flow inactive — nothing to generate. The narrowed-form
52
+ // check (rather than `if (!dobasic)`) is what lets TS know `basicflow`
53
+ // is non-null in the rest of the cmp body.
54
+ if (null == basicflow || true !== basicflow.active) {
45
55
  return
46
56
  }
47
57
 
48
- const PROJUPPER = model.const.Name.toUpperCase().replace(/[^A-Z_]/g, '_')
58
+ const PROJUPPER = nom(model.const, 'Name').toUpperCase().replace(/[^A-Z_]/g, '_')
59
+
60
+ const authActive = isAuthActive(model)
61
+ const apikeyEnvEntry = authActive
62
+ ? `\n "${PROJUPPER}_APIKEY": "NONE",`
63
+ : ''
64
+ const apikeyLiveField = authActive
65
+ ? `\n "apikey": env.get("${PROJUPPER}_APIKEY"),`
66
+ : ''
49
67
 
50
68
  const idnames = buildIdNames(entity, basicflow)
51
69
  const idnamesStr = idnames.map(n => `"${n}"`).join(', ')
@@ -86,6 +104,20 @@ class Test${entity.Name}Entity:
86
104
 
87
105
  def test_should_run_basic_flow(self):
88
106
  setup = _${entity.name}_basic_setup(None)
107
+ # Per-op sdk-test-control.json skip — basic test exercises a flow with
108
+ # multiple ops; skipping any one skips the whole flow (steps depend
109
+ # on each other).
110
+ _live = setup.get("live", False)
111
+ for _op in [${(Array.from(new Set((basicflow.step as any[]).map((s: any) => s.op).filter(Boolean)))).map(o => `"${o}"`).join(', ')}]:
112
+ _skip, _reason = runner.is_control_skipped("entityOp", "${entity.name}." + _op, "live" if _live else "unit")
113
+ if _skip:
114
+ pytest.skip(_reason or "skipped via sdk-test-control.json")
115
+ return
116
+ # The basic flow consumes synthetic IDs from the fixture. In live mode
117
+ # without an *_ENTID env override, those IDs hit the live API and 4xx.
118
+ if setup.get("synthetic_only"):
119
+ pytest.skip("live entity test uses synthetic IDs from fixture — "
120
+ "set ${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID JSON to run live")
89
121
  client = setup["client"]
90
122
 
91
123
  `)
@@ -144,11 +176,17 @@ def _${entity.name}_basic_setup(extra):
144
176
 
145
177
  `)
146
178
 
147
- Content(` env = runner.env_override({
179
+ Content(` # Detect ENTID env override before envOverride consumes it. When live
180
+ # mode is on without a real override, the basic test runs against synthetic
181
+ # IDs from the fixture and 4xx's. We surface this so the test can skip.
182
+ _entid_env_raw = os.environ.get(
183
+ "${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID")
184
+ _idmap_overridden = _entid_env_raw is not None and _entid_env_raw.strip().startswith("{")
185
+
186
+ env = runner.env_override({
148
187
  "${PROJUPPER}_TEST_${entity.name.toUpperCase().replace(/[^A-Z_]/g, '_')}_ENTID": idmap,
149
188
  "${PROJUPPER}_TEST_LIVE": "FALSE",
150
- "${PROJUPPER}_TEST_EXPLAIN": "FALSE",
151
- "${PROJUPPER}_APIKEY": "NONE",
189
+ "${PROJUPPER}_TEST_EXPLAIN": "FALSE",${apikeyEnvEntry}
152
190
  })
153
191
 
154
192
  idmap_resolved = helpers.to_map(
@@ -167,19 +205,21 @@ def _${entity.name}_basic_setup(extra):
167
205
  Content(`
168
206
  if env.get("${PROJUPPER}_TEST_LIVE") == "TRUE":
169
207
  merged_opts = vs.merge([
170
- {
171
- "apikey": env.get("${PROJUPPER}_APIKEY"),
208
+ {${apikeyLiveField}
172
209
  },
173
210
  extra or {},
174
211
  ])
175
212
  client = ${model.const.Name}SDK(helpers.to_map(merged_opts))
176
213
 
214
+ _live = env.get("${PROJUPPER}_TEST_LIVE") == "TRUE"
177
215
  return {
178
216
  "client": client,
179
217
  "data": entity_data,
180
218
  "idmap": idmap_resolved,
181
219
  "env": env,
182
220
  "explain": env.get("${PROJUPPER}_TEST_EXPLAIN") == "TRUE",
221
+ "live": _live,
222
+ "synthetic_only": _live and not _idmap_overridden,
183
223
  "now": int(time.time() * 1000),
184
224
  }
185
225
  `)
@@ -189,9 +229,9 @@ def _${entity.name}_basic_setup(extra):
189
229
 
190
230
  const generateCreate: OpGen = (ctx, step, index) => {
191
231
  const { entity, flow } = ctx
192
- const ref = step.input?.ref ?? entity.name + '_ref01'
193
- const entvar = step.input?.entvar ?? ref + '_ent'
194
- const datavar = step.input?.datavar ?? (ref + '_data' + (step.input?.suffix ?? ''))
232
+ const ref = step.input.ref ?? entity.name + '_ref01'
233
+ const entvar = step.input.entvar ?? ref + '_ent'
234
+ const datavar = step.input.datavar ?? (ref + '_data' + (step.input.suffix ?? ''))
195
235
 
196
236
  const priorSteps = Object.values(flow.step).slice(0, Number(index)) as any[]
197
237
  const needsEnt = !priorSteps.some((s: any) =>
@@ -199,8 +239,8 @@ const generateCreate: OpGen = (ctx, step, index) => {
199
239
 
200
240
  const hasDatvar = priorSteps.some((s: any) => {
201
241
  if ('create' === s.op) {
202
- const priorRef = s.input?.ref ?? entity.name + '_ref01'
203
- const priorDatvar = s.input?.datavar ?? (priorRef + '_data' + (s.input?.suffix ?? ''))
242
+ const priorRef = s.input.ref ?? entity.name + '_ref01'
243
+ const priorDatvar = s.input.datavar ?? (priorRef + '_data' + (s.input.suffix ?? ''))
204
244
  return priorDatvar === datavar
205
245
  }
206
246
  return false
@@ -229,22 +269,27 @@ const generateCreate: OpGen = (ctx, step, index) => {
229
269
  `)
230
270
  }
231
271
 
272
+ const hasEntIdC = null != ctx.entity.id
273
+
232
274
  Content(`
233
275
  ${datavar}_result, err = ${entvar}.create(${datavar}, None)
234
276
  assert err is None
235
277
  ${datavar} = helpers.to_map(${datavar}_result)
236
278
  assert ${datavar} is not None
237
- assert ${datavar}["id"] is not None
238
279
  `)
280
+ if (hasEntIdC) {
281
+ Content(` assert ${datavar}["id"] is not None
282
+ `)
283
+ }
239
284
  }
240
285
 
241
286
 
242
287
  const generateList: OpGen = (ctx, step, index) => {
243
288
  const { entity, flow } = ctx
244
- const ref = step.input?.ref ?? entity.name + '_ref01'
245
- const entvar = step.input?.entvar ?? ref + '_ent'
246
- const matchvar = step.input?.matchvar ?? (ref + '_match' + (step.input?.suffix ?? ''))
247
- const listvar = step.input?.listvar ?? (ref + '_list' + (step.input?.suffix ?? ''))
289
+ const ref = step.input.ref ?? entity.name + '_ref01'
290
+ const entvar = step.input.entvar ?? ref + '_ent'
291
+ const matchvar = step.input.matchvar ?? (ref + '_match' + (step.input.suffix ?? ''))
292
+ const listvar = step.input.listvar ?? (ref + '_list' + (step.input.suffix ?? ''))
248
293
 
249
294
  const priorSteps = Object.values(flow.step).slice(0, Number(index)) as any[]
250
295
  const needsEnt = !priorSteps.some((s: any) =>
@@ -284,7 +329,7 @@ const generateList: OpGen = (ctx, step, index) => {
284
329
  for (const validator of step.valid) {
285
330
  const validRef = validator.def?.ref
286
331
  const hasRefData = validRef && allSteps.some((s: any) => 'create' === s.op &&
287
- ((s.input?.ref ?? entity.name + '_ref01') === validRef))
332
+ ((s.input.ref ?? entity.name + '_ref01') === validRef))
288
333
 
289
334
  if ('ItemExists' === validator.apply && hasRefData) {
290
335
  const refDataVar = validRef + '_data'
@@ -310,17 +355,19 @@ const generateList: OpGen = (ctx, step, index) => {
310
355
 
311
356
  const generateUpdate: OpGen = (ctx, step, index) => {
312
357
  const { entity, flow } = ctx
313
- const ref = step.input?.ref ?? entity.name + '_ref01'
314
- const entvar = step.input?.entvar ?? ref + '_ent'
315
- const datavar = step.input?.datavar ?? (ref + '_data' + (step.input?.suffix ?? ''))
316
- const resdatavar = step.input?.resdatavar ?? (ref + '_resdata' + (step.input?.suffix ?? ''))
317
- const markdefvar = step.input?.markdefvar ?? (ref + '_markdef' + (step.input?.suffix ?? ''))
318
- const srcdatavar = step.input?.srcdatavar ?? (ref + '_data' + (step.input?.suffix ?? ''))
358
+ const ref = step.input.ref ?? entity.name + '_ref01'
359
+ const entvar = step.input.entvar ?? ref + '_ent'
360
+ const datavar = step.input.datavar ?? (ref + '_data' + (step.input.suffix ?? ''))
361
+ const resdatavar = step.input.resdatavar ?? (ref + '_resdata' + (step.input.suffix ?? ''))
362
+ const markdefvar = step.input.markdefvar ?? (ref + '_markdef' + (step.input.suffix ?? ''))
363
+ const srcdatavar = step.input.srcdatavar ?? (ref + '_data' + (step.input.suffix ?? ''))
319
364
 
320
365
  const priorSteps = Object.values(flow.step).slice(0, Number(index)) as any[]
321
366
  const needsEnt = !priorSteps.some((s: any) =>
322
367
  ['create', 'list', 'load', 'update', 'remove'].includes(s.op))
323
368
 
369
+ const hasEntIdU = null != entity.id
370
+
324
371
  Content(` # UPDATE
325
372
  `)
326
373
  if (needsEnt) {
@@ -328,8 +375,11 @@ const generateUpdate: OpGen = (ctx, step, index) => {
328
375
  `)
329
376
  }
330
377
  Content(` ${datavar}_up = {
331
- "id": ${srcdatavar}["id"],
332
378
  `)
379
+ if (hasEntIdU) {
380
+ Content(` "id": ${srcdatavar}["id"],
381
+ `)
382
+ }
333
383
 
334
384
  if (step.data) {
335
385
  const dataEntries = Object.entries(step.data).filter(([k]: any) => k !== 'id' && !k.endsWith('$'))
@@ -344,7 +394,7 @@ const generateUpdate: OpGen = (ctx, step, index) => {
344
394
 
345
395
  if (step.spec) {
346
396
  for (const spec of step.spec) {
347
- if ('TextFieldMark' === spec.apply && null != step.input?.textfield) {
397
+ if ('TextFieldMark' === spec.apply && null != step.input.textfield) {
348
398
  const fieldname = step.input.textfield
349
399
  const fieldvalue = spec.def?.mark ?? `Mark01-${ref}`
350
400
  Content(`
@@ -361,12 +411,15 @@ const generateUpdate: OpGen = (ctx, step, index) => {
361
411
  assert err is None
362
412
  ${resdatavar} = helpers.to_map(${resdatavar}_result)
363
413
  assert ${resdatavar} is not None
364
- assert ${resdatavar}["id"] == ${datavar}_up["id"]
365
414
  `)
415
+ if (hasEntIdU) {
416
+ Content(` assert ${resdatavar}["id"] == ${datavar}_up["id"]
417
+ `)
418
+ }
366
419
 
367
420
  if (step.spec) {
368
421
  for (const spec of step.spec) {
369
- if ('TextFieldMark' === spec.apply && null != step.input?.textfield) {
422
+ if ('TextFieldMark' === spec.apply && null != step.input.textfield) {
370
423
  Content(` assert ${resdatavar}[${markdefvar}_name] == ${markdefvar}_value
371
424
  `)
372
425
  }
@@ -377,11 +430,11 @@ const generateUpdate: OpGen = (ctx, step, index) => {
377
430
 
378
431
  const generateLoad: OpGen = (ctx, step, index) => {
379
432
  const { entity, flow } = ctx
380
- const ref = step.input?.ref ?? entity.name + '_ref01'
381
- const entvar = step.input?.entvar ?? ref + '_ent'
382
- const matchvar = step.input?.matchvar ?? (ref + '_match' + (step.input?.suffix ?? ''))
383
- const datavar = step.input?.datavar ?? (ref + '_data' + (step.input?.suffix ?? ''))
384
- const srcdatavar = step.input?.srcdatavar ?? (ref + '_data' + (step.input?.suffix ?? ''))
433
+ const ref = step.input.ref ?? entity.name + '_ref01'
434
+ const entvar = step.input.entvar ?? ref + '_ent'
435
+ const matchvar = step.input.matchvar ?? (ref + '_match' + (step.input.suffix ?? ''))
436
+ const datavar = step.input.datavar ?? (ref + '_data' + (step.input.suffix ?? ''))
437
+ const srcdatavar = step.input.srcdatavar ?? (ref + '_data' + (step.input.suffix ?? ''))
385
438
 
386
439
  const priorSteps = Object.values(flow.step).slice(0, Number(index)) as any[]
387
440
  const hasEntVar = priorSteps.some((s: any) =>
@@ -392,20 +445,22 @@ const generateLoad: OpGen = (ctx, step, index) => {
392
445
  const hasSrcData = (!flowHasCreate && srcdatavar === (preambleRef + '_data')) ||
393
446
  priorSteps.some((s: any) => {
394
447
  if ('create' === s.op) {
395
- const priorRef = s.input?.ref ?? entity.name + '_ref01'
396
- const priorDatvar = s.input?.datavar ?? (priorRef + '_data' + (s.input?.suffix ?? ''))
448
+ const priorRef = s.input.ref ?? entity.name + '_ref01'
449
+ const priorDatvar = s.input.datavar ?? (priorRef + '_data' + (s.input.suffix ?? ''))
397
450
  return priorDatvar === srcdatavar
398
451
  }
399
452
  return false
400
453
  })
401
454
 
455
+ const hasEntId = null != entity.id
456
+
402
457
  Content(` # LOAD
403
458
  `)
404
459
  if (!hasEntVar) {
405
460
  Content(` ${entvar} = client.${entity.Name}(None)
406
461
  `)
407
462
  }
408
- if (!hasSrcData) {
463
+ if (!hasSrcData && hasEntId) {
409
464
  Content(` ${srcdatavar}_raw = vs.items(helpers.to_map(
410
465
  vs.getpath(setup["data"], "existing.${entity.name}")))
411
466
  ${srcdatavar} = None
@@ -413,7 +468,8 @@ const generateLoad: OpGen = (ctx, step, index) => {
413
468
  ${srcdatavar} = helpers.to_map(${srcdatavar}_raw[0][1])
414
469
  `)
415
470
  }
416
- Content(` ${matchvar} = {
471
+ if (hasEntId) {
472
+ Content(` ${matchvar} = {
417
473
  "id": ${srcdatavar}["id"],
418
474
  }
419
475
  ${datavar}_loaded, err = ${entvar}.load(${matchvar}, None)
@@ -422,32 +478,50 @@ const generateLoad: OpGen = (ctx, step, index) => {
422
478
  assert ${datavar}_load_result is not None
423
479
  assert ${datavar}_load_result["id"] == ${srcdatavar}["id"]
424
480
  `)
481
+ }
482
+ else {
483
+ Content(` ${matchvar} = {}
484
+ ${datavar}_loaded, err = ${entvar}.load(${matchvar}, None)
485
+ assert err is None
486
+ assert ${datavar}_loaded is not None
487
+ `)
488
+ }
425
489
  }
426
490
 
427
491
 
428
492
  const generateRemove: OpGen = (ctx, step, index) => {
429
493
  const { entity, flow } = ctx
430
- const ref = step.input?.ref ?? entity.name + '_ref01'
431
- const entvar = step.input?.entvar ?? ref + '_ent'
432
- const matchvar = step.input?.matchvar ?? (ref + '_match' + (step.input?.suffix ?? ''))
433
- const srcdatavar = step.input?.srcdatavar ?? (ref + '_data')
494
+ const ref = step.input.ref ?? entity.name + '_ref01'
495
+ const entvar = step.input.entvar ?? ref + '_ent'
496
+ const matchvar = step.input.matchvar ?? (ref + '_match' + (step.input.suffix ?? ''))
497
+ const srcdatavar = step.input.srcdatavar ?? (ref + '_data')
434
498
 
435
499
  const priorSteps = Object.values(flow.step).slice(0, Number(index)) as any[]
436
500
  const needsEnt = !priorSteps.some((s: any) =>
437
501
  ['create', 'list', 'load', 'update', 'remove'].includes(s.op))
438
502
 
503
+ const hasEntIdR = null != entity.id
504
+
439
505
  Content(` # REMOVE
440
506
  `)
441
507
  if (needsEnt) {
442
508
  Content(` ${entvar} = client.${entity.Name}(None)
443
509
  `)
444
510
  }
445
- Content(` ${matchvar} = {
511
+ if (hasEntIdR) {
512
+ Content(` ${matchvar} = {
446
513
  "id": ${srcdatavar}["id"],
447
514
  }
448
515
  _, err = ${entvar}.remove(${matchvar}, None)
449
516
  assert err is None
450
517
  `)
518
+ }
519
+ else {
520
+ Content(` ${matchvar} = {}
521
+ _, err = ${entvar}.remove(${matchvar}, None)
522
+ assert err is None
523
+ `)
524
+ }
451
525
  }
452
526
 
453
527