@voxgig/sdkgen 4.2.5 → 4.2.7

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 (71) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/helpers/naming.js +5 -4
  3. package/dist/helpers/naming.js.map +1 -1
  4. package/dist/tsconfig.tsbuildinfo +1 -1
  5. package/package.json +1 -1
  6. package/project/.sdk/model/feature/cost.aon +43 -0
  7. package/project/.sdk/model/feature/feature-index.aon +1 -0
  8. package/project/.sdk/src/cmp/go/utility_go.ts +20 -7
  9. package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +2 -1
  10. package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +2 -1
  11. package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +2 -1
  12. package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +2 -1
  13. package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +2 -1
  14. package/project/.sdk/src/cmp/js/utility_js.ts +20 -7
  15. package/project/.sdk/src/cmp/php/ReadmeExamplesTest_php.ts +144 -12
  16. package/project/.sdk/src/cmp/py/ReadmeExamplesTest_py.ts +98 -3
  17. package/project/.sdk/src/cmp/rb/ReadmeExamplesTest_rb.ts +83 -10
  18. package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.ts +2 -1
  19. package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.ts +2 -1
  20. package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.ts +2 -1
  21. package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.ts +2 -1
  22. package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.ts +2 -1
  23. package/project/.sdk/src/cmp/ts/utility_ts.ts +20 -7
  24. package/project/.sdk/tm/c/core/sdk.h +1 -0
  25. package/project/.sdk/tm/c/feature/cost.c +549 -0
  26. package/project/.sdk/tm/c/src/feature/cost/.gitkeep +0 -0
  27. package/project/.sdk/tm/clojure/src/feature/cost/.gitkeep +0 -0
  28. package/project/.sdk/tm/cpp/feature/cost.hpp +419 -0
  29. package/project/.sdk/tm/cpp/src/feature/cost/.gitkeep +0 -0
  30. package/project/.sdk/tm/csharp/feature/CostFeature.cs +518 -0
  31. package/project/.sdk/tm/csharp/src/feature/cost/.gitkeep +0 -0
  32. package/project/.sdk/tm/dart/lib/feature/cost/CostFeature.dart +446 -0
  33. package/project/.sdk/tm/dart/src/feature/cost/.gitkeep +0 -0
  34. package/project/.sdk/tm/elixir/lib/projectname/feature/cost.ex +442 -0
  35. package/project/.sdk/tm/elixir/src/feature/cost/.gitkeep +0 -0
  36. package/project/.sdk/tm/go/feature/cost_feature.go +424 -0
  37. package/project/.sdk/tm/go/src/feature/cost/.gitkeep +0 -0
  38. package/project/.sdk/tm/go-cli/src/feature/cost/.gitkeep +0 -0
  39. package/project/.sdk/tm/go-mcp/src/feature/cost/.gitkeep +0 -0
  40. package/project/.sdk/tm/java/feature/CostFeature.java +431 -0
  41. package/project/.sdk/tm/java/src/feature/cost/.gitkeep +0 -0
  42. package/project/.sdk/tm/js/src/feature/cost/CostFeature.js +413 -0
  43. package/project/.sdk/tm/js/test/feature/Corpus.test.js +264 -0
  44. package/project/.sdk/tm/kotlin/feature/CostFeature.kt +430 -0
  45. package/project/.sdk/tm/kotlin/src/feature/cost/.gitkeep +0 -0
  46. package/project/.sdk/tm/lean/src/feature/cost/.gitkeep +0 -0
  47. package/project/.sdk/tm/lua/feature/cost_feature.lua +457 -0
  48. package/project/.sdk/tm/lua/src/feature/cost/.gitkeep +0 -0
  49. package/project/.sdk/tm/ocaml/src/feature/cost/.gitkeep +0 -0
  50. package/project/.sdk/tm/perl/feature/cost_feature.pm +446 -0
  51. package/project/.sdk/tm/perl/src/feature/cost/.gitkeep +0 -0
  52. package/project/.sdk/tm/php/feature/CostFeature.php +439 -0
  53. package/project/.sdk/tm/php/feature/TestFeature.php +31 -1
  54. package/project/.sdk/tm/php/src/feature/cost/.gitkeep +0 -0
  55. package/project/.sdk/tm/py/pkg/feature/cost_feature.py +381 -0
  56. package/project/.sdk/tm/py/src/feature/cost/.gitkeep +0 -0
  57. package/project/.sdk/tm/py-data/src/feature/cost/.gitkeep +0 -0
  58. package/project/.sdk/tm/rb/feature/cost_feature.rb +386 -0
  59. package/project/.sdk/tm/rb/src/feature/cost/.gitkeep +0 -0
  60. package/project/.sdk/tm/rust/feature/cost.rs +467 -0
  61. package/project/.sdk/tm/rust/src/feature/cost/.gitkeep +0 -0
  62. package/project/.sdk/tm/scala/feature/CostFeature.scala +388 -0
  63. package/project/.sdk/tm/scala/src/feature/cost/.gitkeep +0 -0
  64. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/CostFeature.swift +425 -0
  65. package/project/.sdk/tm/swift/src/feature/cost/.gitkeep +0 -0
  66. package/project/.sdk/tm/ts/src/feature/cost/CostFeature.ts +415 -0
  67. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +266 -0
  68. package/project/.sdk/tm/zig/feature/cost.zig +457 -0
  69. package/project/.sdk/tm/zig/src/feature/cost/.gitkeep +0 -0
  70. package/project/sdkgen-package.json +2 -1
  71. package/src/helpers/naming.ts +5 -4
