routesync 1.0.34 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -200,6 +200,137 @@ const result = await produkGetIdAction({ params: { id: '42' } })
200
200
 
201
201
  ---
202
202
 
203
+ ## Response Type Inference
204
+
205
+ RouteSync automatically infers the TypeScript response type for each endpoint. The scanner works through **7 stages in order**, stopping at the first successful match.
206
+
207
+ ### How inference works
208
+
209
+ ```
210
+ Controller method
211
+
212
+
213
+ Stage 1: PHP 8 #[RouteSyncResponse] attribute on method ← most explicit
214
+
215
+
216
+ Stage 2: return new UserResource($user) in method body
217
+ │ ├─ Stage 2a: #[RouteSyncResponse] on Resource class
218
+ │ ├─ Stage 2b: @mixin \App\Models\User docblock
219
+ │ ├─ Stage 2c: __construct(User $user) type hint ← NEW
220
+ │ ├─ Stage 2d: @var User $resource docblock ← NEW
221
+ │ ├─ Stage 2e: Strip "Resource" suffix → App\Models\* ← NEW
222
+ │ └─ Stage 2f: toArray() keys vs DB column matching ← NEW
223
+
224
+
225
+ Stage 3: response()->json([...]) inline array
226
+ keys matched against DB columns (min score 2) ← NEW
227
+
228
+
229
+ response: unknown ← annotate manually if all stages fail
230
+ ```
231
+
232
+ ### Zero-config inference (no annotation needed)
233
+
234
+ For the common convention `UserResource` → `User`, RouteSync infers automatically:
235
+
236
+ ```php
237
+ // ✅ Auto-detected — no annotation needed
238
+ public function show(User $user): JsonResponse
239
+ {
240
+ return new UserResource($user);
241
+ }
242
+
243
+ // ✅ Auto-detected — UserResource::collection → User[]
244
+ public function index(): JsonResponse
245
+ {
246
+ return UserResource::collection(User::all());
247
+ }
248
+ ```
249
+
250
+ ### Explicit annotation with PHP 8 Attribute
251
+
252
+ Use `#[RouteSyncResponse]` when auto-inference fails — for example, when the Resource name doesn't match the model, or the response is a DTO/custom shape.
253
+
254
+ **Step 1 — Create the attribute class** (`app/Attributes/RouteSyncResponse.php`):
255
+
256
+ ```php
257
+ <?php
258
+
259
+ namespace App\Attributes;
260
+
261
+ use Attribute;
262
+
263
+ #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
264
+ class RouteSyncResponse
265
+ {
266
+ public function __construct(
267
+ public readonly string $model,
268
+ public readonly bool $collection = false,
269
+ ) {}
270
+ }
271
+ ```
272
+
273
+ **Step 2 — Annotate your controller method:**
274
+
275
+ ```php
276
+ use App\Attributes\RouteSyncResponse;
277
+ use App\Models\User;
278
+
279
+ class AuthController extends Controller
280
+ {
281
+ // Single object response
282
+ #[RouteSyncResponse(model: User::class)]
283
+ public function register(RegisterRequest $request): JsonResponse
284
+ {
285
+ $user = User::create($request->validated());
286
+ $token = $user->createToken('auth')->plainTextToken;
287
+
288
+ return response()->json(['token' => $token, 'user' => new UserResource($user)]);
289
+ }
290
+
291
+ // Collection response
292
+ #[RouteSyncResponse(model: User::class, collection: true)]
293
+ public function index(): JsonResponse
294
+ {
295
+ return response()->json(User::all());
296
+ }
297
+ }
298
+ ```
299
+
300
+ **Or annotate the Resource class directly** (applies to all endpoints that return this Resource):
301
+
302
+ ```php
303
+ use App\Attributes\RouteSyncResponse;
304
+ use App\Models\User;
305
+
306
+ #[RouteSyncResponse(model: User::class)]
307
+ class UserResource extends JsonResource
308
+ {
309
+ public function toArray($request): array
310
+ {
311
+ return [
312
+ 'id' => $this->id,
313
+ 'name' => $this->name,
314
+ 'email' => $this->email,
315
+ ];
316
+ }
317
+ }
318
+ ```
319
+
320
+ > **Priority:** annotation on controller method > annotation on Resource class > auto-inference.
321
+
322
+ ### When to annotate manually
323
+
324
+ | Situation | Solution |
325
+ |---|---|
326
+ | Resource name doesn't match model (`PublicProfileResource` → `User`) | `#[RouteSyncResponse(model: User::class)]` |
327
+ | Response is a DTO, not an Eloquent model | Add model manually after generate |
328
+ | Controller returns `response()->json([...])` without a Resource | `#[RouteSyncResponse(model: User::class)]` |
329
+ | Multiple models in one response | Add model manually after generate |
330
+ | Response is still `unknown` after scan | Add `#[RouteSyncResponse]` attribute |
331
+
332
+ ---
333
+
203
334
  ## Data Transformation
204
335
 
205
336
  RouteSync handles all data mapping automatically:
