@happyvertical/smrt-core 0.46.0 → 0.47.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.
- package/AGENTS.md +39 -0
- package/agents/generators.md +19 -5
- package/agents/system-diagnostics.md +43 -0
- package/dist/browser.js +2 -1
- package/dist/change-feed.d.ts +8 -0
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +1 -1
- package/dist/change-feed.js.map +1 -1
- package/dist/decorators/compatibility.d.ts +46 -0
- package/dist/decorators/compatibility.d.ts.map +1 -1
- package/dist/decorators/compatibility.js +48 -5
- package/dist/decorators/compatibility.js.map +1 -1
- package/dist/decorators/index.d.ts +119 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +52 -8
- package/dist/decorators/index.js.map +1 -1
- package/dist/generators/custom-action.d.ts +379 -2
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +694 -17
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/generators/index.d.ts +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -2
- package/dist/generators/preflight-route.d.ts +12 -10
- package/dist/generators/preflight-route.d.ts.map +1 -1
- package/dist/generators/preflight-route.js +46 -14
- package/dist/generators/preflight-route.js.map +1 -1
- package/dist/generators/rest.d.ts +44 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +84 -9
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +59 -15
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +125 -53
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +184 -53
- package/dist/postgres-permissions.d.ts +9 -1
- package/dist/postgres-permissions.d.ts.map +1 -1
- package/dist/postgres-permissions.js +127 -13
- package/dist/postgres-permissions.js.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/shared-state.d.ts +19 -0
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js +16 -1
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/registry.d.ts +136 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +231 -1
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +18 -16
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +59 -6
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/smrt-knowledge.json +42 -35
- package/dist/system/diagnostics.d.ts +299 -0
- package/dist/system/diagnostics.d.ts.map +1 -0
- package/dist/system/diagnostics.js +530 -0
- package/dist/system/diagnostics.js.map +1 -0
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +2 -1
- package/dist/utils/scanner-module.d.ts +16 -0
- package/dist/utils/scanner-module.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +10 -8
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +34 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +82 -3
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +28 -2
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +121 -51
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +1 -1
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/dist/vite-plugin.js +2 -2
- package/package.json +16 -11
|
@@ -303,9 +303,9 @@ function customActionParameterInputName(_metadata, parameterName) {
|
|
|
303
303
|
*/
|
|
304
304
|
function resolveCustomActionMetadata(options) {
|
|
305
305
|
const defaultScope = options.defaultScope ?? (options.method?.isStatic ? "collection" : "item");
|
|
306
|
-
const requestedScope = readConfiguredScope(options
|
|
306
|
+
const requestedScope = readConfiguredScope(options);
|
|
307
307
|
const scope = requestedScope === defaultScope ? requestedScope : defaultScope;
|
|
308
|
-
const configured = readConfiguredToolMetadata(options
|
|
308
|
+
const configured = readConfiguredToolMetadata(options);
|
|
309
309
|
const effect = configured.effect ?? "destructive";
|
|
310
310
|
return {
|
|
311
311
|
scope,
|
|
@@ -317,6 +317,25 @@ function resolveCustomActionMetadata(options) {
|
|
|
317
317
|
openWorld: configured.openWorld ?? true
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* The declared scope, when it contradicts the receiver the method actually
|
|
322
|
+
* has; `undefined` when there is no declaration or it agrees.
|
|
323
|
+
*
|
|
324
|
+
* A scope is a DECLARATION about a method, not a relocation of it: nothing in
|
|
325
|
+
* a config can move an instance method onto the class. Silently ignoring a
|
|
326
|
+
* contradiction leaves an author believing a route exists at a collection URL
|
|
327
|
+
* that was never written, so the generators report this at build time
|
|
328
|
+
* (#2686).
|
|
329
|
+
*/
|
|
330
|
+
function resolveDeclaredScopeMismatch(options) {
|
|
331
|
+
const declared = resolveEffectiveActionMetadata({
|
|
332
|
+
actionName: options.actionName,
|
|
333
|
+
...options.method ? { method: options.method } : {},
|
|
334
|
+
apiConfig: options.apiConfig
|
|
335
|
+
}).scope;
|
|
336
|
+
if (!declared || declared === options.effectiveScope) return void 0;
|
|
337
|
+
return declared;
|
|
338
|
+
}
|
|
320
339
|
/** Build the custom-action portion of an MCP/WebMCP JSON Schema. */
|
|
321
340
|
function buildCustomActionInputSchema(metadata) {
|
|
322
341
|
const properties = {};
|
|
@@ -365,7 +384,7 @@ function buildCustomActionInvocationArgs(metadata, args) {
|
|
|
365
384
|
if (!metadata.parameters) return [isRecord(options) && Object.keys(options).length > 0 ? options : directArgs];
|
|
366
385
|
if (metadata.parameters.length === 0) return [];
|
|
367
386
|
if (metadata.parameters.length === 1 && metadata.parameters[0]?.name === "options") return [options];
|
|
368
|
-
return metadata.parameters.map((parameter) => args[customActionParameterInputName(metadata, parameter.name)]);
|
|
387
|
+
return metadata.parameters.map((parameter) => coerceCustomActionArgument(args[customActionParameterInputName(metadata, parameter.name)], parameter.type));
|
|
369
388
|
}
|
|
370
389
|
/** Stable MCP `_meta` member shared with the app discovery contract (#2181). */
|
|
371
390
|
var SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY = "io.happyvertical/smrt";
|
|
@@ -388,21 +407,24 @@ function normalizeCustomActionFailure(value) {
|
|
|
388
407
|
...typeof value.correlationId === "string" ? { correlationId: value.correlationId } : {}
|
|
389
408
|
};
|
|
390
409
|
}
|
|
391
|
-
function readConfiguredScope(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
410
|
+
function readConfiguredScope(options) {
|
|
411
|
+
return resolveEffectiveActionMetadata({
|
|
412
|
+
actionName: options.actionName,
|
|
413
|
+
...options.method ? { method: options.method } : {},
|
|
414
|
+
apiConfig: options.apiConfig
|
|
415
|
+
}).scope;
|
|
395
416
|
}
|
|
396
|
-
function readConfiguredToolMetadata(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
function readConfiguredToolMetadata(options) {
|
|
418
|
+
const effective = resolveEffectiveActionMetadata({
|
|
419
|
+
actionName: options.actionName,
|
|
420
|
+
...options.method ? { method: options.method } : {},
|
|
421
|
+
apiConfig: options.apiConfig
|
|
422
|
+
});
|
|
423
|
+
if (effective.effect === "read" && (effective.httpMethod === "PUT" || effective.httpMethod === "PATCH" || effective.httpMethod === "DELETE")) throw new Error(`Custom action ${options.actionName} cannot declare a read effect for a ${effective.httpMethod} route`);
|
|
402
424
|
return {
|
|
403
|
-
...effect ? { effect } : {},
|
|
404
|
-
...
|
|
405
|
-
...
|
|
425
|
+
...effective.effect ? { effect: effective.effect } : {},
|
|
426
|
+
...effective.idempotent !== void 0 ? { idempotent: effective.idempotent } : {},
|
|
427
|
+
...effective.openWorld !== void 0 ? { openWorld: effective.openWorld } : {}
|
|
406
428
|
};
|
|
407
429
|
}
|
|
408
430
|
function redactValue(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
@@ -426,7 +448,662 @@ function isSensitiveKey(key) {
|
|
|
426
448
|
function isRecord(value) {
|
|
427
449
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
428
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Read and validate the `@method()` config the scanner put on a manifest
|
|
453
|
+
* method. Returns `undefined` for an undecorated method.
|
|
454
|
+
*/
|
|
455
|
+
function readMethodDecoratorConfig(method) {
|
|
456
|
+
const raw = method?.decoratorConfig;
|
|
457
|
+
if (!isRecord(raw)) return void 0;
|
|
458
|
+
const scope = raw.scope === "item" || raw.scope === "collection" ? raw.scope : void 0;
|
|
459
|
+
const effect = raw.effect === "read" || raw.effect === "write" || raw.effect === "destructive" ? raw.effect : void 0;
|
|
460
|
+
return {
|
|
461
|
+
...typeof raw.expose === "boolean" ? { expose: raw.expose } : {},
|
|
462
|
+
...typeof raw.reason === "string" ? { reason: raw.reason } : {},
|
|
463
|
+
...isApiHttpMethod(raw.httpMethod) ? { httpMethod: raw.httpMethod } : {},
|
|
464
|
+
...typeof raw.path === "string" ? { path: raw.path } : {},
|
|
465
|
+
...scope ? { scope } : {},
|
|
466
|
+
...effect ? { effect } : {},
|
|
467
|
+
...typeof raw.idempotent === "boolean" ? { idempotent: raw.idempotent } : {},
|
|
468
|
+
...typeof raw.openWorld === "boolean" ? { openWorld: raw.openWorld } : {},
|
|
469
|
+
...typeof raw.description === "string" ? { description: raw.description } : {}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function isApiHttpMethod(value) {
|
|
473
|
+
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* JavaScript values a JSON request body or query string cannot carry, keyed by
|
|
477
|
+
* the type NAME the manifest records for them.
|
|
478
|
+
*
|
|
479
|
+
* Deliberately a name list rather than "anything not primitive": the manifest
|
|
480
|
+
* cannot tell an interface from a class, so an unrecognized capitalized name
|
|
481
|
+
* is assumed to be a plain data bag (see {@link isWireableTypeName}). These are
|
|
482
|
+
* the well-known exceptions where that assumption is wrong for every project.
|
|
483
|
+
* Model classes are excluded separately, by asking the manifest.
|
|
484
|
+
*/
|
|
485
|
+
var NON_SERIALIZABLE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
486
|
+
"Function",
|
|
487
|
+
"bigint",
|
|
488
|
+
"symbol",
|
|
489
|
+
"Buffer",
|
|
490
|
+
"ArrayBuffer",
|
|
491
|
+
"SharedArrayBuffer",
|
|
492
|
+
"DataView",
|
|
493
|
+
"Uint8Array",
|
|
494
|
+
"Int8Array",
|
|
495
|
+
"Uint16Array",
|
|
496
|
+
"Int16Array",
|
|
497
|
+
"Uint32Array",
|
|
498
|
+
"Int32Array",
|
|
499
|
+
"Float32Array",
|
|
500
|
+
"Float64Array",
|
|
501
|
+
"BigInt64Array",
|
|
502
|
+
"BigUint64Array",
|
|
503
|
+
"Blob",
|
|
504
|
+
"File",
|
|
505
|
+
"FormData",
|
|
506
|
+
"ReadableStream",
|
|
507
|
+
"WritableStream",
|
|
508
|
+
"TransformStream",
|
|
509
|
+
"Stream",
|
|
510
|
+
"Readable",
|
|
511
|
+
"Writable",
|
|
512
|
+
"Request",
|
|
513
|
+
"Response",
|
|
514
|
+
"Headers",
|
|
515
|
+
"URL",
|
|
516
|
+
"URLSearchParams",
|
|
517
|
+
"AbortSignal",
|
|
518
|
+
"AbortController",
|
|
519
|
+
"Map",
|
|
520
|
+
"Set",
|
|
521
|
+
"WeakMap",
|
|
522
|
+
"WeakSet",
|
|
523
|
+
"RegExp",
|
|
524
|
+
"Error",
|
|
525
|
+
"Symbol",
|
|
526
|
+
"Promise",
|
|
527
|
+
"SmrtDatabase",
|
|
528
|
+
"DatabaseInterface",
|
|
529
|
+
"SmrtCollection",
|
|
530
|
+
"SmrtObject"
|
|
531
|
+
]);
|
|
532
|
+
/**
|
|
533
|
+
* Generic container names whose ARGUMENTS carry the payload. `Record` is
|
|
534
|
+
* included: its value type is checked, its key type is always a string-ish
|
|
535
|
+
* index and never a receiver.
|
|
536
|
+
*/
|
|
537
|
+
var JSON_CONTAINER_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
538
|
+
"Array",
|
|
539
|
+
"ReadonlyArray",
|
|
540
|
+
"Record",
|
|
541
|
+
"Partial",
|
|
542
|
+
"Required",
|
|
543
|
+
"Readonly",
|
|
544
|
+
"Pick",
|
|
545
|
+
"Omit",
|
|
546
|
+
"NonNullable"
|
|
547
|
+
]);
|
|
548
|
+
/** Primitive/JSON-native type names a wire request can always carry. */
|
|
549
|
+
var JSON_PRIMITIVE_TYPE_NAMES = /* @__PURE__ */ new Set([
|
|
550
|
+
"string",
|
|
551
|
+
"number",
|
|
552
|
+
"boolean",
|
|
553
|
+
"null",
|
|
554
|
+
"undefined",
|
|
555
|
+
"void",
|
|
556
|
+
"any",
|
|
557
|
+
"unknown",
|
|
558
|
+
"object",
|
|
559
|
+
"Date"
|
|
560
|
+
]);
|
|
561
|
+
var WIREABLE = { wireable: true };
|
|
562
|
+
/**
|
|
563
|
+
* True when a manifest type NAME can be carried by a JSON request body.
|
|
564
|
+
*
|
|
565
|
+
* The default is ACCEPT: the manifest records types as strings and cannot tell
|
|
566
|
+
* `RunContentReviewOptions` (an interface — a plain bag) from `Content` (a
|
|
567
|
+
* model class). Rejecting every unrecognized capitalized name would withhold
|
|
568
|
+
* routes from the overwhelmingly common options-bag shape, so an unrecognized
|
|
569
|
+
* name is assumed to be a bag and rejection is driven by positive evidence:
|
|
570
|
+
* a known non-serializable runtime type, a manifest class, or a bare type
|
|
571
|
+
* parameter.
|
|
572
|
+
*/
|
|
573
|
+
function classifyTypeName(typeName, options, depth) {
|
|
574
|
+
const type = typeName.trim();
|
|
575
|
+
if (type === "") return WIREABLE;
|
|
576
|
+
if (type === "Date" && depth > 0) return {
|
|
577
|
+
wireable: false,
|
|
578
|
+
reason: "`Date` is only hydrated as a top-level parameter, so a nested one arrives as a string"
|
|
579
|
+
};
|
|
580
|
+
const unionParts = splitTopLevel(type, "|");
|
|
581
|
+
if (unionParts.length > 1) {
|
|
582
|
+
const branches = unionParts.filter((branch) => branch !== "null" && branch !== "undefined");
|
|
583
|
+
if (branches.length === 0) return WIREABLE;
|
|
584
|
+
const verdicts = branches.map((branch) => classifyTypeName(branch, options, depth));
|
|
585
|
+
if (verdicts.some((verdict) => verdict.wireable)) return WIREABLE;
|
|
586
|
+
return {
|
|
587
|
+
wireable: false,
|
|
588
|
+
reason: `every branch of \`${type}\` is unreachable over HTTP (${verdicts[0]?.reason ?? "not JSON-shaped"})`
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
if (type.endsWith("[]")) return classifyTypeName(type.slice(0, -2), options, depth + 1);
|
|
592
|
+
if (/^'.*'$/su.test(type) || /^-?\d/u.test(type)) return WIREABLE;
|
|
593
|
+
const generic = /^([\w$.]+)\s*<(.*)>$/su.exec(type);
|
|
594
|
+
if (generic) {
|
|
595
|
+
const base = generic[1];
|
|
596
|
+
if (JSON_CONTAINER_TYPE_NAMES.has(base)) {
|
|
597
|
+
if (depth >= WIREABILITY_MAX_DEPTH) return WIREABLE;
|
|
598
|
+
const args = splitTopLevel(generic[2], ",");
|
|
599
|
+
for (const arg of args) {
|
|
600
|
+
const verdict = classifyTypeName(arg, options, depth + 1);
|
|
601
|
+
if (!verdict.wireable) return verdict;
|
|
602
|
+
}
|
|
603
|
+
return WIREABLE;
|
|
604
|
+
}
|
|
605
|
+
return classifyTypeName(base, options, depth + 1);
|
|
606
|
+
}
|
|
607
|
+
if (type === "this") return {
|
|
608
|
+
wireable: false,
|
|
609
|
+
reason: "`this` is an instance of the declaring model class, not JSON data"
|
|
610
|
+
};
|
|
611
|
+
if (NON_SERIALIZABLE_TYPE_NAMES.has(type)) return {
|
|
612
|
+
wireable: false,
|
|
613
|
+
reason: `\`${type}\` is a runtime value a JSON request cannot carry`
|
|
614
|
+
};
|
|
615
|
+
if (JSON_PRIMITIVE_TYPE_NAMES.has(type)) return WIREABLE;
|
|
616
|
+
if (/^T(?:[0-9]|[A-Z][A-Za-z0-9]*)?$/u.test(type) || /^[A-Z]$/u.test(type)) return {
|
|
617
|
+
wireable: false,
|
|
618
|
+
reason: `\`${type}\` is an unresolved type parameter`
|
|
619
|
+
};
|
|
620
|
+
const simpleName = type.includes(".") ? type.split(".").pop() : type;
|
|
621
|
+
if (options.isModelClassName?.(simpleName)) return {
|
|
622
|
+
wireable: false,
|
|
623
|
+
reason: `\`${simpleName}\` is a model class instance, not JSON data`
|
|
624
|
+
};
|
|
625
|
+
return WIREABLE;
|
|
626
|
+
}
|
|
627
|
+
/** Recursion bound for generic type arguments. */
|
|
628
|
+
var WIREABILITY_MAX_DEPTH = 6;
|
|
629
|
+
/**
|
|
630
|
+
* Class names a manifest knows about, cached per manifest object.
|
|
631
|
+
*
|
|
632
|
+
* The manifest is rebuilt, never mutated in place, so identity is a safe cache
|
|
633
|
+
* key; a `WeakMap` keeps a discarded manifest's set collectable.
|
|
634
|
+
*/
|
|
635
|
+
var manifestClassNameCache = /* @__PURE__ */ new WeakMap();
|
|
636
|
+
/**
|
|
637
|
+
* Build the `isModelClassName` predicate {@link classifyMethodWireability}
|
|
638
|
+
* needs, from a manifest.
|
|
639
|
+
*
|
|
640
|
+
* Both the SIMPLE and QUALIFIED name of every manifest class are registered: a
|
|
641
|
+
* parameter is annotated with the simple name in source, but a qualified name
|
|
642
|
+
* can reach the predicate through a `TSQualifiedName` annotation.
|
|
643
|
+
*
|
|
644
|
+
* Returns `undefined` for a missing manifest, which widens the gate — see
|
|
645
|
+
* {@link WireabilityOptions.isModelClassName}.
|
|
646
|
+
*/
|
|
647
|
+
function createManifestClassNamePredicate(manifest) {
|
|
648
|
+
if (!manifest) return void 0;
|
|
649
|
+
let names = manifestClassNameCache.get(manifest);
|
|
650
|
+
if (!names) {
|
|
651
|
+
const collected = /* @__PURE__ */ new Set();
|
|
652
|
+
for (const [key, object] of Object.entries(manifest.objects ?? {})) {
|
|
653
|
+
collected.add(key);
|
|
654
|
+
if (object?.className) collected.add(object.className);
|
|
655
|
+
if (object?.qualifiedName) collected.add(object.qualifiedName);
|
|
656
|
+
}
|
|
657
|
+
names = collected;
|
|
658
|
+
manifestClassNameCache.set(manifest, names);
|
|
659
|
+
}
|
|
660
|
+
return createClassNamePredicate(names);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* The same predicate from a bare name list, for a caller whose class inventory
|
|
664
|
+
* is the live `ObjectRegistry` rather than a manifest — notably
|
|
665
|
+
* `@happyvertical/smrt-users`' CLI resource listing, which iterates
|
|
666
|
+
* `ObjectRegistry.getAllClasses()` and has no manifest to hand.
|
|
667
|
+
*
|
|
668
|
+
* Exported so that caller does not grow its own copy: without a predicate the
|
|
669
|
+
* gate half-applies (every rejection EXCEPT model instances), which is worse
|
|
670
|
+
* than either extreme because the consumer then disagrees with the emitters on
|
|
671
|
+
* exactly the largest group of withheld methods.
|
|
672
|
+
*/
|
|
673
|
+
function createClassNamePredicate(names) {
|
|
674
|
+
const resolved = names instanceof Set ? names : new Set(names);
|
|
675
|
+
return (name) => resolved.has(name);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Split a type string on `delimiter`, but only where it appears OUTSIDE every
|
|
679
|
+
* bracket pair — so `Record<string, Asset | null>` splits into two parts on
|
|
680
|
+
* `,` and one part on `|`, never into truncated fragments like `Record<string`.
|
|
681
|
+
*
|
|
682
|
+
* A naive `split()` on either delimiter produces fragments that match no
|
|
683
|
+
* classification rule and are therefore accepted by the default-accept path,
|
|
684
|
+
* silently widening the gate. Both the union test and the type-argument scan
|
|
685
|
+
* read this one implementation.
|
|
686
|
+
*/
|
|
687
|
+
function splitTopLevel(source, delimiter) {
|
|
688
|
+
const parts = [];
|
|
689
|
+
let depth = 0;
|
|
690
|
+
let start = 0;
|
|
691
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
692
|
+
const char = source[index];
|
|
693
|
+
if (char === "<" || char === "(" || char === "[" || char === "{") depth += 1;
|
|
694
|
+
else if (char === ">" || char === ")" || char === "]" || char === "}") depth -= 1;
|
|
695
|
+
else if (char === delimiter && depth === 0) {
|
|
696
|
+
parts.push(source.slice(start, index));
|
|
697
|
+
start = index + 1;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
parts.push(source.slice(start));
|
|
701
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Whether one declared parameter can be built from a JSON request body or
|
|
705
|
+
* query string.
|
|
706
|
+
*/
|
|
707
|
+
function classifyParameterWireability(parameter, options = {}) {
|
|
708
|
+
if (parameter.name.startsWith("...")) return {
|
|
709
|
+
wireable: false,
|
|
710
|
+
reason: `rest parameter \`${parameter.name}\` cannot be projected from a request body`
|
|
711
|
+
};
|
|
712
|
+
if (parameter.typeUnresolved) return {
|
|
713
|
+
wireable: false,
|
|
714
|
+
reason: `the declared type of \`${parameter.name}\` could not be resolved by the scanner`
|
|
715
|
+
};
|
|
716
|
+
const unionBranches = parameter.unionBranches;
|
|
717
|
+
if (unionBranches && unionBranches.length > 0) {
|
|
718
|
+
for (const branch of unionBranches) {
|
|
719
|
+
if ((branch.memberTypes ?? []).find((memberType) => !classifyTypeName(memberType, options, 1).wireable) !== void 0) continue;
|
|
720
|
+
if (classifyTypeName(branch.type, options, 0).wireable) return WIREABLE;
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
wireable: false,
|
|
724
|
+
reason: `parameter \`${parameter.name}\`: no branch of \`${parameter.type ?? "any"}\` can be built from a JSON request`
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
for (const memberType of parameter.memberTypes ?? []) {
|
|
728
|
+
const verdict = classifyTypeName(memberType, options, 1);
|
|
729
|
+
if (!verdict.wireable) return {
|
|
730
|
+
wireable: false,
|
|
731
|
+
reason: `\`${parameter.name}\` contains a member where ${verdict.reason}`
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const verdict = classifyTypeName(parameter.type ?? "any", options, 0);
|
|
735
|
+
if (verdict.wireable) return WIREABLE;
|
|
736
|
+
return {
|
|
737
|
+
wireable: false,
|
|
738
|
+
reason: `parameter \`${parameter.name}\`: ${verdict.reason}`
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Whether every declared parameter of a method can be built from a JSON
|
|
743
|
+
* request body or query string.
|
|
744
|
+
*
|
|
745
|
+
* A method with NO manifest parameter metadata is wire-able: that is the
|
|
746
|
+
* legacy options-bag contract every transport already supports, and absent
|
|
747
|
+
* metadata is not evidence of a hostile signature.
|
|
748
|
+
*/
|
|
749
|
+
function classifyMethodWireability(method, options = {}) {
|
|
750
|
+
for (const parameter of method.parameters ?? []) {
|
|
751
|
+
const verdict = classifyParameterWireability(parameter, options);
|
|
752
|
+
if (!verdict.wireable) return verdict;
|
|
753
|
+
}
|
|
754
|
+
return WIREABLE;
|
|
755
|
+
}
|
|
756
|
+
var EXPOSED = { exposed: true };
|
|
757
|
+
/**
|
|
758
|
+
* The single decision every generated-API consumer asks: is this method
|
|
759
|
+
* reachable as a custom REST action, and if not, why?
|
|
760
|
+
*
|
|
761
|
+
* ONE resolver, four consumers — both SvelteKit route emitters
|
|
762
|
+
* (`generateRoutesForObject`, `generateCollectionRoutesForObject`), the
|
|
763
|
+
* cli↔api coherence resolver (`resolveApiActionSet`), and the knowledge
|
|
764
|
+
* artifact's API projection. They previously each re-derived a subset: the
|
|
765
|
+
* emitters filtered on `shouldIncludeInApi` plus their own receiver skip,
|
|
766
|
+
* `resolveApiActionSet` mirrored both, and `knowledge.ts` mirrored the
|
|
767
|
+
* receiver half a third time. A gate added to only one of them would report a
|
|
768
|
+
* method as unavailable while still writing its route file, which is the exact
|
|
769
|
+
* incoherence this issue exists to close (#2686).
|
|
770
|
+
*
|
|
771
|
+
* Order matters, and is the tested precedence contract:
|
|
772
|
+
*
|
|
773
|
+
* 1. `api: false` — the class has no REST surface at all.
|
|
774
|
+
* 2. A CRUD verb — the generated operation already owns the name (#2646).
|
|
775
|
+
* 3. Non-public — never a surface.
|
|
776
|
+
* 4. A framework lifecycle method (`save`, `initialize`, `toJSON`, ...) — the
|
|
777
|
+
* mechanism behind generated CRUD, not a distinct operation, even when a
|
|
778
|
+
* subclass declares its own override. `CLIGenerator` and `MCPGenerator`
|
|
779
|
+
* already gate on this; REST did not, and this is where it joins them
|
|
780
|
+
* (#2638, #2657).
|
|
781
|
+
* 5. `api.exclude` — an explicit withdrawal.
|
|
782
|
+
* 6. `api.include` — an explicit allowlist boundary.
|
|
783
|
+
* 7. `@method({ expose: false })` — an explicit withdrawal that outranks every
|
|
784
|
+
* remaining rule, including a legacy `api.routes` entry for the same
|
|
785
|
+
* method. This is why the decorator is `@method()` and not `@action()`:
|
|
786
|
+
* declaring something an action in order to say it is not one contradicts
|
|
787
|
+
* itself.
|
|
788
|
+
* 8. Explicit legacy exposure — a name listed in `api.include` or carrying an
|
|
789
|
+
* `api.routes` entry is a DECLARATION that this method is a route, made
|
|
790
|
+
* before the heuristic existed. It bypasses the heuristic. This is the
|
|
791
|
+
* documented compatibility exception that makes "nothing breaks" true for
|
|
792
|
+
* the 42 existing route entries; without it, migrating a class to the new
|
|
793
|
+
* gate could silently drop a route its author had spelled out.
|
|
794
|
+
* 9. `@method({ expose: true })` — bypasses the heuristic, and NOTHING else.
|
|
795
|
+
* It cannot manufacture a receiver (step 10 still applies), reach a
|
|
796
|
+
* non-public method, or hydrate a parameter the transport cannot build.
|
|
797
|
+
* 10. Wire-ability — every parameter must be constructible from JSON.
|
|
798
|
+
* 11. Receiver — a collection class emits only collection-scoped routes, and a
|
|
799
|
+
* model class cannot host a collection-scoped instance method.
|
|
800
|
+
*/
|
|
801
|
+
function resolveApiMethodExposure(options) {
|
|
802
|
+
const { actionName, method, apiConfig, isCollectionClass = false } = options;
|
|
803
|
+
if (apiConfig === false) return {
|
|
804
|
+
exposed: false,
|
|
805
|
+
code: "api-disabled",
|
|
806
|
+
reason: "api is disabled"
|
|
807
|
+
};
|
|
808
|
+
if (isCrudOperation(actionName)) return {
|
|
809
|
+
exposed: false,
|
|
810
|
+
code: "crud-reserved",
|
|
811
|
+
reason: `\`${actionName}\` is reserved by the generated CRUD operation of the same name`
|
|
812
|
+
};
|
|
813
|
+
if (method.isPublic === false) return {
|
|
814
|
+
exposed: false,
|
|
815
|
+
code: "not-public",
|
|
816
|
+
reason: "not a public method"
|
|
817
|
+
};
|
|
818
|
+
if (isFrameworkLifecycleMethod(actionName)) return {
|
|
819
|
+
exposed: false,
|
|
820
|
+
code: "lifecycle-method",
|
|
821
|
+
reason: `\`${actionName}\` is a framework lifecycle method, not a distinct operation`
|
|
822
|
+
};
|
|
823
|
+
const config = getIncludeExclude(apiConfig);
|
|
824
|
+
if (config.exclude?.includes(actionName)) return {
|
|
825
|
+
exposed: false,
|
|
826
|
+
code: "excluded",
|
|
827
|
+
reason: "listed in api.exclude"
|
|
828
|
+
};
|
|
829
|
+
const includedExplicitly = config.include?.includes(actionName) === true;
|
|
830
|
+
if (config.include !== void 0 && !includedExplicitly) return {
|
|
831
|
+
exposed: false,
|
|
832
|
+
code: "not-included",
|
|
833
|
+
reason: "not listed in api.include"
|
|
834
|
+
};
|
|
835
|
+
const declared = readMethodDecoratorConfig(method);
|
|
836
|
+
if (declared?.expose === false) return {
|
|
837
|
+
exposed: false,
|
|
838
|
+
code: "withheld",
|
|
839
|
+
reason: declared.reason ?? "withheld by @method({ expose: false })"
|
|
840
|
+
};
|
|
841
|
+
const hasLegacyRoute = readApiRouteConfig(apiConfig, actionName) !== void 0;
|
|
842
|
+
if (!(declared?.expose === true || includedExplicitly || hasLegacyRoute)) {
|
|
843
|
+
const wireability = classifyMethodWireability(method, { ...options.isModelClassName ? { isModelClassName: options.isModelClassName } : {} });
|
|
844
|
+
if (!wireability.wireable) return {
|
|
845
|
+
exposed: false,
|
|
846
|
+
code: "not-wireable",
|
|
847
|
+
reason: `not routed: ${wireability.reason}`
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
const receiver = resolveActionReceiver(actionName, method, apiConfig, isCollectionClass);
|
|
851
|
+
if (!receiver.hosted) return {
|
|
852
|
+
exposed: false,
|
|
853
|
+
code: "no-receiver",
|
|
854
|
+
reason: receiver.reason
|
|
855
|
+
};
|
|
856
|
+
return EXPOSED;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Whether the resolved scope has an executable receiver on this host, matching
|
|
860
|
+
* both route emitters' own skips exactly.
|
|
861
|
+
*
|
|
862
|
+
* UNREACHABLE BY CONSTRUCTION, and deliberately kept:
|
|
863
|
+
* {@link resolveCustomActionMetadata} already collapses a contradicting
|
|
864
|
+
* declared scope back to the receiver-derived one, so the resolved scope always
|
|
865
|
+
* equals `defaultScope` and neither branch below can fire. That was equally
|
|
866
|
+
* true of the two `console.warn` skips in `generateRoutesForObject` /
|
|
867
|
+
* `generateCollectionRoutesForObject` that this replaced — moving them here
|
|
868
|
+
* changed nothing about when they fire, and dropping them would remove the only
|
|
869
|
+
* structural guard should that collapse ever be relaxed.
|
|
870
|
+
*
|
|
871
|
+
* The signal a developer actually sees for a contradicting declaration is
|
|
872
|
+
* {@link resolveDeclaredScopeMismatch}, reported by the emitters at build time.
|
|
873
|
+
*/
|
|
874
|
+
function resolveActionReceiver(actionName, method, apiConfig, isCollectionClass) {
|
|
875
|
+
const defaultScope = isCollectionClass ? "collection" : method.isStatic ? "collection" : "item";
|
|
876
|
+
let scope;
|
|
877
|
+
try {
|
|
878
|
+
scope = resolveCustomActionMetadata({
|
|
879
|
+
actionName,
|
|
880
|
+
method: {
|
|
881
|
+
...method.isStatic !== void 0 ? { isStatic: method.isStatic } : {},
|
|
882
|
+
...method.decoratorConfig ? { decoratorConfig: method.decoratorConfig } : {}
|
|
883
|
+
},
|
|
884
|
+
apiConfig,
|
|
885
|
+
defaultScope
|
|
886
|
+
}).scope;
|
|
887
|
+
} catch {
|
|
888
|
+
scope = defaultScope;
|
|
889
|
+
}
|
|
890
|
+
if (isCollectionClass) {
|
|
891
|
+
if (scope !== "collection") return {
|
|
892
|
+
hosted: false,
|
|
893
|
+
reason: "collection class methods only support collection-scoped API routes"
|
|
894
|
+
};
|
|
895
|
+
return { hosted: true };
|
|
896
|
+
}
|
|
897
|
+
if (scope === "collection" && method.isStatic !== true) return {
|
|
898
|
+
hosted: false,
|
|
899
|
+
reason: "collection API routes require a static method"
|
|
900
|
+
};
|
|
901
|
+
return { hosted: true };
|
|
902
|
+
}
|
|
903
|
+
function resolveEffectiveActionMetadata(options) {
|
|
904
|
+
const route = readApiRouteConfig(options.apiConfig, options.actionName);
|
|
905
|
+
const declared = readMethodDecoratorConfig(options.method);
|
|
906
|
+
const legacyDescription = readAiDescription(options.aiConfig, options.actionName);
|
|
907
|
+
const httpMethod = declared?.httpMethod ?? normalizeRouteHttpMethod(route?.method);
|
|
908
|
+
const path = declared?.path ?? (typeof route?.path === "string" ? route.path : void 0);
|
|
909
|
+
const scope = declared?.scope ?? normalizeRouteScope(route?.scope);
|
|
910
|
+
const effect = declared?.effect ?? normalizeRouteEffect(route?.effect);
|
|
911
|
+
const idempotent = declared?.idempotent ?? (typeof route?.idempotent === "boolean" ? route.idempotent : void 0);
|
|
912
|
+
const openWorld = declared?.openWorld ?? (typeof route?.openWorld === "boolean" ? route.openWorld : void 0);
|
|
913
|
+
const description = declared?.description ?? legacyDescription;
|
|
914
|
+
return {
|
|
915
|
+
...httpMethod ? { httpMethod } : {},
|
|
916
|
+
...path !== void 0 ? { path } : {},
|
|
917
|
+
...scope ? { scope } : {},
|
|
918
|
+
...effect ? { effect } : {},
|
|
919
|
+
...idempotent !== void 0 ? { idempotent } : {},
|
|
920
|
+
...openWorld !== void 0 ? { openWorld } : {},
|
|
921
|
+
...description !== void 0 ? { description } : {}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function normalizeRouteHttpMethod(value) {
|
|
925
|
+
return isApiHttpMethod(value) ? value : void 0;
|
|
926
|
+
}
|
|
927
|
+
function normalizeRouteScope(value) {
|
|
928
|
+
return value === "item" || value === "collection" ? value : void 0;
|
|
929
|
+
}
|
|
930
|
+
function normalizeRouteEffect(value) {
|
|
931
|
+
return value === "read" || value === "write" || value === "destructive" ? value : void 0;
|
|
932
|
+
}
|
|
933
|
+
function readAiDescription(aiConfig, actionName) {
|
|
934
|
+
if (!isRecord(aiConfig) || !isRecord(aiConfig.descriptions)) return void 0;
|
|
935
|
+
const description = aiConfig.descriptions[actionName];
|
|
936
|
+
return typeof description === "string" ? description : void 0;
|
|
937
|
+
}
|
|
938
|
+
function readApiRouteConfig(apiConfig, actionName) {
|
|
939
|
+
if (!isRecord(apiConfig) || !isRecord(apiConfig.routes)) return void 0;
|
|
940
|
+
const route = apiConfig.routes[actionName];
|
|
941
|
+
return isRecord(route) ? route : void 0;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Narrow a scanned transport config's `include`/`exclude`.
|
|
945
|
+
*
|
|
946
|
+
* A non-array value is treated as unset rather than throwing later on
|
|
947
|
+
* `.includes()` — the same defensive stance every other reader of scanned
|
|
948
|
+
* decorator config takes, because this data came from an AST, not a compiler.
|
|
949
|
+
*/
|
|
950
|
+
function getIncludeExclude(config) {
|
|
951
|
+
if (config === true || config === void 0 || !isRecord(config)) return {};
|
|
952
|
+
return {
|
|
953
|
+
...Array.isArray(config.include) ? { include: config.include } : {},
|
|
954
|
+
...Array.isArray(config.exclude) ? { exclude: config.exclude } : {}
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Whether a method's `@method()` declaration is also a RUNTIME REST route
|
|
959
|
+
* declaration, the way an `api.routes[m]` entry is.
|
|
960
|
+
*
|
|
961
|
+
* The runtime `APIGenerator` transport is deliberately declaration-gated: it
|
|
962
|
+
* serves a custom collection action only where one was declared, because its URL
|
|
963
|
+
* shape supports a single segment and an undeclared public method has never had
|
|
964
|
+
* a route there. `dispatchCustomCollectionAction` and the `isRestActionRoutable`
|
|
965
|
+
* preflight prediction must agree on that gate exactly, so both read this (#2686).
|
|
966
|
+
*
|
|
967
|
+
* True for any option that migrates from `ApiCustomRouteConfig` — its complete
|
|
968
|
+
* field set is `scope`, `method`, `path`, `effect`, `idempotent`, `openWorld` —
|
|
969
|
+
* because a legacy `routes: { m: { effect: 'write' } }` entry with no path or
|
|
970
|
+
* verb already dispatches at `POST /<collection>/m`, and migrating it onto the
|
|
971
|
+
* method must not silently delete that endpoint. Also true for an explicit
|
|
972
|
+
* `expose: true`, which is a stronger statement that the method is an action
|
|
973
|
+
* than an empty route entry is.
|
|
974
|
+
*
|
|
975
|
+
* FALSE for a bare `@method()` and for a `description`-only one. Neither
|
|
976
|
+
* migrates from a route entry — `description` migrates from `ai.descriptions`,
|
|
977
|
+
* and a bare decorator is a review marker — so counting them would hand the
|
|
978
|
+
* runtime transport endpoints it never served.
|
|
979
|
+
*/
|
|
980
|
+
function declaresRuntimeRestRoute(method) {
|
|
981
|
+
if (readMethodDecoratorConfig(method)?.expose === false) return false;
|
|
982
|
+
return declaresRuntimeRestRouteShape(method);
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Whether the author WROTE a runtime REST route declaration on this method,
|
|
986
|
+
* ignoring whether they then withheld it.
|
|
987
|
+
*
|
|
988
|
+
* Deliberately distinct from {@link declaresRuntimeRestRoute}: the dispatcher
|
|
989
|
+
* must still SEE a withheld declaration in order to refuse it explicitly. This
|
|
990
|
+
* router resolves `POST /<collection>/<segment>` to `create` when nothing
|
|
991
|
+
* claims the segment, so dropping a withheld action from the candidate set
|
|
992
|
+
* would turn a request aimed at an explicitly withheld operation into a silent
|
|
993
|
+
* row insert. The candidate set reads this; the preflight PREDICTION reads
|
|
994
|
+
* {@link declaresRuntimeRestRoute}, which adds the `expose: false` veto —
|
|
995
|
+
* "there is a declaration here" and "it is reachable" are different questions.
|
|
996
|
+
*/
|
|
997
|
+
function declaresRuntimeRestRouteShape(method) {
|
|
998
|
+
const declared = readMethodDecoratorConfig(method);
|
|
999
|
+
if (!declared) return false;
|
|
1000
|
+
return declared.httpMethod !== void 0 || declared.path !== void 0 || declared.scope !== void 0 || declared.effect !== void 0 || declared.idempotent !== void 0 || declared.openWorld !== void 0 || declared.expose !== void 0;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Coerce one transport-supplied argument into the runtime value the declared
|
|
1004
|
+
* parameter type needs.
|
|
1005
|
+
*
|
|
1006
|
+
* Today that means exactly one conversion: a `Date` parameter, which the
|
|
1007
|
+
* wire-ability heuristic accepts as JSON-shaped. JSON has no date type, so a
|
|
1008
|
+
* caller can only send an ISO string (or an epoch number) and the receiving
|
|
1009
|
+
* method — which calls `getTime()`, or hands the value to a query builder that
|
|
1010
|
+
* expects a `Date` — would otherwise get a string. Accepting `Date` as
|
|
1011
|
+
* wire-able and NOT hydrating it here would generate a route that 500s, so the
|
|
1012
|
+
* two are one decision (#2686).
|
|
1013
|
+
*
|
|
1014
|
+
* Deliberately narrow:
|
|
1015
|
+
* - Only a TOP-LEVEL declared parameter is converted. A `Date` nested inside a
|
|
1016
|
+
* named options bag is invisible to the manifest (the bag is accepted
|
|
1017
|
+
* heuristically, its members unresolved), so it is not hydrated and the
|
|
1018
|
+
* method must accept the string itself.
|
|
1019
|
+
* - An already-`Date` value, and anything that is not a string or finite
|
|
1020
|
+
* number, passes through untouched, so a runtime caller invoking the same
|
|
1021
|
+
* helper is never degraded.
|
|
1022
|
+
* - An unparseable string passes through as-is rather than becoming an
|
|
1023
|
+
* `Invalid Date`, leaving the method's own validation in charge of the error
|
|
1024
|
+
* message.
|
|
1025
|
+
*/
|
|
1026
|
+
function coerceCustomActionArgument(value, declaredType) {
|
|
1027
|
+
if (!declaredType || !declaredTypeAcceptsDate(declaredType)) return value;
|
|
1028
|
+
return toCustomActionDate(value);
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* The `Date` half of {@link coerceCustomActionArgument}, exported on its own
|
|
1032
|
+
* because generated SvelteKit route code calls it directly: the generator
|
|
1033
|
+
* already knows at build time which parameters are `Date`-typed, so the
|
|
1034
|
+
* emitted handler names the conversion rather than re-deriving it from a type
|
|
1035
|
+
* string at runtime. Both paths share this one implementation so the two
|
|
1036
|
+
* transports cannot drift.
|
|
1037
|
+
*/
|
|
1038
|
+
function toCustomActionDate(value) {
|
|
1039
|
+
if (value instanceof Date) return value;
|
|
1040
|
+
if (typeof value === "number" && Number.isFinite(value)) return new Date(value);
|
|
1041
|
+
if (typeof value !== "string" || value.trim() === "") return value;
|
|
1042
|
+
const parsed = new Date(value);
|
|
1043
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Decode a `number`-typed action argument that arrived over a QUERY STRING.
|
|
1047
|
+
*
|
|
1048
|
+
* A GET handler builds its options from `URLSearchParams`, so every value is a
|
|
1049
|
+
* string: `limit: number` reached the method as `'2'` and any arithmetic on it
|
|
1050
|
+
* silently produced string concatenation or `NaN` (#2686). A JSON body needs no
|
|
1051
|
+
* such repair, which is why this is emitted only on GET routes.
|
|
1052
|
+
*
|
|
1053
|
+
* Leaves anything it cannot decode alone, so a malformed value reaches the
|
|
1054
|
+
* method's own validation rather than becoming a silent `NaN`.
|
|
1055
|
+
*/
|
|
1056
|
+
function toCustomActionNumber(value) {
|
|
1057
|
+
if (typeof value === "number") return value;
|
|
1058
|
+
if (typeof value !== "string" || value.trim() === "") return value;
|
|
1059
|
+
const parsed = Number(value);
|
|
1060
|
+
return Number.isFinite(parsed) ? parsed : value;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* The `boolean` counterpart to {@link toCustomActionNumber}. A query string
|
|
1064
|
+
* carries `?active=false`, and the bare string `'false'` is TRUTHY — the most
|
|
1065
|
+
* dangerous of these coercions, since it inverts a guard rather than degrading
|
|
1066
|
+
* it. Only the four canonical spellings decode; anything else is left for the
|
|
1067
|
+
* method's own validation.
|
|
1068
|
+
*/
|
|
1069
|
+
function toCustomActionBoolean(value) {
|
|
1070
|
+
if (typeof value === "boolean") return value;
|
|
1071
|
+
if (typeof value !== "string") return value;
|
|
1072
|
+
const normalized = value.trim().toLowerCase();
|
|
1073
|
+
if (normalized === "true" || normalized === "1") return true;
|
|
1074
|
+
if (normalized === "false" || normalized === "0") return false;
|
|
1075
|
+
return value;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* The query-string decoder a GET route should apply to one parameter, or
|
|
1079
|
+
* `undefined` when the value passes through untouched.
|
|
1080
|
+
*
|
|
1081
|
+
* Mirrors {@link declaredTypeAcceptsDate}: a nullish branch does not change the
|
|
1082
|
+
* representation, but a genuine alternative (`number | string`) means the
|
|
1083
|
+
* method already accepts what the query string sends, so nothing is decoded.
|
|
1084
|
+
*/
|
|
1085
|
+
function queryStringDecoderFor(declaredType) {
|
|
1086
|
+
if (!declaredType) return void 0;
|
|
1087
|
+
if (declaredTypeAcceptsDate(declaredType)) return "toCustomActionDate";
|
|
1088
|
+
const branches = splitTopLevel(declaredType, "|").map((branch) => branch.trim()).filter((branch) => branch !== "null" && branch !== "undefined");
|
|
1089
|
+
if (branches.length === 0) return void 0;
|
|
1090
|
+
if (branches.every((branch) => branch === "number")) return "toCustomActionNumber";
|
|
1091
|
+
if (branches.every((branch) => branch === "boolean")) return "toCustomActionBoolean";
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* True when the declared type is a `Date` and nothing else.
|
|
1095
|
+
*
|
|
1096
|
+
* `Date | null` and `Date | undefined` qualify — a nullish branch is not an
|
|
1097
|
+
* alternative representation. `Date | string` deliberately does NOT: that
|
|
1098
|
+
* signature already accepts the string a JSON caller sends, so the method's
|
|
1099
|
+
* own handling is authoritative and converting behind its back would change
|
|
1100
|
+
* which branch it takes.
|
|
1101
|
+
*/
|
|
1102
|
+
function declaredTypeAcceptsDate(declaredType) {
|
|
1103
|
+
const branches = declaredType.split("|").map((branch) => branch.trim()).filter((branch) => branch !== "null" && branch !== "undefined");
|
|
1104
|
+
return branches.length > 0 && branches.every((branch) => branch === "Date");
|
|
1105
|
+
}
|
|
429
1106
|
//#endregion
|
|
430
|
-
export { CRUD_OPERATIONS, FRAMEWORK_LIFECYCLE_METHOD_NAMES, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInputSchema, buildCustomActionInvocationArgs, customActionParameterInputName, isCrudOperation, isCrudToolAction, isFrameworkLifecycleMethod, normalizeCustomActionFailure, resolveCustomActionMetadata, resolveCustomActionNames };
|
|
1107
|
+
export { CRUD_OPERATIONS, FRAMEWORK_LIFECYCLE_METHOD_NAMES, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInputSchema, buildCustomActionInvocationArgs, classifyMethodWireability, classifyParameterWireability, coerceCustomActionArgument, createClassNamePredicate, createManifestClassNamePredicate, customActionParameterInputName, declaredTypeAcceptsDate, declaresRuntimeRestRoute, declaresRuntimeRestRouteShape, isCrudOperation, isCrudToolAction, isFrameworkLifecycleMethod, normalizeCustomActionFailure, queryStringDecoderFor, readMethodDecoratorConfig, resolveApiMethodExposure, resolveCustomActionMetadata, resolveCustomActionNames, resolveDeclaredScopeMismatch, resolveEffectiveActionMetadata, toCustomActionBoolean, toCustomActionDate, toCustomActionNumber };
|
|
431
1108
|
|
|
432
1109
|
//# sourceMappingURL=custom-action.js.map
|