@@ -0,0 +1,439 @@
1
+ <?php
2
+ declare(strict_types=1);
3
+
4
+ // ProjectName SDK cost feature
5
+
6
+ require_once __DIR__ . '/BaseFeature.php';
7
+
8
+ // Cost tracking and spend budget. Uses BOTH seams, which is the point of
9
+ // the feature: money is spent per HTTP ATTEMPT (a retried call is charged
10
+ // again, because the upstream API charges it again), but it is owed by an
11
+ // OPERATION. So the transport wrap prices each attempt, and PreDone
12
+ // attributes the running total to `<entity>.<op>` and to the caller
13
+ // (`ctrl->actor`, the same actor the audit feature records).
14
+ //
15
+ // The price of an attempt comes from the first source that answers: a
16
+ // response header (`header` x `perUnit`), the rate table (`rates`, keyed
17
+ // `<entity>.<op>` / `<op>` / `*`), then the flat `unit`. A body figure
18
+ // (`path` x `perUnit`, e.g. 'usage.total_tokens') is read at PreDone
19
+ // instead, from the already-parsed result, and describes the whole call, so
20
+ // it REPLACES the per-attempt estimate rather than adding to it.
21
+ //
22
+ // `budget` caps total spend. With `onBudget: 'deny'` a further operation is
23
+ // refused at PrePoint (the error is placed in `$ctx->out['point']`), before
24
+ // an endpoint is resolved and before anything reaches the network.
25
+ //
26
+ // ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
27
+ // cache is charged for money that was never spent. The default (map) order
28
+ // puts cache innermost and cost outside it, so activate them in list form
29
+ // with cost first. Mirrors ts/src/feature/cost/CostFeature.ts.
30
+ class ProjectNameCostFeature extends ProjectNameBaseFeature
31
+ {
32
+ private mixed $client;
33
+ private ?array $options;
34
+ private ?\WeakMap $pending;
35
+ private int $seq;
36
+
37
+ public function __construct()
38
+ {
39
+ parent::__construct();
40
+ $this->version = '0.0.1';
41
+ $this->name = 'cost';
42
+ $this->active = true;
43
+ $this->client = null;
44
+ $this->options = null;
45
+ $this->pending = null;
46
+ $this->seq = 0;
47
+ }
48
+
49
+ public function init(ProjectNameContext $ctx, array $options): void
50
+ {
51
+ $this->client = $ctx->client;
52
+ $this->options = $options;
53
+ $this->active = ($options['active'] ?? null) === true;
54
+ $this->pending = new \WeakMap();
55
+ $this->seq = 0;
56
+
57
+ $limit = $this->_limit();
58
+
59
+ $client = $this->client;
60
+ if (!isset($client->_cost)) {
61
+ $client->_cost = [
62
+ 'currency' => (string)($this->options['currency'] ?? 'USD'),
63
+ 'total' => [
64
+ 'calls' => 0, 'attempts' => 0,
65
+ 'amount' => 0.0, 'reported' => 0.0, 'estimated' => 0.0,
66
+ ],
67
+ 'ops' => [],
68
+ 'actors' => [],
69
+ 'budget' => [
70
+ 'limit' => $limit, 'spent' => 0.0,
71
+ 'remaining' => $limit, 'exceeded' => false,
72
+ ],
73
+ 'last' => null,
74
+ ];
75
+ }
76
+
77
+ if (!$this->active) {
78
+ return;
79
+ }
80
+
81
+ $utility = $ctx->utility;
82
+ $inner = $utility->fetcher;
83
+
84
+ $utility->fetcher = function (ProjectNameContext $ctx2, string $url, array $fetchdef) use ($inner): array {
85
+ return $this->_charge($ctx2, $url, $fetchdef, $inner);
86
+ };
87
+ }
88
+
89
+ // The budget gate. Runs before endpoint resolution, so a refused call
90
+ // costs nothing at all.
91
+ public function PrePoint(ProjectNameContext $ctx): void
92
+ {
93
+ if (!$this->active) {
94
+ return;
95
+ }
96
+
97
+ // Mark the context as running through the pipeline, so _charge knows a
98
+ // PreDone is coming and does not commit the spend itself.
99
+ if ($this->pending !== null) {
100
+ $entry = isset($this->pending[$ctx]) ? $this->pending[$ctx] : $this->_newPending();
101
+ $entry['piped'] = true;
102
+ $this->pending[$ctx] = $entry;
103
+ }
104
+
105
+ $limit = $this->_limit();
106
+ if ($limit <= 0.0) {
107
+ return;
108
+ }
109
+
110
+ $client = $this->client;
111
+ if ($client->_cost['total']['amount'] < $limit) {
112
+ return;
113
+ }
114
+
115
+ $cost = $client->_cost;
116
+ $cost['budget']['exceeded'] = true;
117
+ $client->_cost = $cost;
118
+
119
+ if (($this->options['onBudget'] ?? 'warn') !== 'deny') {
120
+ return;
121
+ }
122
+
123
+ $err = $ctx->make_error('cost_budget',
124
+ 'Cost budget of ' . $this->_numstr($limit) . ' ' . $cost['currency'] .
125
+ ' is spent (' . $this->_numstr((float)$cost['total']['amount']) . ' ' .
126
+ $cost['currency'] . ' used)');
127
+
128
+ // Short-circuit endpoint resolution; the pipeline surfaces this error.
129
+ $ctx->out['point'] = $err;
130
+ }
131
+
132
+ private function _charge(ProjectNameContext $ctx, string $url, array $fetchdef, callable $inner): array
133
+ {
134
+ // A rejecting transport still costs an attempt. Without this, a run of
135
+ // connection-level failures under `retry` (which catches and tries
136
+ // again) would be charged nothing at all, and an onBudget 'deny'
137
+ // ceiling could never stop it.
138
+ $threw = null;
139
+ try {
140
+ [$res, $err] = $inner($ctx, $url, $fetchdef);
141
+ } catch (\Throwable $ex) {
142
+ $threw = $ex;
143
+ $res = null;
144
+ $err = $ex;
145
+ }
146
+
147
+ [$amount, $source] = $this->_price($ctx, $res);
148
+
149
+ $entry = ($this->pending !== null && isset($this->pending[$ctx]))
150
+ ? $this->pending[$ctx]
151
+ : $this->_newPending();
152
+
153
+ $entry['attempts']++;
154
+
155
+ // Accumulated here, committed once at PreDone. Adding each attempt to
156
+ // the running total and then subtracting it again when a body figure
157
+ // supersedes it loses precision to catastrophic cancellation.
158
+ //
159
+ // Reported and estimated are kept apart per ATTEMPT: a 503 priced from
160
+ // the rate table followed by a 200 carrying the cost header is part
161
+ // estimate, part reported, and collapsing both into the final
162
+ // attempt's category would corrupt the split.
163
+ $entry['amount'] += $amount;
164
+ $entry[('header' === $source || 'body' === $source) ? 'reported' : 'estimated'] += $amount;
165
+ $entry['source'] = $source;
166
+
167
+ if ($this->pending !== null) {
168
+ $this->pending[$ctx] = $entry;
169
+ }
170
+
171
+ $client = $this->client;
172
+ $cost = $client->_cost;
173
+ $cost['total']['attempts']++;
174
+ $client->_cost = $cost;
175
+
176
+ // direct() and graphql() reach the transport without dispatching any
177
+ // pipeline hooks, so there is no PrePoint to gate on and no PreDone to
178
+ // commit. Their spend is committed here, or it would never be counted.
179
+ // `piped` is set by PrePoint, so its absence is the signal.
180
+ if (!$entry['piped']) {
181
+ $this->_commit($ctx, $entry, '_', 'direct');
182
+ if ($this->pending !== null) {
183
+ unset($this->pending[$ctx]);
184
+ }
185
+ }
186
+
187
+ if ($threw !== null) {
188
+ throw $threw;
189
+ }
190
+
191
+ return [$res, $err];
192
+ }
193
+
194
+ private function _newPending(): array
195
+ {
196
+ return [
197
+ 'attempts' => 0, 'amount' => 0.0,
198
+ 'reported' => 0.0, 'estimated' => 0.0,
199
+ 'source' => 'none', 'piped' => false,
200
+ ];
201
+ }
202
+
203
+ // Attribute the operation's spend once the call is finished.
204
+ public function PreDone(ProjectNameContext $ctx): void
205
+ {
206
+ $this->_finish($ctx, true);
207
+ }
208
+
209
+ // A failed operation still spent the money. When the pipeline throws,
210
+ // PreDone never runs, so without this the attempts are counted and the
211
+ // spend is not, and a budget could never see the cost of a failed call.
212
+ // Whichever hook fires first consumes the pending entry, so it commits
213
+ // exactly once.
214
+ public function PreUnexpected(ProjectNameContext $ctx): void
215
+ {
216
+ $this->_finish($ctx, false);
217
+ }
218
+
219
+ private function _finish(ProjectNameContext $ctx, bool $done): void
220
+ {
221
+ if (!$this->active || $this->pending === null || !isset($this->pending[$ctx])) {
222
+ return;
223
+ }
224
+ $entry = $this->pending[$ctx];
225
+ unset($this->pending[$ctx]);
226
+
227
+ // A FAILED operation that made no attempt never reached the network:
228
+ // PrePoint creates the pending entry to mark the context as piped, and
229
+ // then the budget gate refuses the call (rbac, or an unresolvable
230
+ // endpoint, short-circuits just as early). Committing it would count a
231
+ // call that never happened and file a zero-amount record as `last`.
232
+ //
233
+ // A SUCCEEDED operation that made no attempt is the opposite case: it was
234
+ // served from the cache. That is a real call, and the fact that it cost
235
+ // nothing is the whole point of ordering cost inside the cache.
236
+ if (!$done && 0 === $entry['attempts']) {
237
+ return;
238
+ }
239
+
240
+ $entity = ($ctx->op !== null && $ctx->op->entity !== '') ? $ctx->op->entity : '_';
241
+ $opname = ($ctx->op !== null && $ctx->op->name !== '') ? $ctx->op->name : '_';
242
+
243
+ $this->_commit($ctx, $entry, $entity, $opname);
244
+ }
245
+
246
+ // Commit one operation's spend: totals, budget, per-op and per-actor
247
+ // attribution, and the record. Shared by _finish and the raw-request path
248
+ // in _charge, which has no PreDone to reach.
249
+ private function _commit(ProjectNameContext $ctx, array $entry, string $entity, string $opname): void
250
+ {
251
+ $client = $this->client;
252
+ $cost = $client->_cost;
253
+
254
+ $amount = (float)$entry['amount'];
255
+ $reported = (float)$entry['reported'];
256
+ $estimated = (float)$entry['estimated'];
257
+ $source = (string)$entry['source'];
258
+
259
+ // A body figure prices the whole call, so it replaces the per-attempt
260
+ // estimate rather than adding to it, and being server-stated the whole
261
+ // amount counts as reported.
262
+ $body = $this->_body($ctx);
263
+ if ($body !== null) {
264
+ $amount = $body;
265
+ $reported = $body;
266
+ $estimated = 0.0;
267
+ $source = 'body';
268
+ }
269
+
270
+ $cost = $this->_spend($cost, $amount, $reported, $estimated);
271
+
272
+ // ctrl->actor is an optional extension property on the control
273
+ // object, same as the audit feature reads.
274
+ $ctrl_actor = $ctx->ctrl->actor ?? null;
275
+ $actor = (is_string($ctrl_actor) && $ctrl_actor !== '') ? $ctrl_actor
276
+ : (string)($this->options['actor'] ?? 'anonymous');
277
+
278
+ $cost['total']['calls']++;
279
+ $cost['ops'] = $this->_bump($cost['ops'], $entity . '.' . $opname, $amount);
280
+ $cost['actors'] = $this->_bump($cost['actors'], $actor, $amount);
281
+
282
+ $this->seq++;
283
+ $record = [
284
+ 'seq' => $this->seq,
285
+ 'entity' => $entity,
286
+ 'op' => $opname,
287
+ 'actor' => $actor,
288
+ 'amount' => $amount,
289
+ 'currency' => $cost['currency'],
290
+ 'source' => $source,
291
+ 'attempts' => $entry['attempts'],
292
+ ];
293
+ $cost['last'] = $record;
294
+ $client->_cost = $cost;
295
+
296
+ $sink = $this->options['sink'] ?? null;
297
+ if (is_callable($sink)) {
298
+ try {
299
+ $sink($record);
300
+ } catch (\Throwable $e) {
301
+ // A failing sink must never take down the call.
302
+ }
303
+ }
304
+ }
305
+
306
+ // Price one attempt: a reported header figure, else the rate table, else
307
+ // the flat unit.
308
+ private function _price(ProjectNameContext $ctx, mixed $res): array
309
+ {
310
+ $header = $this->options['header'] ?? '';
311
+ if (is_string($header) && '' !== $header) {
312
+ $val = $this->_header($res, $header);
313
+ if ($val !== null) {
314
+ return [$val * $this->_perUnit(), 'header'];
315
+ }
316
+ }
317
+
318
+ $rate = $this->_rate($ctx);
319
+ if ($rate !== null) {
320
+ return [$rate, 'table'];
321
+ }
322
+
323
+ $unit = $this->options['unit'] ?? 0;
324
+ if (is_numeric($unit) && 0.0 !== (float)$unit) {
325
+ return [(float)$unit, 'unit'];
326
+ }
327
+
328
+ return [0.0, 'none'];
329
+ }
330
+
331
+ // The rate table uses the same lookup grammar as rbac's rules:
332
+ // `<entity>.<op>`, then `<op>`, then `*`.
333
+ private function _rate(ProjectNameContext $ctx): ?float
334
+ {
335
+ $rates = $this->options['rates'] ?? null;
336
+ if (!is_array($rates)) {
337
+ return null;
338
+ }
339
+
340
+ $entity = '';
341
+ if ($ctx->entity !== null && isset($ctx->entity->name)) {
342
+ $entity = (string)$ctx->entity->name;
343
+ } elseif ($ctx->op !== null) {
344
+ $entity = $ctx->op->entity;
345
+ }
346
+ $opname = $ctx->op !== null ? $ctx->op->name : '';
347
+
348
+ foreach ([$entity . '.' . $opname, $opname, '*'] as $key) {
349
+ $val = $rates[$key] ?? null;
350
+ if (is_numeric($val)) {
351
+ return (float)$val;
352
+ }
353
+ }
354
+ return null;
355
+ }
356
+
357
+ // A usage figure from the parsed result body, priced by perUnit. Read
358
+ // here, not at the transport seam, because the body is one-shot.
359
+ private function _body(ProjectNameContext $ctx): ?float
360
+ {
361
+ $path = $this->options['path'] ?? '';
362
+ if (!is_string($path) || '' === $path) {
363
+ return null;
364
+ }
365
+ if ($ctx->result === null || $ctx->result->body === null) {
366
+ return null;
367
+ }
368
+ $val = \Voxgig\Struct\Struct::getpath($ctx->result->body, $path);
369
+ if (!is_numeric($val)) {
370
+ return null;
371
+ }
372
+ return (float)$val * $this->_perUnit();
373
+ }
374
+
375
+ private function _spend(array $cost, float $amount, float $reported, float $estimated): array
376
+ {
377
+ $cost['total']['amount'] += $amount;
378
+ $cost['total']['reported'] += $reported;
379
+ $cost['total']['estimated'] += $estimated;
380
+
381
+ $limit = (float)$cost['budget']['limit'];
382
+ $cost['budget']['spent'] = $cost['total']['amount'];
383
+ if ($limit > 0.0) {
384
+ $cost['budget']['remaining'] = max(0.0, $limit - (float)$cost['total']['amount']);
385
+ if ((float)$cost['total']['amount'] >= $limit) {
386
+ $cost['budget']['exceeded'] = true;
387
+ }
388
+ } else {
389
+ $cost['budget']['remaining'] = 0.0;
390
+ }
391
+ return $cost;
392
+ }
393
+
394
+ private function _bump(array $bucket, string $key, float $amount): array
395
+ {
396
+ if (!isset($bucket[$key])) {
397
+ $bucket[$key] = ['calls' => 0, 'amount' => 0.0];
398
+ }
399
+ $bucket[$key]['calls']++;
400
+ $bucket[$key]['amount'] += $amount;
401
+ return $bucket;
402
+ }
403
+
404
+ private function _header(mixed $res, string $name): ?float
405
+ {
406
+ if (!is_array($res)) {
407
+ return null;
408
+ }
409
+ $headers = $res['headers'] ?? null;
410
+ if (!is_array($headers)) {
411
+ return null;
412
+ }
413
+ $lower = strtolower($name);
414
+ foreach ($headers as $key => $val) {
415
+ if (strtolower((string)$key) === $lower && is_numeric($val)) {
416
+ return (float)$val;
417
+ }
418
+ }
419
+ return null;
420
+ }
421
+
422
+ private function _perUnit(): float
423
+ {
424
+ $per = $this->options['perUnit'] ?? 0;
425
+ return is_numeric($per) ? (float)$per : 0.0;
426
+ }
427
+
428
+ private function _limit(): float
429
+ {
430
+ $budget = $this->options['budget'] ?? 0;
431
+ return is_numeric($budget) ? (float)$budget : 0.0;
432
+ }
433
+
434
+ // Render a money amount without an exponent or trailing zeros.
435
+ private function _numstr(float $n): string
436
+ {
437
+ return rtrim(rtrim(number_format($n, 10, '.', ''), '0'), '.') ?: '0';
438
+ }
439
+ }
@@ -179,7 +179,37 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
179
179
  return $respond(200, $out);