package/dist/cli.js CHANGED
@@ -8361,25 +8361,189 @@ var import_path = __toESM(require("path"));
8361
8361
  var LaravelRouteParser = class {
8362
8362
  async parse(filePath, options = {}) {
8363
8363
  const projectRoot = import_path.default.resolve(import_path.default.dirname(filePath), "..");
8364
+ const extractModels = options.extractModels ? "true" : "false";
8364
8365
  const phpScript = `<?php
8365
8366
  require __DIR__.'/vendor/autoload.php';
8366
8367
  $app = require_once __DIR__.'/bootstrap/app.php';
8367
- $kernel = $app->make(Illuminate\\Contracts\\Console\\Kernel::class);
8368
+ $kernel = $app->make(IlluminateContractsConsoleKernel::class);
8368
8369
  $kernel->bootstrap();
8369
8370
 
8370
- $result = [
8371
- 'routes' => [],
8372
- 'models' => []
8373
- ];
8371
+ $result = ['routes' => [], 'models' => []];
8372
+
8373
+ // \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8374
+
8375
+ function rsync_get_source(ReflectionFunctionAbstract $ref): ?string {
8376
+ $file = $ref->getFileName();
8377
+ $start = $ref->getStartLine();
8378
+ $end = $ref->getEndLine();
8379
+ if (!$file || $start === false || $end === false) return null;
8380
+ return implode('', array_slice(file($file), $start - 1, $end - $start + 1));
8381
+ }
8382
+
8383
+ /**
8384
+ * Try to resolve responseMetadata from a Resource class.
8385
+ * Checks (in order):
8386
+ * 1. PHP 8 #[RouteSyncResponse] attribute on class
8387
+ * 2. @mixin docblock
8388
+ * 3. Constructor parameter type hint \u2190 NEW
8389
+ * 4. $this->resource @var docblock \u2190 NEW
8390
+ * 5. Strip Resource suffix \u2192 match AppModels*
8391
+ * 6. toArray() field vs DB column intersection
8392
+ */
8393
+ function rsync_infer_from_resource(string $resourceClass, bool $collection): ?array {
8394
+ if (!class_exists($resourceClass)) return null;
8395
+ $resRef = new ReflectionClass($resourceClass);
8396
+
8397
+ // 1. PHP 8 attribute
8398
+ foreach ($resRef->getAttributes() as $attr) {
8399
+ $short = class_basename($attr->getName());
8400
+ if (in_array($short, ['Response', 'RouteSyncResponse'])) {
8401
+ $args = $attr->getArguments();
8402
+ $type = $args[0] ?? $args['type'] ?? $args['model'] ?? $args['response'] ?? null;
8403
+ if ($type) return ['type' => class_basename($type), 'collection' => $collection];
8404
+ }
8405
+ }
8406
+
8407
+ // 2. @mixin docblock
8408
+ $doc = $resRef->getDocComment();
8409
+ if ($doc && preg_match('/@mixins+([\\\\w]+)/', $doc, $m)) {
8410
+ return ['type' => class_basename($m[1]), 'collection' => $collection];
8411
+ }
8412
+
8413
+ // 3. Constructor parameter type hint: __construct(User $user)
8414
+ if ($resRef->hasMethod('__construct')) {
8415
+ $ctor = $resRef->getMethod('__construct');
8416
+ foreach ($ctor->getParameters() as $param) {
8417
+ $ptype = $param->getType();
8418
+ if ($ptype && !$ptype->isBuiltin()) {
8419
+ $cn = $ptype->getName();
8420
+ if (is_subclass_of($cn, 'IlluminateDatabaseEloquentModel')) {
8421
+ return ['type' => class_basename($cn), 'collection' => $collection];
8422
+ }
8423
+ }
8424
+ }
8425
+ }
8426
+
8427
+ // 4. $this->resource @var docblock in class body or toArray()
8428
+ $classDoc = $resRef->getDocComment() ?: '';
8429
+ foreach (['toArray', 'toResponse'] as $mname) {
8430
+ if ($resRef->hasMethod($mname)) {
8431
+ $src = rsync_get_source($resRef->getMethod($mname)) ?? '';
8432
+ $classDoc .= $src;
8433
+ }
8434
+ }
8435
+ if (preg_match('/@vars+([\\\\w]+)s+$(?:resource|model)/', $classDoc, $m)) {
8436
+ $cn = ltrim($m[1], '\\');
8437
+ $fqcn = str_contains($cn, '\\') ? $cn : 'App\\Models\\' . $cn;
8438
+ if (class_exists($fqcn)) {
8439
+ return ['type' => class_basename($fqcn), 'collection' => $collection];
8440
+ }
8441
+ }
8442
+
8443
+ // 5. Strip Resource suffix \u2192 AppModels<Name>
8444
+ $inferredName = preg_replace('/Resource$/', '', class_basename($resourceClass));
8445
+ if ($inferredName) {
8446
+ $mc = 'App\\Models\\' . $inferredName;
8447
+ if (class_exists($mc)) {
8448
+ return ['type' => $inferredName, 'collection' => $collection];
8449
+ }
8450
+ }
8451
+
8452
+ // 6. toArray() field vs DB column intersection
8453
+ if ($resRef->hasMethod('toArray')) {
8454
+ $src = rsync_get_source($resRef->getMethod('toArray')) ?? '';
8455
+ preg_match_all('/['"]([a-zA-Z0-9_]+)['"]s*=>/', $src, $km);
8456
+ $resFields = array_unique($km[1] ?? []);
8457
+ if (!empty($resFields)) {
8458
+ $bestModel = null; $bestScore = 0;
8459
+ $modelsPath = app_path('Models');
8460
+ if (is_dir($modelsPath)) {
8461
+ foreach (IlluminateSupportFacadesFile::allFiles($modelsPath) as $mf) {
8462
+ $mn = preg_replace('/.php$/', '', $mf->getFilename());
8463
+ $mc = 'App\\Models\\' . $mn;
8464
+ if (!class_exists($mc)) continue;
8465
+ try {
8466
+ $mi = new $mc();
8467
+ $cols = array_column(IlluminateSupportFacadesSchema::getColumns($mi->getTable()), 'name');
8468
+ $score = count(array_intersect($resFields, $cols));
8469
+ if ($score > $bestScore) { $bestScore = $score; $bestModel = $mn; }
8470
+ } catch (Exception $e) {}
8471
+ }
8472
+ }
8473
+ if ($bestModel && $bestScore > 0) {
8474
+ return ['type' => $bestModel, 'collection' => $collection];
8475
+ }
8476
+ }
8477
+ }
8478
+
8479
+ return null;
8480
+ }
8481
+
8482
+ /**
8483
+ * Try to resolve responseMetadata directly from controller method source.
8484
+ * Handles cases where no Resource class is used at all.
8485
+ */
8486
+ function rsync_infer_from_source(?string $source): ?array {
8487
+ if (!$source) return null;
8488
+
8489
+ // return new SomeResource($x)
8490
+ if (preg_match('/returns+news+([a-zA-Z0-9_]+Resource)s*(/', $source, $m)) {
8491
+ $rc = 'App\\Http\\Resources\\' . $m[1];
8492
+ $result = rsync_infer_from_resource($rc, false);
8493
+ if ($result) return $result;
8494
+ }
8495
+
8496
+ // SomeResource::collection(...)
8497
+ if (preg_match('/([a-zA-Z0-9_]+Resource)::collections*(/', $source, $m)) {
8498
+ $rc = 'App\\Http\\Resources\\' . $m[1];
8499
+ $result = rsync_infer_from_resource($rc, true);
8500
+ if ($result) return $result;
8501
+ }
8502
+
8503
+ // ->paginate() or ->simplePaginate() with Resource
8504
+ if (preg_match('/([a-zA-Z0-9_]+Resource)::collection.*paginate/s', $source, $m)) {
8505
+ $rc = 'App\\Http\\Resources\\' . $m[1];
8506
+ $result = rsync_infer_from_resource($rc, true);
8507
+ if ($result) { $result['paginated'] = true; return $result; }
8508
+ }
8509
+
8510
+ // response()->json(['token' => ..., 'user' => ...]) \u2014 inline array
8511
+ // Extract top-level keys and try to match a model
8512
+ if (preg_match('/response()s*->s*jsons*(s*[([^]]{0,800})]/', $source, $jsonMatch)) {
8513
+ preg_match_all('/['"]([a-zA-Z0-9_]+)['"]s*=>/', $jsonMatch[1], $km);
8514
+ $keys = array_unique($km[1] ?? []);
8515
+ if (!empty($keys)) {
8516
+ $bestModel = null; $bestScore = 0;
8517
+ foreach (IlluminateSupportFacadesFile::allFiles(app_path('Models')) as $mf) {
8518
+ $mn = preg_replace('/.php$/', '', $mf->getFilename());
8519
+ $mc = 'App\\Models\\' . $mn;
8520
+ if (!class_exists($mc)) continue;
8521
+ try {
8522
+ $mi = new $mc();
8523
+ $cols = array_column(IlluminateSupportFacadesSchema::getColumns($mi->getTable()), 'name');
8524
+ $camelCols = array_map(fn($c) => lcfirst(str_replace('_', '', ucwords($c, '_'))), $cols);
8525
+ $score = count(array_intersect($keys, array_merge($cols, $camelCols)));
8526
+ if ($score > $bestScore) { $bestScore = $score; $bestModel = $mn; }
8527
+ } catch (Exception $e) {}
8528
+ }
8529
+ if ($bestModel && $bestScore >= 2) {
8530
+ return ['type' => $bestModel, 'collection' => false];
8531
+ }
8532
+ }
8533
+ }
8534
+
8535
+ return null;
8536
+ }
8537
+
8538
+ // \u2500\u2500\u2500 Main Route Loop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8374
8539
 
8375
- // Extract Routes
8376
8540
  $routes = app('router')->getRoutes();
8377
8541
  foreach ($routes as $route) {
8378
8542
  if (!str_starts_with($route->uri(), 'api/')) continue;
8379
-
8543
+
8380
8544
  $methods = array_diff($route->methods(), ['HEAD']);
8381
8545
  $middlewares = $route->gatherMiddleware();
8382
-
8546
+
8383
8547
  $auth = false;
8384
8548
  foreach ($middlewares as $mw) {
8385
8549
  if (is_string($mw) && (str_contains($mw, 'auth') || str_contains($mw, 'sanctum'))) {
@@ -8388,17 +8552,21 @@ foreach ($routes as $route) {
8388
8552
  }
8389
8553
 
8390
8554
  $schema = [];
8555
+ $responseMetadata = null;
8391
8556
  $action = $route->getAction();
8557
+
8392
8558
  if (isset($action['uses']) && is_string($action['uses']) && str_contains($action['uses'], '@')) {
8393
8559
  list($controller, $method) = explode('@', $action['uses']);
8394
8560
  if (class_exists($controller)) {
8395
8561
  try {
8396
8562
  $reflector = new ReflectionMethod($controller, $method);
8563
+
8564
+ // \u2500\u2500 Request schema from FormRequest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8397
8565
  foreach ($reflector->getParameters() as $param) {
8398
8566
  $type = $param->getType();
8399
8567
  if ($type && !$type->isBuiltin()) {
8400
8568
  $className = $type->getName();
8401
- if (is_subclass_of($className, 'Illuminate\\Foundation\\Http\\FormRequest')) {
8569
+ if (is_subclass_of($className, 'IlluminateFoundationHttpFormRequest')) {
8402
8570
  $request = new $className();
8403
8571
  if (method_exists($request, 'rules')) {
8404
8572
  $schema = $request->rules();
@@ -8406,174 +8574,97 @@ foreach ($routes as $route) {
8406
8574
  }
8407
8575
  }
8408
8576
  }
8409
-
8410
- // Parse PHP 8 Attributes for Response Metadata
8411
- $responseMetadata = null;
8412
- $attributes = $reflector->getAttributes();
8413
- foreach ($attributes as $attr) {
8414
- $attrName = $attr->getName();
8415
- $shortName = class_basename($attrName);
8416
-
8417
- if (in_array($shortName, ['Response', 'RouteSyncResponse'])) {
8577
+
8578
+ // \u2500\u2500 Stage 1: PHP 8 Attribute on controller method \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8579
+ foreach ($reflector->getAttributes() as $attr) {
8580
+ $short = class_basename($attr->getName());
8581
+ if (in_array($short, ['Response', 'RouteSyncResponse'])) {
8418
8582
  $args = $attr->getArguments();
8419
-
8420
- $type = null;
8421
- if (isset($args[0])) {
8422
- $type = $args[0];
8423
- } elseif (isset($args['type'])) {
8424
- $type = $args['type'];
8425
- } elseif (isset($args['model'])) {
8426
- $type = $args['model'];
8427
- } elseif (isset($args['response'])) {
8428
- $type = $args['response'];
8429
- }
8430
-
8431
- $collection = false;
8432
- if (isset($args[1])) {
8433
- $collection = (bool) $args[1];
8434
- } elseif (isset($args['collection'])) {
8435
- $collection = (bool) $args['collection'];
8436
- }
8437
-
8583
+ $type = $args[0] ?? $args['type'] ?? $args['model'] ?? $args['response'] ?? null;
8438
8584
  if ($type) {
8439
- $responseMetadata = [
8440
- 'type' => class_basename($type),
8441
- 'collection' => $collection
8442
- ];
8585
+ $collection = (bool)($args[1] ?? $args['collection'] ?? false);
8586
+ $responseMetadata = ['type' => class_basename($type), 'collection' => $collection];
8443
8587
  break;
8444
8588
  }
8445
8589
  }
8446
8590
  }
8447
-
8448
-
8449
- $fileName = $reflector->getFileName();
8450
- $startLine = $reflector->getStartLine();
8451
- $endLine = $reflector->getEndLine();
8452
- $methodSource = null;
8453
-
8454
- if ($fileName && $startLine !== false && $endLine !== false) {
8455
- $lines = file($fileName);
8456
- $methodSource = implode("", array_slice($lines, $startLine - 1, $endLine - $startLine + 1));
8457
- }
8458
8591
 
8459
- // Resource Discovery
8460
- if (!$responseMetadata && $methodSource) {
8461
- $resourceName = null;
8462
- $collection = false;
8463
-
8464
- if (preg_match('/return\\s+new\\s+([a-zA-Z0-9_]+Resource)/', $methodSource, $matches)) {
8465
- $resourceName = $matches[1];
8466
- } elseif (preg_match('/return\\s+([a-zA-Z0-9_]+Resource)::collection/', $methodSource, $matches)) {
8467
- $resourceName = $matches[1];
8468
- $collection = true;
8469
- }
8470
-
8471
- if ($resourceName) {
8472
- $resourceClass = 'App\\\\Http\\\\Resources\\\\' . $resourceName;
8473
- if (class_exists($resourceClass)) {
8474
- $resReflector = new ReflectionClass($resourceClass);
8475
- $resAttrs = $resReflector->getAttributes();
8476
- foreach ($resAttrs as $attr) {
8477
- $shortName = class_basename($attr->getName());
8478
- if (in_array($shortName, ['Response', 'RouteSyncResponse'])) {
8479
- $args = $attr->getArguments();
8480
- $type = $args[0] ?? $args['type'] ?? $args['model'] ?? $args['response'] ?? null;
8481
- if ($type) {
8482
- $responseMetadata = [
8483
- 'type' => class_basename($type),
8484
- 'collection' => $collection
8485
- ];
8486
- }
8487
- }
8488
- }
8489
-
8490
- if (!$responseMetadata) {
8491
- $docComment = $resReflector->getDocComment();
8492
- if ($docComment && preg_match('/@mixin\\s+([\\\\\\\\a-zA-Z0-9_]+)/', $docComment, $mixinMatches)) {
8493
- $responseMetadata = [
8494
- 'type' => class_basename($mixinMatches[1]),
8495
- 'collection' => $collection
8496
- ];
8497
- }
8498
- }
8499
- }
8500
- }
8592
+ $methodSource = rsync_get_source($reflector);
8593
+
8594
+ // \u2500\u2500 Stage 2: Source-based inference \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8595
+ if (!$responseMetadata) {
8596
+ $responseMetadata = rsync_infer_from_source($methodSource);
8501
8597
  }
8502
-
8503
- // Fallback: Try to parse $request->validate([...]) from source code
8598
+
8599
+ // \u2500\u2500 Stage 3: Fallback $request->validate([...]) for schema \u2500\u2500
8504
8600
  if (empty($schema) && $methodSource) {
8505
- // Look for $request->validate([ ... ])
8506
- if (preg_match('/\\\\$request->validate\\\\s*\\\\(\\\\s*\\\\[(.*?)\\\\]\\\\s*\\\\)/s', $methodSource, $matches)) {
8507
- $rulesString = $matches[1];
8508
- // Match 'field' => 'rules'
8509
- preg_match_all('~[\\'"]([a-zA-Z0-9_.*]+)[\\'"]\\\\s*=>\\\\s*[\\'"](.*?)[\\'"]~', $rulesString, $ruleMatches);
8510
- if (!empty($ruleMatches[1])) {
8511
- foreach ($ruleMatches[1] as $index => $field) {
8512
- $schema[$field] = $ruleMatches[2][$index];
8513
- }
8601
+ if (preg_match('/$request->validates*(s*[(.*?)]s*)/s', $methodSource, $vm)) {
8602
+ preg_match_all('/['"]([a-zA-Z0-9_.*]+)['"]s*=>s*['"]([^'"]*)['"]/', $vm[1], $rm);
8603
+ foreach ($rm[1] as $i => $field) {
8604
+ $schema[$field] = $rm[2][$i];
8514
8605
  }
8515
8606
  }
8516
8607
  }
8517
- } catch (\\Exception $e) {}
8608
+
8609
+ } catch (Exception $e) {}
8518
8610
  }
8519
8611
  }
8520
8612
 
8521
8613
  foreach ($methods as $method) {
8522
- $nameParts = explode('/', preg_replace('/^api\\//', '', $route->uri()));
8523
- $resource = preg_replace('/\\{.*\\}/', '', $nameParts[0]);
8614
+ $nameParts = explode('/', preg_replace('/^api//', '', $route->uri()));
8615
+ $resource = preg_replace('/{.*}/', '', $nameParts[0]);
8524
8616
  if (empty($resource)) $resource = 'api';
8525
-
8526
- $name = $resource . '.' . strtolower($method);
8527
-
8617
+
8528
8618
  $result['routes'][] = [
8529
- 'name' => $route->getName() ?: $name,
8530
- 'method' => $method,
8531
- 'path' => '/' . preg_replace('/^api\\//', '', $route->uri()),
8532
- 'auth' => $auth,
8619
+ 'name' => $route->getName() ?: ($resource . '.' . strtolower($method)),
8620
+ 'method' => $method,
8621
+ 'path' => '/' . preg_replace('/^api//', '', $route->uri()),
8622
+ 'auth' => $auth,
8533
8623
  'middleware' => $middlewares,
8534
- 'schema' => empty($schema) ? null : ['rules' => $schema],
8535
- 'response' => $responseMetadata
8624
+ 'schema' => empty($schema) ? null : ['rules' => $schema],
8625
+ 'response' => $responseMetadata,
8536
8626
  ];
8537
8627
  }
8538
8628
  }
8539
8629
 
8540
- // Extract Models if requested
8541
- $extractModels = ${options.extractModels ? "true" : "false"};
8630
+ // \u2500\u2500\u2500 Extract Models \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8631
+
8632
+ $extractModels = ${extractModels};
8542
8633
  if ($extractModels) {
8543
8634
  $modelsPath = app_path('Models');
8544
8635
  if (is_dir($modelsPath)) {
8545
- $files = \\Illuminate\\Support\\Facades\\File::allFiles($modelsPath);
8636
+ $files = IlluminateSupportFacadesFile::allFiles($modelsPath);
8546
8637
  foreach ($files as $file) {
8547
- $class = 'App\\\\Models\\\\' . str_replace('/', '\\\\', $file->getRelativePathname());
8548
- $class = preg_replace('/\\.php$/', '', $class);
8549
-
8550
- if (class_exists($class) && is_subclass_of($class, 'Illuminate\\\\Database\\\\Eloquent\\\\Model')) {
8638
+ $class = 'App\\Models\\' . str_replace('/', '\\', $file->getRelativePathname());
8639
+ $class = preg_replace('/.php$/', '', $class);
8640
+
8641
+ if (class_exists($class) && is_subclass_of($class, 'Illuminate\\Database\\Eloquent\\Model')) {
8551
8642
  try {
8552
8643
  $reflection = new ReflectionClass($class);
8553
8644
  if ($reflection->isAbstract()) continue;
8554
-
8555
- $model = new $class();
8556
- $table = $model->getTable();
8557
- $columns = \\Illuminate\\Support\\Facades\\Schema::getColumns($table);
8558
-
8645
+
8646
+ $model = new $class();
8647
+ $table = $model->getTable();
8648
+ $columns = IlluminateSupportFacadesSchema::getColumns($table);
8649
+
8559
8650
  $parsedColumns = [];
8560
8651
  foreach ($columns as $col) {
8561
8652
  $parsedColumns[] = [
8562
- 'name' => $col['name'],
8563
- 'type' => $col['type'], // Use 'type' which contains the raw type like enum('a','b') instead of 'type_name'
8564
- 'nullable' => $col['nullable']
8653
+ 'name' => $col['name'],
8654
+ 'type' => $col['type'],
8655
+ 'nullable' => $col['nullable'],
8565
8656
  ];
8566
8657
  }
8567
-
8658
+
8568
8659
  $result['models'][] = [
8569
- 'name' => class_basename($class),
8570
- 'table' => $table,
8660
+ 'name' => class_basename($class),
8661
+ 'table' => $table,
8571
8662
  'columns' => $parsedColumns,
8572
- 'hidden' => $model->getHidden(),
8663
+ 'hidden' => $model->getHidden(),
8573
8664
  'appends' => $model->getAppends(),
8574
- 'casts' => $model->getCasts()
8665
+ 'casts' => $model->getCasts(),
8575
8666
  ];
8576
- } catch (\\Exception $e) {}
8667
+ } catch (Exception $e) {}
8577
8668
  }
8578
8669
  }
8579
8670
  }
@@ -8726,26 +8817,10 @@ var SDKGenerator = class {
8726
8817
  const lines = [];
8727
8818
  let usesZod = false;
8728
8819
  let usesTypes = false;
8729
- const zodRoutes = /* @__PURE__ */ new Set();
8730
- if (options.zod) {
8731
- for (const [group, routes] of Object.entries(grouped)) {
8732
- for (const route of routes) {
8733
- if (route.schema?.rules) {
8734
- usesZod = true;
8735
- zodRoutes.add(`${group}.${route.actionName}`);
8736
- }
8737
- if (route.response) {
8738
- usesTypes = true;
8739
- }
8740
- }
8741
- }
8742
- } else {
8743
- for (const [group, routes] of Object.entries(grouped)) {
8744
- for (const route of routes) {
8745
- if (route.response) {
8746
- usesTypes = true;
8747
- }
8748
- }
8820
+ for (const routes of Object.values(grouped)) {
8821
+ for (const route of routes) {
8822
+ if (options.zod && route.schema?.rules) usesZod = true;
8823
+ if (route.response) usesTypes = true;
8749
8824
  }
8750
8825
  }
8751
8826
  lines.push(`// Auto-generated by routesync. Do not edit manually.`);
@@ -8761,15 +8836,15 @@ var SDKGenerator = class {
8761
8836
  lines.push(`import * as Types from './types'`);
8762
8837
  }
8763
8838
  lines.push(``);
8764
- for (const [group, routes] of Object.entries(grouped)) {
8839
+ for (const [groupName, routes] of Object.entries(grouped)) {
8765
8840
  for (const route of routes) {
8766
- const TitleCaseGroup = group.charAt(0).toUpperCase() + group.slice(1);
8841
+ const TitleCaseGroup = groupName.charAt(0).toUpperCase() + groupName.slice(1);
8767
8842
  const TitleCaseAction = route.actionName.charAt(0).toUpperCase() + route.actionName.slice(1);
8768
8843
  const ContractName = `${TitleCaseGroup}${TitleCaseAction}Contract`;
8844
+ const SchemaName = `${TitleCaseGroup}${TitleCaseAction}Schema`;
8769
8845
  const pathParams = Array.from(route.runtimePath.matchAll(/:([a-zA-Z0-9_]+)/g)).map((m) => m[1]);
8770
8846
  const paramsType = pathParams.length > 0 ? `{ ${pathParams.map((p) => `${p}: string`).join(", ")} }` : `unknown`;
8771
- const methodActionName = TitleCaseGroup + TitleCaseAction;
8772
- const bodyType = route.schema?.rules && usesZod ? `z.infer<typeof Schemas.${methodActionName}Schema>` : `unknown`;
8847
+ const bodyType = route.schema?.rules && usesZod ? `z.infer<typeof Schemas.${SchemaName}>` : `unknown`;
8773
8848
  const responseType = route.response ? `Types.${route.response.type}${route.response.collection ? "[]" : ""}` : `unknown`;
8774
8849
  lines.push(`export type ${ContractName} = {`);
8775
8850
  lines.push(` request: {`);
@@ -8783,23 +8858,23 @@ var SDKGenerator = class {
8783
8858
  }
8784
8859
  lines.push(``);
8785
8860
  lines.push(`export const api = defineApi({`);
8786
- for (const [group, routes] of Object.entries(grouped)) {
8787
- lines.push(` ${group}: {`);
8861
+ for (const [groupName, routes] of Object.entries(grouped)) {
8862
+ lines.push(` ${groupName}: {`);
8788
8863
  for (const route of routes) {
8789
- const TitleCaseGroup = group.charAt(0).toUpperCase() + group.slice(1);
8864
+ const TitleCaseGroup = groupName.charAt(0).toUpperCase() + groupName.slice(1);
8790
8865
  const TitleCaseAction = route.actionName.charAt(0).toUpperCase() + route.actionName.slice(1);
8791
8866
  const ContractName = `${TitleCaseGroup}${TitleCaseAction}Contract`;
8867
+ const SchemaName = `${TitleCaseGroup}${TitleCaseAction}Schema`;
8792
8868
  lines.push(` ${route.actionName}: endpoint<${ContractName}['response'], ${ContractName}['request']['params'], ${ContractName}['request']['body']>({`);
8793
8869
  lines.push(` method: '${route.method}',`);
8794
8870
  lines.push(` path: '${route.runtimePath}',`);
8795
8871
  if (route.auth) lines.push(` auth: true,`);
8796
8872
  if (options.zod && route.schema?.rules) {
8797
- lines.push(` schema: {`);
8798
- lines.push(` body: Schemas.${ContractName.replace("Contract", "Schema")}`);
8799
- lines.push(` },`);
8873
+ lines.push(` schema: { body: Schemas.${SchemaName} },`);
8800
8874
  }
8801
- if (options.zod && route.response && options.models) {
8802
- lines.push(` responseSchema: Schemas.${route.response.type}Schema${route.response.collection ? ".array()" : ""},`);
8875
+ if (options.zod && options.models && route.response) {
8876
+ const responseSchema = `Schemas.${route.response.type}Schema${route.response.collection ? ".array()" : ""}`;
8877
+ lines.push(` responseSchema: ${responseSchema},`);
8803
8878
  }
8804
8879
  lines.push(` }),`);
8805
8880
  }
@@ -8857,7 +8932,6 @@ var import_fs_extra4 = __toESM(require_lib());
8857
8932
  var TypeGenerator = class {
8858
8933
  static async generate(manifest, outputDir) {
8859
8934
  const lines = [];
8860
- const hasSchemas = manifest.routes.some((r) => r.schema && Object.keys(r.schema).length > 0);
8861
8935
  lines.push(`// Auto-generated by routesync. Do not edit manually.`);
8862
8936
  lines.push(``);
8863
8937
  lines.push(`export interface ApiResponse<T = unknown> {`);
@@ -8885,15 +8959,17 @@ var TypeGenerator = class {
8885
8959
  for (const model of manifest.models) {
8886
8960
  lines.push(`export interface ${model.name} {`);
8887
8961
  for (const col of model.columns) {
8888
- if (model.hidden && model.hidden.includes(col.name)) continue;
8962
+ if (model.hidden?.includes(col.name)) continue;
8889
8963
  const tsType = this.mapSqlTypeToTs(col.type);
8890
8964
  const finalTsType = col.nullable ? `${tsType} | null` : tsType;
8891
- const safeName = camelCase(col.name).match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) ? camelCase(col.name) : `"${camelCase(col.name)}"`;
8965
+ const key = camelCase(col.name);
8966
+ const safeName = key.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) ? key : `"${key}"`;
8892
8967
  lines.push(` ${safeName}: ${finalTsType}`);
8893
8968
  }
8894
8969
  if (model.appends && model.appends.length > 0) {
8895
8970
  for (const append of model.appends) {
8896
- const safeAppend = camelCase(append).match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) ? camelCase(append) : `"${camelCase(append)}"`;
8971
+ const key = camelCase(append);
8972
+ const safeAppend = key.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) ? key : `"${key}"`;
8897
8973
  lines.push(` ${safeAppend}?: unknown`);
8898
8974
  }
8899
8975
  }
@@ -8908,7 +8984,7 @@ var TypeGenerator = class {
8908
8984
  const typeName = toTypeName(resource ?? "");
8909
8985
  lines.push(`export interface ${typeName} {`);
8910
8986
  lines.push(` id: number`);
8911
- lines.push(` // TODO: Add ${resource} fields`);
8987
+ lines.push(` // TODO: Add ${resource} fields \u2014 run scan with --models for real types`);
8912
8988
  lines.push(` createdAt?: string`);
8913
8989
  lines.push(` updatedAt?: string`);
8914
8990
  lines.push(`}`);
package/dist/react.d.mts CHANGED
@@ -63,6 +63,12 @@ type EndpointCallableOptions<TParams, TBody> = (unknown extends TParams ? {
63
63
  query?: Record<string, any>;
64
64
  headers?: Record<string, string>;
65
65
  };
66
+ type LooseEndpointOptions = {
67
+ params?: unknown;
68
+ query?: Record<string, unknown>;
69
+ body?: unknown;
70
+ headers?: Record<string, string>;
71
+ };
66
72
  type RequiredKeys<T> = {
67
73
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
68
74
  }[keyof T];
@@ -74,7 +80,7 @@ interface ApiError {
74
80
  }
75
81
  interface EndpointCallable<TResponse = unknown, TParams = unknown, TBody = unknown> {
76
82
  (...args: OptionalIfEmpty<EndpointCallableOptions<TParams, TBody>>): Promise<TResponse>;
77
- (options: EndpointCallableOptions<TParams, TBody> | undefined): Promise<TResponse>;
83
+ (options: LooseEndpointOptions | undefined): Promise<TResponse>;
78
84
  (options: CallOptions<TParams, TBody>): Promise<TResponse>;
79
85
  /** Original RouteDefinition — used by useApiQuery / useApiMutation */
80
86
  $def: RouteDefinition<TResponse, TParams, TBody>;
@@ -86,9 +92,9 @@ interface EndpointCallable<TResponse = unknown, TParams = unknown, TBody = unkno
86
92
 
87
93
  type ApiQueryOptions<TResponse, TError = ApiError, TData = TResponse> = Omit<UseQueryOptions<TResponse, TError, TData>, 'queryKey' | 'queryFn'>;
88
94
  declare function useApiQuery<TResponse, TParams, TBody, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options: EndpointCallableOptions<TParams, TBody>, queryOptions?: ApiQueryOptions<TResponse, TError, TData>): ReturnType<typeof useQuery<TResponse, TError, TData>>;
89
- declare function useApiQuery<TResponse, TParams, TBody, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options?: EndpointCallableOptions<TParams, TBody>, queryOptions?: ApiQueryOptions<TResponse, TError, TData>): ReturnType<typeof useQuery<TResponse, TError, TData>>;
90
- declare function useApiSuspenseQuery<TResponse = unknown, TParams = unknown, TBody = unknown, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options?: EndpointCallableOptions<TParams, TBody>, queryOptions?: Omit<UseSuspenseQueryOptions<TResponse, TError, TData>, 'queryKey' | 'queryFn'>): _tanstack_react_query.UseSuspenseQueryResult<TData, TError>;
91
- declare function useApiInfiniteQuery<TResponse = unknown, TParams = unknown, TBody = unknown, TError = ApiError, TData = InfiniteData<TResponse>, TPageParam = unknown>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options: EndpointCallableOptions<TParams, TBody> | undefined, queryOptions: Omit<UseInfiniteQueryOptions<TResponse, TError, TData, any, TPageParam>, 'queryKey' | 'queryFn'> & {
95
+ declare function useApiQuery<TResponse, TParams, TBody, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options?: LooseEndpointOptions, queryOptions?: ApiQueryOptions<TResponse, TError, TData>): ReturnType<typeof useQuery<TResponse, TError, TData>>;
96
+ declare function useApiSuspenseQuery<TResponse = unknown, TParams = unknown, TBody = unknown, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options?: LooseEndpointOptions, queryOptions?: Omit<UseSuspenseQueryOptions<TResponse, TError, TData>, 'queryKey' | 'queryFn'>): _tanstack_react_query.UseSuspenseQueryResult<TData, TError>;
97
+ declare function useApiInfiniteQuery<TResponse = unknown, TParams = unknown, TBody = unknown, TError = ApiError, TData = InfiniteData<TResponse>, TPageParam = unknown>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options: LooseEndpointOptions | undefined, queryOptions: Omit<UseInfiniteQueryOptions<TResponse, TError, TData, any, TPageParam>, 'queryKey' | 'queryFn'> & {
92
98
  getNextPageParam: UseInfiniteQueryOptions<TResponse, TError, TData, any, TPageParam>['getNextPageParam'];
93
99
  }): _tanstack_react_query.UseInfiniteQueryResult<TData, TError>;
94
100
 
package/dist/react.d.ts CHANGED
@@ -63,6 +63,12 @@ type EndpointCallableOptions<TParams, TBody> = (unknown extends TParams ? {
63
63
  query?: Record<string, any>;
64
64
  headers?: Record<string, string>;
65
65
  };
66
+ type LooseEndpointOptions = {
67
+ params?: unknown;
68
+ query?: Record<string, unknown>;
69
+ body?: unknown;
70
+ headers?: Record<string, string>;
71
+ };
66
72
  type RequiredKeys<T> = {
67
73
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
68
74
  }[keyof T];
@@ -74,7 +80,7 @@ interface ApiError {
74
80
  }
75
81
  interface EndpointCallable<TResponse = unknown, TParams = unknown, TBody = unknown> {
76
82
  (...args: OptionalIfEmpty<EndpointCallableOptions<TParams, TBody>>): Promise<TResponse>;
77
- (options: EndpointCallableOptions<TParams, TBody> | undefined): Promise<TResponse>;
83
+ (options: LooseEndpointOptions | undefined): Promise<TResponse>;
78
84
  (options: CallOptions<TParams, TBody>): Promise<TResponse>;
79
85
  /** Original RouteDefinition — used by useApiQuery / useApiMutation */
80
86
  $def: RouteDefinition<TResponse, TParams, TBody>;
@@ -86,9 +92,9 @@ interface EndpointCallable<TResponse = unknown, TParams = unknown, TBody = unkno
86
92
 
87
93
  type ApiQueryOptions<TResponse, TError = ApiError, TData = TResponse> = Omit<UseQueryOptions<TResponse, TError, TData>, 'queryKey' | 'queryFn'>;
88
94
  declare function useApiQuery<TResponse, TParams, TBody, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options: EndpointCallableOptions<TParams, TBody>, queryOptions?: ApiQueryOptions<TResponse, TError, TData>): ReturnType<typeof useQuery<TResponse, TError, TData>>;
89
- declare function useApiQuery<TResponse, TParams, TBody, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options?: EndpointCallableOptions<TParams, TBody>, queryOptions?: ApiQueryOptions<TResponse, TError, TData>): ReturnType<typeof useQuery<TResponse, TError, TData>>;
90
- declare function useApiSuspenseQuery<TResponse = unknown, TParams = unknown, TBody = unknown, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options?: EndpointCallableOptions<TParams, TBody>, queryOptions?: Omit<UseSuspenseQueryOptions<TResponse, TError, TData>, 'queryKey' | 'queryFn'>): _tanstack_react_query.UseSuspenseQueryResult<TData, TError>;
91
- declare function useApiInfiniteQuery<TResponse = unknown, TParams = unknown, TBody = unknown, TError = ApiError, TData = InfiniteData<TResponse>, TPageParam = unknown>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options: EndpointCallableOptions<TParams, TBody> | undefined, queryOptions: Omit<UseInfiniteQueryOptions<TResponse, TError, TData, any, TPageParam>, 'queryKey' | 'queryFn'> & {
95
+ declare function useApiQuery<TResponse, TParams, TBody, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options?: LooseEndpointOptions, queryOptions?: ApiQueryOptions<TResponse, TError, TData>): ReturnType<typeof useQuery<TResponse, TError, TData>>;
96
+ declare function useApiSuspenseQuery<TResponse = unknown, TParams = unknown, TBody = unknown, TError = ApiError, TData = TResponse>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options?: LooseEndpointOptions, queryOptions?: Omit<UseSuspenseQueryOptions<TResponse, TError, TData>, 'queryKey' | 'queryFn'>): _tanstack_react_query.UseSuspenseQueryResult<TData, TError>;
97
+ declare function useApiInfiniteQuery<TResponse = unknown, TParams = unknown, TBody = unknown, TError = ApiError, TData = InfiniteData<TResponse>, TPageParam = unknown>(endpoint: EndpointCallable<TResponse, TParams, TBody>, options: LooseEndpointOptions | undefined, queryOptions: Omit<UseInfiniteQueryOptions<TResponse, TError, TData, any, TPageParam>, 'queryKey' | 'queryFn'> & {
92
98
  getNextPageParam: UseInfiniteQueryOptions<TResponse, TError, TData, any, TPageParam>['getNextPageParam'];
93
99
  }): _tanstack_react_query.UseInfiniteQueryResult<TData, TError>;
94
100
 
package/dist/react.js CHANGED
@@ -33,30 +33,30 @@ module.exports = __toCommonJS(src_exports);
33
33
  // packages/react/src/hooks/useQuery.ts
34
34
  var import_react_query = require("@tanstack/react-query");
35
35
  function useApiQuery(endpoint, options, queryOptions) {
36
- const queryKey = endpoint.$queryKey(options);
37
- const callEndpoint = endpoint;
36
+ const queryKey = options ? [...endpoint.$key, options] : endpoint.$key;
38
37
  return (0, import_react_query.useQuery)({
39
38
  queryKey,
40
- queryFn: () => callEndpoint(options),
39
+ queryFn: () => endpoint(options),
41
40
  ...queryOptions
42
41
  });
43
42
  }
44
43
  function useApiSuspenseQuery(endpoint, options, queryOptions) {
45
- const queryKey = endpoint.$queryKey(options);
46
- const callEndpoint = endpoint;
44
+ const queryKey = options ? [...endpoint.$key, options] : endpoint.$key;
47
45
  return (0, import_react_query.useSuspenseQuery)({
48
46
  queryKey,
49
- queryFn: () => callEndpoint(options),
47
+ queryFn: () => endpoint(options),
50
48
  ...queryOptions
51
49
  });
52
50
  }
53
51
  function useApiInfiniteQuery(endpoint, options, queryOptions) {
54
- const queryKey = endpoint.$queryKey(options);
52
+ const queryKey = options ? [...endpoint.$key, options] : endpoint.$key;
55
53
  return (0, import_react_query.useInfiniteQuery)({
56
54
  queryKey,
57
55
  queryFn: ({ pageParam }) => {
58
56
  const callOptions = {
59
- ...options,
57
+ params: options?.params,
58
+ body: options?.body,
59
+ headers: options?.headers,
60
60
  query: { ...options?.query, page: pageParam }
61
61
  };
62
62
  return endpoint(callOptions);
package/dist/react.mjs CHANGED
@@ -1,30 +1,30 @@
1
1
  // packages/react/src/hooks/useQuery.ts
2
2
  import { useQuery, useSuspenseQuery, useInfiniteQuery } from "@tanstack/react-query";
3
3
  function useApiQuery(endpoint, options, queryOptions) {
4
- const queryKey = endpoint.$queryKey(options);
5
- const callEndpoint = endpoint;
4
+ const queryKey = options ? [...endpoint.$key, options] : endpoint.$key;
6
5
  return useQuery({
7
6
  queryKey,
8
- queryFn: () => callEndpoint(options),
7
+ queryFn: () => endpoint(options),
9
8
  ...queryOptions
10
9
  });
11
10
  }
12
11
  function useApiSuspenseQuery(endpoint, options, queryOptions) {
13
- const queryKey = endpoint.$queryKey(options);
14
- const callEndpoint = endpoint;
12
+ const queryKey = options ? [...endpoint.$key, options] : endpoint.$key;
15
13
  return useSuspenseQuery({
16
14
  queryKey,
17
- queryFn: () => callEndpoint(options),
15
+ queryFn: () => endpoint(options),
18
16
  ...queryOptions
19
17
  });
20
18
  }
21
19
  function useApiInfiniteQuery(endpoint, options, queryOptions) {
22
- const queryKey = endpoint.$queryKey(options);
20
+ const queryKey = options ? [...endpoint.$key, options] : endpoint.$key;
23
21
  return useInfiniteQuery({
24
22
  queryKey,
25
23
  queryFn: ({ pageParam }) => {
26
24
  const callOptions = {
27
- ...options,
25
+ params: options?.params,
26
+ body: options?.body,
27
+ headers: options?.headers,
28
28
  query: { ...options?.query, page: pageParam }
29
29
  };
30
30
  return endpoint(callOptions);
package/dist/sdk.d.mts CHANGED
@@ -133,6 +133,12 @@ type EndpointCallableOptions<TParams, TBody> = (unknown extends TParams ? {
133
133
  query?: Record<string, any>;
134
134
  headers?: Record<string, string>;
135
135
  };
136
+ type LooseEndpointOptions = {
137
+ params?: unknown;
138
+ query?: Record<string, unknown>;
139
+ body?: unknown;
140
+ headers?: Record<string, string>;
141
+ };
136
142
  type RequiredKeys<T> = {
137
143
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
138
144
  }[keyof T];
@@ -144,7 +150,7 @@ interface ApiError {
144
150
  }
145
151
  interface EndpointCallable<TResponse = unknown, TParams = unknown, TBody = unknown> {
146
152
  (...args: OptionalIfEmpty<EndpointCallableOptions<TParams, TBody>>): Promise<TResponse>;
147
- (options: EndpointCallableOptions<TParams, TBody> | undefined): Promise<TResponse>;
153
+ (options: LooseEndpointOptions | undefined): Promise<TResponse>;
148
154
  (options: CallOptions<TParams, TBody>): Promise<TResponse>;
149
155
  /** Original RouteDefinition — used by useApiQuery / useApiMutation */
150
156
  $def: RouteDefinition<TResponse, TParams, TBody>;
@@ -312,4 +318,4 @@ declare function mapKeysDeep<T>(value: T, keyCase: KeyCase): T;
312
318
  declare function toCamelCase<T>(value: T): T;
313
319
  declare function toSnakeCase<T>(value: T): T;
314
320
 
315
- export { type ApiDefinition, type ApiError, type ApiResponse, type CallOptions, type CamelCasedPropertiesDeep, type CamelToSnake, type EndpointCallable, type EndpointCallableOptions, type EndpointDefinition$1 as EndpointDefinition, GenericService, type GenericServiceOptions, type HttpMethod, type Id, type KeyCase, type OptionalIfEmpty, type ParseResult, type ParserSchema, type QueryParams, type ResourceConfig, type ResourceDefinition, type RouteDefinition, type RouteMapper, type RouteParserSchema, type RouteSchema, type RouteSchemaMap, type RouteSchemaValue, type RouteTransform, type RouteTransformMap, type SchemaLike, type ServiceConfig, type SnakeCasedPropertiesDeep, type SnakeToCamel, type UnknownRecord, camelToSnakeKey, createClient$1 as createClient, createClient as createHttpClient, createService, defineApi, endpoint, generateHooks, mapKeysDeep, parseWithSchema, resource, snakeToCamelKey, toCamelCase, toSnakeCase };
321
+ export { type ApiDefinition, type ApiError, type ApiResponse, type CallOptions, type CamelCasedPropertiesDeep, type CamelToSnake, type EndpointCallable, type EndpointCallableOptions, type EndpointDefinition$1 as EndpointDefinition, GenericService, type GenericServiceOptions, type HttpMethod, type Id, type KeyCase, type LooseEndpointOptions, type OptionalIfEmpty, type ParseResult, type ParserSchema, type QueryParams, type ResourceConfig, type ResourceDefinition, type RouteDefinition, type RouteMapper, type RouteParserSchema, type RouteSchema, type RouteSchemaMap, type RouteSchemaValue, type RouteTransform, type RouteTransformMap, type SchemaLike, type ServiceConfig, type SnakeCasedPropertiesDeep, type SnakeToCamel, type UnknownRecord, camelToSnakeKey, createClient$1 as createClient, createClient as createHttpClient, createService, defineApi, endpoint, generateHooks, mapKeysDeep, parseWithSchema, resource, snakeToCamelKey, toCamelCase, toSnakeCase };
package/dist/sdk.d.ts CHANGED
@@ -133,6 +133,12 @@ type EndpointCallableOptions<TParams, TBody> = (unknown extends TParams ? {
133
133
  query?: Record<string, any>;
134
134
  headers?: Record<string, string>;
135
135
  };
136
+ type LooseEndpointOptions = {
137
+ params?: unknown;
138
+ query?: Record<string, unknown>;
139
+ body?: unknown;
140
+ headers?: Record<string, string>;
141
+ };
136
142
  type RequiredKeys<T> = {
137
143
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
138
144
  }[keyof T];
@@ -144,7 +150,7 @@ interface ApiError {
144
150
  }
145
151
  interface EndpointCallable<TResponse = unknown, TParams = unknown, TBody = unknown> {
146
152
  (...args: OptionalIfEmpty<EndpointCallableOptions<TParams, TBody>>): Promise<TResponse>;
147
- (options: EndpointCallableOptions<TParams, TBody> | undefined): Promise<TResponse>;
153
+ (options: LooseEndpointOptions | undefined): Promise<TResponse>;
148
154
  (options: CallOptions<TParams, TBody>): Promise<TResponse>;
149
155
  /** Original RouteDefinition — used by useApiQuery / useApiMutation */
150
156
  $def: RouteDefinition<TResponse, TParams, TBody>;
@@ -312,4 +318,4 @@ declare function mapKeysDeep<T>(value: T, keyCase: KeyCase): T;
312
318
  declare function toCamelCase<T>(value: T): T;
313
319
  declare function toSnakeCase<T>(value: T): T;
314
320
 
315
- export { type ApiDefinition, type ApiError, type ApiResponse, type CallOptions, type CamelCasedPropertiesDeep, type CamelToSnake, type EndpointCallable, type EndpointCallableOptions, type EndpointDefinition$1 as EndpointDefinition, GenericService, type GenericServiceOptions, type HttpMethod, type Id, type KeyCase, type OptionalIfEmpty, type ParseResult, type ParserSchema, type QueryParams, type ResourceConfig, type ResourceDefinition, type RouteDefinition, type RouteMapper, type RouteParserSchema, type RouteSchema, type RouteSchemaMap, type RouteSchemaValue, type RouteTransform, type RouteTransformMap, type SchemaLike, type ServiceConfig, type SnakeCasedPropertiesDeep, type SnakeToCamel, type UnknownRecord, camelToSnakeKey, createClient$1 as createClient, createClient as createHttpClient, createService, defineApi, endpoint, generateHooks, mapKeysDeep, parseWithSchema, resource, snakeToCamelKey, toCamelCase, toSnakeCase };
321
+ export { type ApiDefinition, type ApiError, type ApiResponse, type CallOptions, type CamelCasedPropertiesDeep, type CamelToSnake, type EndpointCallable, type EndpointCallableOptions, type EndpointDefinition$1 as EndpointDefinition, GenericService, type GenericServiceOptions, type HttpMethod, type Id, type KeyCase, type LooseEndpointOptions, type OptionalIfEmpty, type ParseResult, type ParserSchema, type QueryParams, type ResourceConfig, type ResourceDefinition, type RouteDefinition, type RouteMapper, type RouteParserSchema, type RouteSchema, type RouteSchemaMap, type RouteSchemaValue, type RouteTransform, type RouteTransformMap, type SchemaLike, type ServiceConfig, type SnakeCasedPropertiesDeep, type SnakeToCamel, type UnknownRecord, camelToSnakeKey, createClient$1 as createClient, createClient as createHttpClient, createService, defineApi, endpoint, generateHooks, mapKeysDeep, parseWithSchema, resource, snakeToCamelKey, toCamelCase, toSnakeCase };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routesync",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Laravel routes to typed frontend SDKs.",
5
5
  "main": "./dist/sdk.js",
6
6
  "module": "./dist/sdk.mjs",