@wavemaker-ai/react-codegen 1.0.0-rc.647684 → 12.0.0-rc.335

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/command.js +52 -7
  2. package/command.js.map +1 -1
  3. package/dist/transpiler/index.d.mts +42 -1
  4. package/dist/transpiler/index.mjs +682 -90
  5. package/dist/transpiler/index.mjs.map +1 -1
  6. package/dist/transpiler/wm-styles.css +10 -20
  7. package/index.js +7 -0
  8. package/index.js.map +1 -1
  9. package/package-lock.json +102 -102
  10. package/package.json +1 -1
  11. package/src/app.generator.js +223 -35
  12. package/src/app.generator.js.map +1 -1
  13. package/src/handlebar-helpers.js +32 -0
  14. package/src/handlebar-helpers.js.map +1 -1
  15. package/src/increment-builder.js +32 -2
  16. package/src/increment-builder.js.map +1 -1
  17. package/src/profiles/profile.js +2 -0
  18. package/src/profiles/profile.js.map +1 -1
  19. package/src/profiles/web-preview.profile.js +4 -0
  20. package/src/profiles/web-preview.profile.js.map +1 -1
  21. package/src/services/designtime-collector.js +235 -0
  22. package/src/services/designtime-collector.js.map +1 -0
  23. package/src/services/doc-normalizer.js +185 -0
  24. package/src/services/doc-normalizer.js.map +1 -0
  25. package/src/services/method-emitter.js +367 -0
  26. package/src/services/method-emitter.js.map +1 -0
  27. package/src/services/model-emitter.js +281 -0
  28. package/src/services/model-emitter.js.map +1 -0
  29. package/src/services/schema-to-ts.js +50 -0
  30. package/src/services/schema-to-ts.js.map +1 -0
  31. package/src/services/service.generator.js +223 -0
  32. package/src/services/service.generator.js.map +1 -0
  33. package/src/transpile/bind.ex.transformer.js +4 -1
  34. package/src/transpile/bind.ex.transformer.js.map +1 -1
  35. package/src/transpile/components/container/accordion.transformer.js +4 -4
  36. package/src/transpile/components/container/accordion.transformer.js.map +1 -1
  37. package/src/transpile/components/container/tabs.transformer.js +4 -2
  38. package/src/transpile/components/container/tabs.transformer.js.map +1 -1
  39. package/src/transpile/components/data/list/list-transformer.js +7 -2
  40. package/src/transpile/components/data/list/list-transformer.js.map +1 -1
  41. package/src/transpile/components/data/table/table.transformer.js +14 -20
  42. package/src/transpile/components/data/table/table.transformer.js.map +1 -1
  43. package/src/transpile/components/data/table/utils.js +90 -0
  44. package/src/transpile/components/data/table/utils.js.map +1 -1
  45. package/src/transpile/components/dialogs/dialog-actions.transformer.js +4 -6
  46. package/src/transpile/components/dialogs/dialog-actions.transformer.js.map +1 -1
  47. package/src/transpile/components/dialogs/dialog.transformer.js +14 -3
  48. package/src/transpile/components/dialogs/dialog.transformer.js.map +1 -1
  49. package/src/transpile/components/layout/layout-region-factory.js +8 -2
  50. package/src/transpile/components/layout/layout-region-factory.js.map +1 -1
  51. package/src/transpile/components/nav/nav.transformer.js +3 -5
  52. package/src/transpile/components/nav/nav.transformer.js.map +1 -1
  53. package/src/transpile/components/utils.js +11 -5
  54. package/src/transpile/components/utils.js.map +1 -1
  55. package/src/transpile/transpile.js +62 -21
  56. package/src/transpile/transpile.js.map +1 -1
  57. package/src/transpile/variables-template-source.js +2 -2
  58. package/src/transpile/variables-template-source.js.map +1 -1
  59. package/src/utils.browser.js +51 -6
  60. package/src/utils.browser.js.map +1 -1
  61. package/src/variables/variable.transformer.js +623 -12
  62. package/src/variables/variable.transformer.js.map +1 -1
  63. package/templates/project/app/client.layout.tsx +7 -7
  64. package/templates/project/app/components.css +10 -0
  65. package/templates/project/app/widgetInlineStylesOverride.css +0 -20
  66. package/templates/project/package.json +6 -5
  67. package/templates/project/services/base.service.ts +361 -0
  68. package/templates/service/service-class.hbs +19 -0
  69. package/templates/variables.template +67 -1
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const bind_ex_transformer_1 = __importDefault(require("../transpile/bind.ex.transformer"));
7
+ const schema_to_ts_1 = require("../services/schema-to-ts");
8
+ const doc_normalizer_1 = require("../services/doc-normalizer");
7
9
  const lodash_1 = require("lodash");
8
10
  function bindingValueIsStatic(value) {
9
11
  const v = Array.isArray(value) ? value[0] : value;
@@ -40,6 +42,42 @@ function isStaticParamsVariable(variable) {
40
42
  }
41
43
  return filterExpressionsAreStatic(variable.filterExpressions);
42
44
  }