180
180
 
181
181
  } elseif ($op->name === 'list') {
182
- $found = $find_all($entmap, $fctx->reqmatch, $alias);
182
+ // FILTER THE MATCH THE WAY THE TS MOCK DOES.
183
+ //
184
+ // This branch used the whole of reqmatch, while load goes
185
+ // through resolve_match and the TS mock runs every op through
186
+ // buildArgs — which keeps `id` and the point's REQUIRED params
187
+ // and drops the rest. buildArgs is right here in this file and
188
+ // its docblock says it mirrors the TS one; list simply did not
189
+ // call it.
190
+ //
191
+ // So a match key that is neither `id` nor a required route
192
+ // param matched NOTHING in php and was IGNORED in ts. trello's
193
+ // board_star lists on `board_id`, which is not a field of that
194
+ // entity (id, pos, member_id, id_board) and not a route param:
195
+ // php returned an empty list and BoardStarEntityTest failed on
196
+ // an assertion that ts, js and every other port passed.
197
+ //
198
+ // buildArgs returns a struct query; find_all takes a flat map,
199
+ // so take the primary key of each $OR clause. find_all already
200
+ // does its own alias fallback, which is what the second $OR
201
+ // element carries.
202
+ $listargs = $this->buildArgs($fctx, $op, $fctx->reqmatch);
203
+ $listmatch = [];
204
+ foreach (($listargs['$AND'] ?? []) as $clause) {
205
+ $ors = $clause['$OR'] ?? [];
206
+ if (is_array($ors) && count($ors) > 0 && is_array($ors[0])) {
207
+ foreach ($ors[0] as $lk => $lv) {
208
+ $listmatch[$lk] = $lv;
209
+ }
210
+ }
211
+ }
212
+ $found = $find_all($entmap, $listmatch, $alias);
183
213
  $cleaned = [];
184
214
  foreach ($found as $e) {
185
215
  if (is_array($e)) unset($e['$KEY']);
File without changes