45
+ // Converts plural entity name to singular: Countries→Country, Addresses→Address, Users→User
46
+ function simpleSingularize(plural) {
47
+ const lower = plural.toLowerCase();
48
+ if (lower.endsWith("ies"))
49
+ return plural.substring(0, plural.length - 3) + "y";
50
+ if (lower.endsWith("es"))
51
+ return plural.substring(0, plural.length - 2);
52
+ if (lower.endsWith("s"))
53
+ return plural.substring(0, plural.length - 1);
54
+ return plural;
55
+ }
56
+ // Converts singular entity name to plural following English morphology rules:
57
+ // Employee→Employees, Country→Countries, Address→Addresses, Church→Churches, Day→Days
58
+ function simplePluralize(singular) {
59
+ if (!singular)
60
+ return singular;
61
+ const last = singular[singular.length - 1].toLowerCase();
62
+ const prev = singular.length > 1 ? singular[singular.length - 2].toLowerCase() : null;
63
+ const vowels = "aeiouy";
64
+ if (last === "x" || last === "s") {
65
+ return singular + "es";
66
+ }
67
+ else if (last === "y") {
68
+ return prev !== null && vowels.includes(prev)
69
+ ? singular + "s" // Day → Days
70
+ : singular.slice(0, -1) + "ies"; // City → Cities
71
+ }
72
+ else if (last === "h") {
73
+ return prev !== null && (prev === "c" || prev === "s")
74
+ ? singular + "es" // Church→Churches, Dish→Dishes
75
+ : singular + "s";
76
+ }
77
+ else {
78
+ return singular + "s";
79
+ }
80
+ }
43
81
  function parseStaticJsonValue(value) {
44
82
  const trimmedValue = value.trim();
45
83
  if (!trimmedValue.startsWith("{") && !trimmedValue.startsWith("[")) {
@@ -551,9 +589,350 @@ function transformModelVariable(variable, scope) {
551
589
  tv.isList = !!variable.isList;
552
590
  return tv;
553
591
  }
554
- function transformServiceVariable(variable, scope) {
592
+ const capitalizeFirstLetter = (str) => {
593
+ return str.charAt(0).toUpperCase() + str.slice(1);
594
+ };
595
+ /** Matches the singleton export name emitted by service.generator.ts for a given service class. */
596
+ const getServiceInstanceName = (className) => className.charAt(0).toLowerCase() + className.slice(1);
597
+ /**
598
+ * The name a generated `*.variables.ts` uses to refer to a service singleton.
599
+ *
600
+ * The singleton a service file exports is named after its own class (e.g. `departmentService` in
601
+ * `services/hrdb/DepartmentService.ts`) — the folder, not the name, says which backend service it
602
+ * belongs to. But one generated variables file can import singletons from SEVERAL services at
603
+ * once, and two services that share an entity name (an "Employee" in both hrdb and salesdb) would
604
+ * then export identically-named singletons. So the local name is qualified with the owning
605
+ * service id (`hrdbEmployeeService` / `salesdbEmployeeService`) and the import is aliased to it
606
+ * (see templates/variables.template) — keeping the generated file's own export clean while every
607
+ * import site stays unambiguous.
608
+ */
609
+ function getServiceLocalName(serviceId, className) {
610
+ const instanceName = getServiceInstanceName(className);
611
+ if (!serviceId) {
612
+ return instanceName;
613
+ }
614
+ const prefix = (0, schema_to_ts_1.toPascalCase)(serviceId);
615
+ const core = prefix.endsWith("Service") ? prefix.slice(0, -"Service".length) : prefix;
616
+ // Already service-named (e.g. class SecurityService in service securityService) — the
617
+ // qualified form would just double the word.
618
+ if (className.startsWith(core)) {
619
+ return instanceName;
620
+ }
621
+ return getServiceInstanceName(`${core}${className}`);
622
+ }
623
+ /**
624
+ * Derives service class name from controller. Strip "Controller", separators, ensure PascalCase.
625
+ * Matches generated API files (apiNameSuffix=Service).
626
+ */
627
+ function getServiceClassName(prefix) {
628
+ if (!prefix || typeof prefix !== "string")
629
+ return "Service";
630
+ // Strip separators including underscore — OpenAPI generator also removes underscores
631
+ // from tag names when computing class names (e.g. "Master_LOV" → "MasterLOV")
632
+ const base = prefix.replace(/[\s/\\\-_]+/g, "").trim();
633
+ if (!base)
634
+ return "Service";
635
+ let basePascal = capitalizeFirstLetter(base);
636
+ if (basePascal.endsWith("Controller")) {
637
+ basePascal = basePascal.slice(0, -"Controller".length);
638
+ }
639
+ if (!basePascal)
640
+ return "Service";
641
+ return basePascal.endsWith("Service") ? basePascal : basePascal + "Service";
642
+ }
643
+ /**
644
+ * Some operationIds are free text with spaces or other separators
645
+ * (e.g. "update Product Offer category", "create/update bulk rules") instead
646
+ * of a valid identifier. camelCase them to match the generated API method name
647
+ * (e.g. "updateProductOfferCategory"). Mirrors toCamelCaseIdentifier in
648
+ * scripts/swagger-builder.js, which sanitizes the same values before codegen.
649
+ */
650
+ function toCamelCaseIdentifier(name) {
651
+ if (typeof name !== "string" || !/[^a-zA-Z0-9]/.test(name))
652
+ return name;
653
+ const words = name.split(/[^a-zA-Z0-9]+/).filter(Boolean);
654
+ if (!words.length)
655
+ return name;
656
+ let result = words
657
+ .map((word, i) => (i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)))
658
+ .join("");
659
+ if (/^[0-9]/.test(result))
660
+ result = "_" + result;
661
+ return result;
662
+ }
663
+ /**
664
+ * Mirrors doc-normalizer.ts's hasMultipleDistinctTags()-gated OpenAPIService behavior: a
665
+ * connector whose spec never tagged operations meaningfully (every operation shares one tag,
666
+ * often the generic placeholder "default") gets lumped into one class named after the service
667
+ * itself; a connector with real distinct per-entity tags (e.g. "Department"/"Employee") splits
668
+ * into one class per entity, same as any other service type. `variable.controller` already
669
+ * carries that same signal here — WM sets it to the literal "default" placeholder for the
670
+ * lumped case (mirroring the doc's own generic tag) and to the real entity name otherwise — so
671
+ * this can be decided without needing the underlying designtime doc at variable-transform time.
672
+ */
673
+ function resolveServiceTag(serviceId, controller, serviceType) {
674
+ const isLumpedOpenApiService = serviceType === "OpenAPIService" && (!controller || controller.toLowerCase() === "default");
675
+ const base = isLumpedOpenApiService
676
+ ? serviceId.endsWith("Service")
677
+ ? serviceId.slice(0, -"Service".length)
678
+ : serviceId
679
+ : controller || serviceId;
680
+ // Real designtime tags (mirrored onto variable.controller) are often not valid identifiers
681
+ // as-is (e.g. "Manage - Category", "Query - Entity Lock" — spaces and hyphens straight from a
682
+ // human-authored Swagger tag). A bare capitalize-first-letter leaves those characters in
683
+ // place, producing an invalid JS/TS class/instance name for invokeHttp to call into. Must use
684
+ // the exact same toPascalCase() doc-normalizer.ts uses when it computes the SAME tag on the
685
+ // generation side, or the two computations disagree on the class name.
686
+ return base ? (0, schema_to_ts_1.toPascalCase)(base) : base;
687
+ }
688
+ /**
689
+ * Returns serviceFile and invokeFnName for both ServiceVariable and LiveVariable.
690
+ * - ServiceVariable: uses controller + operation (e.g. EmployeeService.countEmployees)
691
+ * - LiveVariable: uses type + operation mapping (read→filterDepartments, create→createDepartment, etc.)
692
+ */
693
+ function formatServiceDetails(variable) {
694
+ if (variable.operationId) {
695
+ // Use variable.service as the authoritative service identifier so that service names
696
+ // containing underscores (e.g. "Master_LOV") are handled correctly.
697
+ // Fallback: split operationId on first "_" (legacy behaviour for variables without service field).
698
+ const serviceName = variable.service;
699
+ let prefix;
700
+ let invokeFnName;
701
+ if (serviceName) {
702
+ prefix = serviceName;
703
+ // Always rederive via the EXACT same deriveMethodName() doc-normalizer.ts uses on the
704
+ // generation side, with the same candidates (service id, and the operation's own
705
+ // controller/tag in both its raw "<Controller>Controller" and WM-cleaned forms) — this
706
+ // can never drift from what was actually generated, unlike trusting variable.operation
707
+ // (WM's own bare-action-name guess) or hand-rolling a narrower prefix-stripping rule here:
708
+ // - JavaService/SoapService operationIds carry a "<Controller>_" tag layer that must be
709
+ // fully stripped (e.g. "GetLoggedInUserController_getUser" -> "getUser").
710
+ // - OpenAPIService operationIds can carry BOTH a service-id AND controller/tag layer that
711
+ // must both be stripped (e.g. "hrdbSwagger_DepartmentController_getDepartment" ->
712
+ // "getDepartment").
713
+ // - RestService single-operation connectors bake the service's own id in as their tag —
714
+ // deriveMethodName() only collapses a DOUBLED prefix to one copy rather than stripping
715
+ // it away (e.g. "swagger_swagger_invoke" -> "swaggerInvoke"), so two different
716
+ // connectors don't collide on one generic action name; a genuinely single-prefixed one
717
+ // (e.g. "CreateContractSpecification_invoke") still strips fully, to "invoke".
718
+ const controllerRaw = variable.controller ? `${variable.controller}Controller` : undefined;
719
+ const prefixCandidates = [serviceName, variable.controller, controllerRaw].filter(Boolean);
720
+ invokeFnName = (0, doc_normalizer_1.deriveMethodName)(variable.operationId, prefixCandidates);
721
+ }
722
+ else {
723
+ const sepIdx = variable.operationId.indexOf("_");
724
+ prefix = sepIdx >= 0 ? variable.operationId.substring(0, sepIdx) : variable.operationId;
725
+ invokeFnName =
726
+ sepIdx >= 0 ? variable.operationId.substring(sepIdx + 1) : variable.operationId;
727
+ }
728
+ // Some operationIds are free text with spaces or other separators
729
+ // (e.g. "update Product Offer category", "create/update bulk rules"),
730
+ // which isn't a valid JS identifier as-is. The generated API method name
731
+ // camelCases these (e.g. "updateProductOfferCategory"), so do the same
732
+ // here to keep the invoked method name in sync.
733
+ invokeFnName = toCamelCaseIdentifier(invokeFnName);
734
+ // variable.service is the backend service id (e.g. "hrdb", "hrdbSwagger") — the same value
735
+ // service.generator.ts's collected.serviceName carries. Prefer the (service, controller)
736
+ // pair when available (reliable, WM-Studio-provided, and required so two different backend
737
+ // services sharing a controller/entity name don't collide on one class) — fall back to the
738
+ // legacy operationId-derived `prefix` only for older variables with no `service` field.
739
+ const formattedServiceName = variable.service
740
+ ? (0, schema_to_ts_1.buildServiceClassName)(variable.service, resolveServiceTag(variable.service, variable.controller, variable.serviceType))
741
+ : getServiceClassName(prefix);
742
+ return {
743
+ formattedServiceName,
744
+ invokeFnName,
745
+ };
746
+ }
747
+ // LiveVariable: has type and operation, maps to API function names
748
+ if (variable.type && variable.operation) {
749
+ const entity = variable.type;
750
+ // Normalize to singular first (entity may arrive as singular or plural from WaveMaker).
751
+ // Then re-pluralize for read so the method name is always the correct plural form.
752
+ // e.g. "Employee" → singular="Employee" → plural="Employees" → filterEmployees
753
+ // e.g. "Addresses" → singular="Address" → plural="Addresses" → filterAddresses
754
+ const singular = simpleSingularize(entity);
755
+ const plural = simplePluralize(singular);
756
+ const operationMap = {
757
+ read: `filter${plural}`,
758
+ insert: `create${singular}`,
759
+ update: `edit${singular}`,
760
+ delete: `delete${singular}`,
761
+ };
762
+ const invokeFnName = operationMap[variable.operation];
763
+ if (!invokeFnName)
764
+ return null;
765
+ // variable.liveSource is the owning DataService's id (e.g. "hrdb", "salesdb") — same
766
+ // rationale as ServiceVariable above: prefix so two DataServices sharing an entity name
767
+ // (e.g. both have "Employee") don't collide on one generated class.
768
+ const formattedServiceName = variable.liveSource
769
+ ? (0, schema_to_ts_1.buildServiceClassName)(variable.liveSource, capitalizeFirstLetter(entity))
770
+ : capitalizeFirstLetter(entity + "Service");
771
+ return {
772
+ formattedServiceName,
773
+ invokeFnName,
774
+ };
775
+ }
776
+ return null;
777
+ }
778
+ /**
779
+ * A variable's backing service is only real if it was actually generated into
780
+ * services/apis/*.ts. validServiceClassNames is undefined when that hasn't been
781
+ * checked (e.g. direct unit tests) — in that case, skip validation entirely.
782
+ */
783
+ function isServiceAvailable(className, validServiceClassNames) {
784
+ return !validServiceClassNames || !className || validServiceClassNames.has(className);
785
+ }
786
+ /**
787
+ * The key an argument's value actually arrives under in the params object the runtime hands
788
+ * `invokeHttp`.
789
+ *
790
+ * For a ServiceVariable that's the swagger parameter's own name — `inputFields` is keyed by
791
+ * exactly those. A LiveVariable never populates `inputFields` with its DB operation's fields:
792
+ * LiveVariableManager builds its own `dbOperationOptions` and that object is what reaches
793
+ * `invokeHttp`, keyed by the runtime's OWN names (see VARIABLE_URLS.DATABASE in
794
+ * @wavemaker-ai/variables' variables.constants.ts and getEntityData()'s dbOperationOptions):
795
+ * - `page` / `size` / `sort` — same names, handled by the pageable branch above.
796
+ * - `data` — the request payload. For a filter/search read this is the query fragment the
797
+ * manager pre-builds as `"q=<query>"` (posted form-urlencoded, matching the backend's
798
+ * `consumes = "application/x-www-form-urlencoded"`); for insert/update it's the entity.
799
+ * Either way it is NEVER keyed by the swagger name (`q`, or the entity name for a body), so
800
+ * reading the swagger name here would silently always yield `undefined` — dropping every
801
+ * filter on a LiveVariable read.
802
+ * - `id` — the primary key for update/delete by id.
803
+ */
804
+ /**
805
+ * The expression the invokeHttp glue uses to source one positional argument from the runtime's
806
+ * flat `params` object. `page`/`size`/`sort` collapse into one `Pageable` literal; everything else
807
+ * reads its key. Values pass through as they arrive — base.service.ts's serializers strip any
808
+ * `<name>=` prefix @wavemaker-ai/variables baked in, since they already hold the wire name.
809
+ */
810
+ function callArgExpression(arg, isLiveVariable, isBodyTypeQueryOrProcedure = false) {
811
+ // A query/procedure variable (controller QueryExecution|ProcedureExecution + post|put) does not
812
+ // put its body under the swagger param's name: ServiceVariableUtils calls processRequestBody(),
813
+ // which assembles the body from that param's CHILD fields — so inputFields is FLAT
814
+ // ({USERNAME, PASSWORD, ...}) with no `CreateUserRequest` key. Reading the swagger name there
815
+ // yields undefined and the request goes out with no body.
816
+ if (isBodyTypeQueryOrProcedure && arg.source.kind === "body") {
817
+ return "params";
818
+ }
819
+ if (arg.source.kind === "pageable") {
820
+ const props = arg.source.paramNames
821
+ .map(wireName => {
822
+ const prop = wireName === "sort" ? "sort" : wireName === "size" ? "pageSize" : "pageNumber";
823
+ return `${prop}: params?.[${JSON.stringify(wireName)}]`;
824
+ })
825
+ .join(", ");
826
+ return `{ ${props} }`;
827
+ }
828
+ return `params?.[${JSON.stringify(liveVariableParamKey(arg, isLiveVariable))}]`;
829
+ }
830
+ /**
831
+ * Sets the fields templates/variables.template's `invokeHttp` glue needs to call a generated
832
+ * service method whose signature mirrors the backend operation's own declared parameters (see
833
+ * src/services/method-emitter.ts): one positional argument per parameter, in declared order.
834
+ *
835
+ * `tv.callArgs` is a list of ready-to-emit JS expressions, each sourcing one argument from the
836
+ * flat params object the WM runtime hands `invokeHttp` — including reconstructing the single
837
+ * `pageable` object the backend binds page/size/sort into. `tv.serviceImportPath` is where the
838
+ * owning singleton lives, now that each service has its own folder and a class name alone no
839
+ * longer determines its path.
840
+ *
841
+ * Both come from the tables the generator itself wrote while emitting these exact methods, so a
842
+ * call site can never name an argument the generated method doesn't have. When the lookup isn't
843
+ * available (service generation didn't run — e.g. direct unit tests) `callArgs` stays undefined
844
+ * and the template falls back to passing the params object straight through.
845
+ */
846
+ function applyServiceCallShape(tv, lookupKey, methodArgsByOperation, servicePathByOperation, isLiveVariable = false, isBodyTypeQueryOrProcedure = false) {
847
+ var _a;
848
+ const importPath = servicePathByOperation === null || servicePathByOperation === void 0 ? void 0 : servicePathByOperation.get(lookupKey);
849
+ if (importPath) {
850
+ tv.serviceImportPath = importPath;
851
+ // The path comes from the generator itself, so it is authoritative about WHICH class owns this
852
+ // operation — more so than the variable's own `controller`, which can be stale (a real case:
853
+ // a variable claims `controller: "Manage - Category"` for an operation the designtime doc
854
+ // actually tags "Manage - Product Offer Group Category"). Re-derive the class and its exported
855
+ // singleton from the resolved path so the import names always match the file being imported;
856
+ // otherwise we emit `import { manageCategoryService } from '.../ManageProductOfferGroupCategoryService'`
857
+ // — a module that has no such export.
858
+ const resolvedClassName = importPath.split("/").pop();
859
+ if (resolvedClassName && resolvedClassName !== tv.serviceFileClass) {
860
+ tv.serviceFile = resolvedClassName;
861
+ tv.serviceFileClass = resolvedClassName;
862
+ tv.serviceExportName = getServiceInstanceName(resolvedClassName);
863
+ tv.serviceInstanceName = getServiceLocalName((_a = tv.service) !== null && _a !== void 0 ? _a : tv.liveSource, resolvedClassName);
864
+ }
865
+ }
866
+ const args = methodArgsByOperation === null || methodArgsByOperation === void 0 ? void 0 : methodArgsByOperation.get(lookupKey);
867
+ if (!args) {
868
+ return;
869
+ }
870
+ // Handlebars treats an EMPTY array as falsy, so `{{#if callArgs}}` cannot distinguish "this
871
+ // operation takes no arguments" from "no lookup was available". A zero-arg method (e.g.
872
+ // `sample(ctx?)`) then fell through to the `(params, ctx)` fallback and was called with two
873
+ // arguments — a compile error. This flag records that the args ARE known, whatever their count.
874
+ tv.hasResolvedCallArgs = true;
875
+ // page/size/sort arrive under those exact names for BOTH variable kinds — LiveVariable's
876
+ // manager builds them into its dbOperationOptions, and a ServiceVariable's inputFields keys
877
+ // them by the swagger param names, which are also page/size/sort.
878
+ tv.callArgs = args.map(arg => callArgExpression(arg, isLiveVariable, isBodyTypeQueryOrProcedure));
879
+ }
880
+ function liveVariableParamKey(arg, isLiveVariable) {
881
+ const swaggerName = arg.source.paramName;
882
+ if (!isLiveVariable) {
883
+ return swaggerName;
884
+ }
885
+ switch (arg.source.kind) {
886
+ case "query":
887
+ // The only query param a LiveVariable operation declares besides the pageable trio is the
888
+ // filter query (`q`), which the manager delivers in `data`, already "q="-prefixed and
889
+ // unwrapped back to a bare value by the runtime's normalizeInvokeHttpParams().
890
+ return swaggerName === "q" ? "data" : swaggerName;
891
+ case "body":
892
+ case "formData":
893
+ return "data";
894
+ case "path":
895
+ // A LiveVariable's by-id operations receive the key as `id`, whatever the entity calls its
896
+ // primary key column (deptId, empId, ...).
897
+ return "id";
898
+ default:
899
+ return swaggerName;
900
+ }
901
+ }
902
+ function transformServiceVariable(variable, scope, validServiceClassNames, methodArgsByOperation, methodNameByOperation, servicePathByOperation) {
903
+ var _a;
904
+ // A variable whose service was never generated (no swagger definition, service removed, ...)
905
+ // is still emitted — just without an invokeHttp. useHttp's send() falls back to its own axios
906
+ // instance when `variable.invokeHttp == undefined`, so the call still goes out the pre-codegen
907
+ // way instead of the variable disappearing from the page.
908
+ const serviceDetails = formatServiceDetails(variable);
909
+ let skipInvokeHttp = false;
910
+ if (!isServiceAvailable(serviceDetails === null || serviceDetails === void 0 ? void 0 : serviceDetails.formattedServiceName, validServiceClassNames)) {
911
+ console.warn(`ServiceVariable "${variable.name}" references service "${variable.service}" ` +
912
+ `(${serviceDetails === null || serviceDetails === void 0 ? void 0 : serviceDetails.formattedServiceName}), which was not generated. Emitting the ` +
913
+ `variable without an invokeHttp — the runtime falls back to its own transport.`);
914
+ skipInvokeHttp = true;
915
+ }
916
+ // The service CLASS existing isn't enough — the specific OPERATION must exist too. A project's
917
+ // variable JSON can name an operationId the designtime doc no longer exposes (a real case: a
918
+ // variable kept an operationId for an endpoint since removed from the backend, while its
919
+ // service and controller are still perfectly valid). Generating for it produced a call to a
920
+ // method that doesn't exist AND, because the per-operation lookup missed, an import path with
921
+ // the service folder blank (`@/services//XService`) — a build break rather than a clear signal.
922
+ // When the lookup tables are available (they aren't in direct unit tests) and this operation
923
+ // isn't in them, treat it exactly like a missing service and skip the variable.
924
+ const operationKey = `${variable.service}::${variable.operationId}`;
925
+ if (servicePathByOperation && !servicePathByOperation.has(operationKey)) {
926
+ console.warn(`ServiceVariable "${variable.name}" references operation "${variable.operationId}" on ` +
927
+ `service "${variable.service}", which has no such operation. Emitting the variable ` +
928
+ `without an invokeHttp — the runtime falls back to its own transport.`);
929
+ skipInvokeHttp = true;
930
+ }
555
931
  const tv = transformVariable(variable, scope);
556
932
  tv.classname = "ServiceVariable";
933
+ // Gates the service import AND the invokeHttp block in the template: with no
934
+ // generated method to call, emitting either would not compile.
935
+ tv.hasInvokeHttp = !skipInvokeHttp;
557
936
  tv.operationId = variable.operationId;
558
937
  tv.isList = !!variable.isList;
559
938
  tv.inFlightBehavior = variable.inFlightBehavior;
@@ -567,6 +946,30 @@ function transformServiceVariable(variable, scope) {
567
946
  tv.autoUpdate = variable.autoUpdate;
568
947
  tv.orderBy = variable.orderBy;
569
948
  tv.isStaticParams = isStaticParamsVariable(variable);
949
+ tv.serviceFile = serviceDetails === null || serviceDetails === void 0 ? void 0 : serviceDetails.formattedServiceName;
950
+ tv.serviceFileClass = serviceDetails === null || serviceDetails === void 0 ? void 0 : serviceDetails.formattedServiceName;
951
+ // `serviceInstanceName` is the name used at the call site (and aliased to on import);
952
+ // `serviceExportName` is what the service file actually exports.
953
+ tv.serviceExportName =
954
+ serviceDetails && getServiceInstanceName(serviceDetails.formattedServiceName);
955
+ tv.serviceInstanceName =
956
+ serviceDetails && getServiceLocalName(variable.service, serviceDetails.formattedServiceName);
957
+ // Prefer the actual generated method name (looked up by the exact same key
958
+ // service.generator.ts built it with) over re-deriving it from variable.operationId via
959
+ // formatServiceDetails/deriveMethodName: WM doesn't always record a variable's operationId as
960
+ // the designtime doc's raw operationId verbatim (see NormalizedOperation.operationIdAliases),
961
+ // so blindly re-running the same stripping rules on a shorter/differently-prefixed alias can
962
+ // land on a name the generated class doesn't actually have (e.g. a doc-side "doubled prefix"
963
+ // operationId that collapses to "fooBar" on the generation side, but whose variable-side alias
964
+ // has only one prefix layer and over-strips down to bare "bar"). Falls back to the re-derived
965
+ // name only when the lookup can't help (service generation didn't run, e.g. direct unit tests).
966
+ const lookupKey = `${variable.service}::${variable.operationId}`;
967
+ tv.invokeFnName = (_a = methodNameByOperation === null || methodNameByOperation === void 0 ? void 0 : methodNameByOperation.get(lookupKey)) !== null && _a !== void 0 ? _a : serviceDetails === null || serviceDetails === void 0 ? void 0 : serviceDetails.invokeFnName;
968
+ // Mirrors ServiceVariableUtils' isBodyTypeQueryOrProcedure: a query/procedure variable's body
969
+ // is assembled from the body param's child fields, so its params object is flat.
970
+ const isBodyTypeQueryOrProcedure = ["QueryExecution", "ProcedureExecution"].includes(variable.controller) &&
971
+ ["put", "post"].includes(variable.operationType);
972
+ applyServiceCallShape(tv, lookupKey, methodArgsByOperation, servicePathByOperation, false, isBodyTypeQueryOrProcedure);
570
973
  return tv;
571
974
  }
572
975
  function transformNavigationAction(variable, scope) {
@@ -596,11 +999,13 @@ function transformNotificationAction(variable, scope, imports) {
596
999
  tv.classname = "NotificationAction";
597
1000
  tv.group = "action";
598
1001
  tv.operation = variable.operation;
599
- const partialContent = Array.isArray(variable.dataBinding)
600
- ? variable.dataBinding.filter((binding) => {
601
- return binding.target === "page";
602
- })[0]
603
- : null;
1002
+ const bindings = Array.isArray(variable.dataBinding) ? variable.dataBinding : [];
1003
+ const getBinding = (target) => bindings.find((b) => b.target === target);
1004
+ const contentBinding = getBinding("content");
1005
+ const rendersPartial = contentBinding
1006
+ ? contentBinding.value === "page"
1007
+ : variable.operation !== "toast";
1008
+ const partialContent = rendersPartial ? getBinding("page") : null;
604
1009
  if (variable.onOk || variable.onClick) {
605
1010
  tv.onOk = appendIfNotEmpty((0, bind_ex_transformer_1.default)(variable.onOk || variable.onClick, scope, "event"), ";");
606
1011
  }
@@ -658,9 +1063,91 @@ function transformLogoutVariable(variable, scope) {
658
1063
  tv.redirectTo = variable.redirectTo;
659
1064
  return tv;
660
1065
  }
661
- function transformLiveVariable(variable, scope) {
1066
+ /**
1067
+ * The `${serviceName}::${operationIdAlias}` key of the operation whose GENERATED method is named
1068
+ * `methodName` on service `serviceName`. LiveVariables identify their operation by that derived
1069
+ * method name rather than by a doc operationId (they have none), so this reverses the
1070
+ * methodNameByOperation table to reach the other per-operation lookups keyed the same way.
1071
+ */
1072
+ function findOperationKeyByMethodName(serviceName, methodName, methodNameByOperation) {
1073
+ if (!serviceName || !methodName || !methodNameByOperation) {
1074
+ return undefined;
1075
+ }
1076
+ const prefix = `${serviceName}::`;
1077
+ for (const [key, name] of methodNameByOperation) {
1078
+ if (name === methodName && key.startsWith(prefix)) {
1079
+ return key;
1080
+ }
1081
+ }
1082
+ return undefined;
1083
+ }
1084
+ /**
1085
+ * Fallback for when findOperationKeyByMethodName's guessed name (built via
1086
+ * simpleSingularize/simplePluralize) doesn't match anything real: those are an English-stemming
1087
+ * HEURISTIC, and WM entity names aren't always actual English plurals to begin with — an entity
1088
+ * literally named "AllTypes" has no distinct singular in WM's own generated methods at all
1089
+ * (`findAllTypes`, `createAllTypes`, `filterAllTypes`, `deleteAllTypes` all keep "AllTypes"
1090
+ * unchanged), so the stemmer's round-trip ("AllTypes" -> "AllTyp" -> "AllTyps") produces a name
1091
+ * nothing was ever generated as, and the reverse lookup silently misses.
1092
+ *
1093
+ * WM's own raw operationId for a Data Service entity operation is always
1094
+ * `<entity>Controller_<verb><NameVariant>` — the verb always comes right after the tag,
1095
+ * regardless of what spelling variant WM chose for NameVariant. Searching by that verb prefix
1096
+ * against the RAW (un-derived) key finds the real operation without having to guess the spelling
1097
+ * variant at all. Among ties (e.g. an association method like `findAssociatedEmployees` also
1098
+ * starting with "find"), the shortest key wins — the plain CRUD operation, not one with an extra
1099
+ * qualifier tacked on.
1100
+ */
1101
+ function findLiveOperationKeyByVerb(liveSource, entity, verbTokens, methodNameByOperation) {
1102
+ if (!liveSource || !entity || !methodNameByOperation) {
1103
+ return undefined;
1104
+ }
1105
+ const tagPrefix = `${liveSource}::${entity}Controller_`;
1106
+ let best;
1107
+ for (const key of methodNameByOperation.keys()) {
1108
+ if (!key.startsWith(tagPrefix))
1109
+ continue;
1110
+ const verbAndName = key.slice(tagPrefix.length);
1111
+ if (!verbTokens.some(verb => verbAndName.startsWith(verb)))
1112
+ continue;
1113
+ if (!best || key.length < best.length) {
1114
+ best = key;
1115
+ }
1116
+ }
1117
+ return best;
1118
+ }
1119
+ /** WM's own verb token for each DataService action — the literal prefix its raw operationId's tail (after `<Entity>Controller_`) always starts with, independent of the entity's own singular/plural spelling. */
1120
+ const LIVE_OPERATION_VERBS = {
1121
+ readTableData: ["find"],
1122
+ searchTableDataWithQuery: ["filter"],
1123
+ searchTableData: ["search"],
1124
+ insertTableData: ["create"],
1125
+ updateTableData: ["edit"],
1126
+ deleteTableData: ["delete"],
1127
+ };
1128
+ /** Which DataService action formatServiceDetails' operationMap maps each `variable.operation` value to — mirrors that map's own verb choice (read -> filter/searchTableDataWithQuery, not find/readTableData) so the fallback resolves the SAME operation the guessed name was aiming for. */
1129
+ const LIVE_DEFAULT_ACTION_BY_OPERATION = {
1130
+ read: "searchTableDataWithQuery",
1131
+ insert: "insertTableData",
1132
+ update: "updateTableData",
1133
+ delete: "deleteTableData",
1134
+ };
1135
+ function transformLiveVariable(variable, scope, validServiceClassNames, methodArgsByOperation, methodNameByOperation, servicePathByOperation) {
1136
+ var _a, _b;
1137
+ const serviceDetails = formatServiceDetails(variable);
1138
+ let skipInvokeHttp = false;
1139
+ if (serviceDetails &&
1140
+ !isServiceAvailable(serviceDetails.formattedServiceName, validServiceClassNames)) {
1141
+ console.warn(`LiveVariable "${variable.name}" targets entity "${variable.type}" ` +
1142
+ `(${serviceDetails.formattedServiceName}), which was not generated. Emitting the ` +
1143
+ `variable without an invokeHttp — the runtime falls back to its own transport.`);
1144
+ skipInvokeHttp = true;
1145
+ }
662
1146
  const tv = transformVariable(variable, scope);
663
1147
  tv.classname = "LiveVariable";
1148
+ // Gates the service import AND the invokeHttp block in the template: with no
1149
+ // generated method to call, emitting either would not compile.
1150
+ tv.hasInvokeHttp = !skipInvokeHttp;
664
1151
  tv.isList = !!variable.isList;
665
1152
  tv.inFlightBehavior = variable.inFlightBehavior;
666
1153
  tv.maxResults = variable.maxResults;
@@ -677,9 +1164,132 @@ function transformLiveVariable(variable, scope) {
677
1164
  tv.filterExpressions = variable.filterExpressions;
678
1165
  tv._id = variable._id;
679
1166
  tv.isStaticParams = isStaticParamsVariable(variable);
1167
+ if (serviceDetails) {
1168
+ tv.serviceFile = serviceDetails.formattedServiceName;
1169
+ tv.serviceFileClass = serviceDetails.formattedServiceName;
1170
+ tv.serviceExportName = getServiceInstanceName(serviceDetails.formattedServiceName);
1171
+ tv.serviceInstanceName = getServiceLocalName(variable.liveSource, serviceDetails.formattedServiceName);
1172
+ tv.invokeFnName = serviceDetails.invokeFnName;
1173
+ // A LiveVariable has no raw `operationId` to key the sidecar lookups with — its method name
1174
+ // is derived from its operation instead (read -> filterDepartments, insert ->
1175
+ // createDepartment, ...). Resolve the same tables by that derived METHOD NAME, which the
1176
+ // generator also indexes (see findOperationKeyByMethodName), so a LiveVariable's generated
1177
+ // call gets the same correct positional arguments a ServiceVariable's does.
1178
+ let lookupKey = findOperationKeyByMethodName(variable.liveSource, serviceDetails.invokeFnName, methodNameByOperation);
1179
+ // The guessed name above is an English-stemming heuristic and can miss for entity names that
1180
+ // aren't real English plurals (see findLiveOperationKeyByVerb) — fall back to a verb-prefix
1181
+ // search against the raw operationId, which needs no such guess.
1182
+ if (!lookupKey) {
1183
+ lookupKey = findLiveOperationKeyByVerb(variable.liveSource, variable.type, (_a = LIVE_OPERATION_VERBS[LIVE_DEFAULT_ACTION_BY_OPERATION[variable.operation]]) !== null && _a !== void 0 ? _a : [], methodNameByOperation);
1184
+ }
1185
+ if (lookupKey) {
1186
+ // The fallback resolves the operation independently of the guessed name, so the guess
1187
+ // itself may still be wrong — realign tv.invokeFnName to the actual generated method name
1188
+ // rather than leaving the mismatched guess in place (this is the value applyServiceCallShape
1189
+ // uses at the top level when liveTargets ends up empty and the template falls back to it).
1190
+ tv.invokeFnName = (_b = methodNameByOperation === null || methodNameByOperation === void 0 ? void 0 : methodNameByOperation.get(lookupKey)) !== null && _b !== void 0 ? _b : tv.invokeFnName;
1191
+ applyServiceCallShape(tv, lookupKey, methodArgsByOperation, servicePathByOperation, true // LiveVariable — its params object is keyed by the runtime's own DB-operation names
1192
+ );
1193
+ }
1194
+ // `variable.operation` is only the DEFAULT operation — the one `.invoke()` runs. Every
1195
+ // LiveVariable carries all four record methods (see live-variable.ts's DataSource.Operation
1196
+ // switch), and a table bound to it as `datasource` calls insert/update/delete on THAT
1197
+ // variable, not on some other one. Emitting only the default meant a grid edit on a
1198
+ // `read` variable ran filterX with the row data. So resolve all four and let the glue
1199
+ // dispatch on the DB operation the manager passes in `params.operation`.
1200
+ tv.liveTargets = buildLiveTargets(variable, methodArgsByOperation, methodNameByOperation, servicePathByOperation);
1201
+ }
680
1202
  return tv;
681
1203
  }
682
- function transformCrudVariable(variable, scope) {
1204
+ /**
1205
+ * A LiveVariable's invokeHttp targets, one per DB operation the runtime can ask it for.
1206
+ *
1207
+ * Keyed by the manager's own action names (`params.operation`, set in
1208
+ * live-variable.manager.ts). Only the operations whose method actually generated are included, so
1209
+ * a service missing (say) a delete endpoint simply has no `delete` case rather than a call to a
1210
+ * method that does not exist.
1211
+ */
1212
+ function buildLiveTargets(variable, methodArgsByOperation, methodNameByOperation, servicePathByOperation) {
1213
+ var _a, _b, _c;
1214
+ const entity = variable.type;
1215
+ if (!entity) {
1216
+ return undefined;
1217
+ }
1218
+ const singular = simpleSingularize(entity);
1219
+ const plural = simplePluralize(singular);
1220
+ // The manager's action -> the generated method for it. Each action is a DISTINCT backend
1221
+ // endpoint with its own body shape, so they cannot share one method:
1222
+ // readTableData / searchTableDataWithQuery -> POST /<entity>/filter, body `q=<query string>`
1223
+ // searchTableData -> POST /<entity>/search, body a QueryFilter[] ARRAY
1224
+ // Collapsing any two of these sent the wrong body to the wrong route and silently matched
1225
+ // nothing.
1226
+ const byAction = [
1227
+ // readTableData's template is `GET /<entity>?page=&size=&:sort` — the collection GET, which
1228
+ // the generator names find<Plural>. filter<Plural> is the POST /filter twin and belongs only
1229
+ // to the query-bearing actions.
1230
+ ["readTableData", `find${plural}`],
1231
+ ["searchTableDataWithQuery", `filter${plural}`],
1232
+ ["searchTableData", `search${plural}ByQueryFilters`],
1233
+ ["insertTableData", `create${singular}`],
1234
+ ["updateTableData", `edit${singular}`],
1235
+ ["deleteTableData", `delete${singular}`],
1236
+ ];
1237
+ // All four methods for one entity are emitted onto ONE class (the entity's own service), and a
1238
+ // LiveVariable is pinned to a single liveSource — so every target shares the variable's own
1239
+ // import and instance. Only the method name and its argument list differ per action.
1240
+ const targets = [];
1241
+ for (const [action, guessedMethodName] of byAction) {
1242
+ let lookupKey = findOperationKeyByMethodName(variable.liveSource, guessedMethodName, methodNameByOperation);
1243
+ // Same English-stemming-guess-can-miss situation as transformLiveVariable's default target
1244
+ // above — fall back to a verb-prefix search that needs no singular/plural guess at all.
1245
+ if (!lookupKey) {
1246
+ lookupKey = findLiveOperationKeyByVerb(variable.liveSource, entity, (_a = LIVE_OPERATION_VERBS[action]) !== null && _a !== void 0 ? _a : [], methodNameByOperation);
1247
+ }
1248
+ if (!lookupKey || (servicePathByOperation && !servicePathByOperation.has(lookupKey))) {
1249
+ continue; // that operation was not generated for this entity
1250
+ }
1251
+ // The fallback resolves independently of the guess, so use the actual generated method name
1252
+ // (methodNameByOperation's own value for this key) rather than the possibly-wrong guess.
1253
+ const methodName = (_b = methodNameByOperation === null || methodNameByOperation === void 0 ? void 0 : methodNameByOperation.get(lookupKey)) !== null && _b !== void 0 ? _b : guessedMethodName;
1254
+ const shape = {};
1255
+ applyServiceCallShape(shape, lookupKey, methodArgsByOperation, servicePathByOperation, true);
1256
+ targets.push({
1257
+ action,
1258
+ invokeFnName: methodName,
1259
+ callArgs: (_c = shape.callArgs) !== null && _c !== void 0 ? _c : [],
1260
+ hasResolvedCallArgs: !!shape.hasResolvedCallArgs,
1261
+ });
1262
+ }
1263
+ return targets.length ? targets : undefined;
1264
+ }
1265
+ /**
1266
+ * A CrudVariable's invokeHttp targets, keyed by operationType.
1267
+ *
1268
+ * Unlike a ServiceVariable (one fixed operation), a CrudVariable names a GROUP whose member is
1269
+ * chosen per call, so the glue dispatches on the operation the runtime records for that call.
1270
+ * Each entry carries everything the call needs: the singleton to import, its local alias, the
1271
+ * method, and how to source each positional argument.
1272
+ */
1273
+ function buildCrudTargets(variable, crudOperationsById) {
1274
+ const group = crudOperationsById === null || crudOperationsById === void 0 ? void 0 : crudOperationsById.get(variable.crudOperationId);
1275
+ if (!group) {
1276
+ return undefined;
1277
+ }
1278
+ const targets = Object.entries(group).map(([operationType, target]) => {
1279
+ const className = target.importPath.split("/").pop();
1280
+ return {
1281
+ operationType,
1282
+ importPath: target.importPath,
1283
+ serviceExportName: getServiceInstanceName(className),
1284
+ serviceInstanceName: getServiceLocalName(variable.service, className),
1285
+ invokeFnName: target.methodName,
1286
+ // A CrudVariable is never a LiveVariable, so params are keyed by the swagger names.
1287
+ callArgs: target.args.map(arg => callArgExpression(arg, false)),
1288
+ };
1289
+ });
1290
+ return targets.length ? targets : undefined;
1291
+ }
1292
+ function transformCrudVariable(variable, scope, crudOperationsById) {
683
1293
  const tv = transformVariable(variable, scope);
684
1294
  tv._id = variable._id;
685
1295
  tv.name = variable.name;
@@ -705,15 +1315,16 @@ function transformCrudVariable(variable, scope) {
705
1315
  tv.dataBinding = variable._transformedDataBinding || variable.dataBinding;
706
1316
  tv.classname = "CrudVariable";
707
1317
  tv.group = "variable";
1318
+ tv.crudTargets = buildCrudTargets(variable, crudOperationsById);
708
1319
  tv.isStaticParams = isStaticParamsVariable(variable);
709
1320
  return tv;
710
1321
  }
711
- exports.default = (variable, scope, appUrl, imports) => {
1322
+ exports.default = (variable, scope, appUrl, imports, validServiceClassNames, methodArgsByOperation, methodNameByOperation, servicePathByOperation, crudOperationsById) => {
712
1323
  switch (variable.category) {
713
1324
  case "wm.Variable":
714
1325
  return transformModelVariable(variable, scope);
715
1326
  case "wm.ServiceVariable":
716
- return transformServiceVariable(variable, scope);
1327
+ return transformServiceVariable(variable, scope, validServiceClassNames, methodArgsByOperation, methodNameByOperation, servicePathByOperation);
717
1328
  case "wm.NavigationAction":
718
1329
  return transformNavigationAction(variable, scope);
719
1330
  case "wm.TimerAction":
@@ -727,9 +1338,9 @@ exports.default = (variable, scope, appUrl, imports) => {
727
1338
  case "wm.LogoutAction":
728
1339
  return transformLogoutVariable(variable, scope);
729
1340
  case "wm.LiveVariable":
730
- return transformLiveVariable(variable, scope);
1341
+ return transformLiveVariable(variable, scope, validServiceClassNames, methodArgsByOperation, methodNameByOperation, servicePathByOperation);
731
1342
  case "wm.CrudVariable":
732
- return transformCrudVariable(variable, scope);
1343
+ return transformCrudVariable(variable, scope, crudOperationsById);
733
1344
  }
734
1345
  return null;
735
1346
  };