@memberjunction/codegen-lib 5.51.0 → 6.1.0-edge.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +194 -0
  3. package/dist/Database/codeGenDatabaseProvider.d.ts +15 -0
  4. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  5. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  6. package/dist/Database/manage-metadata.d.ts +22 -1
  7. package/dist/Database/manage-metadata.d.ts.map +1 -1
  8. package/dist/Database/manage-metadata.js +121 -5
  9. package/dist/Database/manage-metadata.js.map +1 -1
  10. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +37 -3
  11. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  12. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +65 -4
  13. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  14. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +2 -0
  15. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  16. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +12 -1
  17. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  18. package/dist/Database/providers/sqlserver/__tests__/LayeredBaseView.test.d.ts +2 -0
  19. package/dist/Database/providers/sqlserver/__tests__/LayeredBaseView.test.d.ts.map +1 -0
  20. package/dist/Database/providers/sqlserver/__tests__/LayeredBaseView.test.js +252 -0
  21. package/dist/Database/providers/sqlserver/__tests__/LayeredBaseView.test.js.map +1 -0
  22. package/dist/Database/search-guardrails.d.ts +146 -0
  23. package/dist/Database/search-guardrails.d.ts.map +1 -0
  24. package/dist/Database/search-guardrails.js +233 -0
  25. package/dist/Database/search-guardrails.js.map +1 -0
  26. package/dist/Database/sql.d.ts.map +1 -1
  27. package/dist/Database/sql.js +42 -10
  28. package/dist/Database/sql.js.map +1 -1
  29. package/dist/Database/sql_codegen.d.ts +40 -0
  30. package/dist/Database/sql_codegen.d.ts.map +1 -1
  31. package/dist/Database/sql_codegen.js +110 -21
  32. package/dist/Database/sql_codegen.js.map +1 -1
  33. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
  34. package/dist/Manifest/GenerateClassRegistrationsManifest.js +25 -5
  35. package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
  36. package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
  37. package/dist/Misc/action_subclasses_codegen.js +13 -3
  38. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  39. package/dist/Misc/advanced_generation.d.ts.map +1 -1
  40. package/dist/Misc/advanced_generation.js +8 -1
  41. package/dist/Misc/advanced_generation.js.map +1 -1
  42. package/dist/Misc/entity_subclasses_codegen.d.ts +81 -1
  43. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  44. package/dist/Misc/entity_subclasses_codegen.js +234 -1
  45. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  46. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  47. package/dist/Misc/graphql_server_codegen.js +13 -2
  48. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  49. package/package.json +40 -35
@@ -12,6 +12,7 @@ import { SQLUtilityBase } from "./sql.js";
12
12
  import { applyIncludeSchemaScope } from "./schema-scope.js";
13
13
  import { AdvancedGeneration } from "../Misc/advanced_generation.js";
14
14
  import { CodeGenReporter } from "../Misc/codegen-reporter.js";
15
+ import { applySearchableFieldsCap, entityLevelEnableBlockedReason, isNarrativeFieldName, normalizePredicate, normalizeSmartFieldResultShape, } from "./search-guardrails.js";
15
16
  import { mapExternalNativeTypeToMJ } from "../Misc/externalTypeMapping.js";
16
17
  import { SQLParser } from "@memberjunction/sql-parser";
17
18
  import { createDisplayName, generatePluralName, MJGlobal, stripTrailingChars, UUIDsEqual } from "@memberjunction/global";
@@ -106,7 +107,7 @@ export class ManageMetadataBase {
106
107
  }
107
108
  /**
108
109
  * Produces a schema-qualified object reference.
109
- * SQL Server: [schema].[object], PostgreSQL: schema."object"
110
+ * SQL Server: [schema].[object], PostgreSQL: "schema"."object"
110
111
  */
111
112
  qs(schema, object) {
112
113
  return this.dialect.QuoteSchema(schema, object);
@@ -1297,6 +1298,7 @@ export class ManageMetadataBase {
1297
1298
  await import('@memberjunction/external-data-source-sqlserver');
1298
1299
  await import('@memberjunction/external-data-source-mysql');
1299
1300
  await import('@memberjunction/external-data-source-oracle');
1301
+ await import('@memberjunction/external-data-source-databricks');
1300
1302
  const router = MJGlobal.Instance.ClassFactory.CreateInstance(ExternalDataSourceReadRouter);
1301
1303
  if (!router) {
1302
1304
  logError(' Cannot sync external entity fields: no ExternalDataSourceReadRouter is registered. Ensure @memberjunction/external-data-sources (and the relevant driver) are loaded in the CodeGen process.');
@@ -4777,6 +4779,10 @@ export class ManageMetadataBase {
4777
4779
  * Apply smart field identification results to entity fields and entity-level search configuration
4778
4780
  */
4779
4781
  async applySmartFieldIdentification(pool, entity, fields, result) {
4782
+ // Run the guardrail pipeline once up front so every downstream applier
4783
+ // sees the same already-normalized result. See search-guardrails.ts for
4784
+ // the rules. Mutates result in place — see normalizeSearchFlagsInPlace.
4785
+ this.normalizeSearchFlagsInPlace(entity, fields, result);
4780
4786
  const sqlStatements = [];
4781
4787
  this.applyNameFieldUpdates(sqlStatements, fields, result);
4782
4788
  this.applyDefaultInViewUpdates(sqlStatements, fields, result);
@@ -4796,6 +4802,100 @@ export class ManageMetadataBase {
4796
4802
  }
4797
4803
  }
4798
4804
  }
4805
+ /**
4806
+ * Apply the code-level search guardrails to the LLM result, mutating
4807
+ * `result` in place so every downstream applier reads the normalized
4808
+ * version. The pure heuristics live in `search-guardrails.ts`; this
4809
+ * method wires them to the per-field metadata (Type, Length, IsPrimaryKey,
4810
+ * AutoUpdate flags) we have on `fields`.
4811
+ *
4812
+ * Order of operations:
4813
+ * 1. Shape cleanup (empty/contradictory states).
4814
+ * 2. Field-level eligibility — filter through `isFieldEligibleForUserSearch`
4815
+ * and the narrative-field-name blocklist.
4816
+ * 3. Per-entity cap.
4817
+ * 4. Predicate normalization — rewrite `Contains` to a default when the
4818
+ * field isn't FTS-backed, fill in defaults for missing entries.
4819
+ * 5. Telemetry — record proposed/accepted counts and any rewrites for
4820
+ * the end-of-run report.
4821
+ */
4822
+ normalizeSearchFlagsInPlace(entity, fields, result) {
4823
+ const proposedSearchableCount = (result.searchableFields ?? []).length;
4824
+ const proposedAllowUserSearch = result.allowUserSearch === true;
4825
+ // 1. Shape cleanup (defensive — identifyFields() also runs this).
4826
+ const cleaned = normalizeSmartFieldResultShape(result);
4827
+ result.searchableFields = cleaned.searchableFields ?? [];
4828
+ result.searchPredicates = cleaned.searchPredicates ?? [];
4829
+ result.allowUserSearch = cleaned.allowUserSearch;
4830
+ // 2. Eligibility filter (PK / non-text / MAX-without-FTX / narrative).
4831
+ const ftxEnabled = !!entity.FullTextSearchEnabled;
4832
+ const fieldByName = new Map();
4833
+ for (const f of fields) {
4834
+ fieldByName.set(f.Name, f);
4835
+ }
4836
+ const eligible = [];
4837
+ let droppedNarrativeCount = 0;
4838
+ let droppedIneligibleCount = 0;
4839
+ for (const name of result.searchableFields) {
4840
+ const field = fieldByName.get(name);
4841
+ if (!field)
4842
+ continue; // already logged elsewhere
4843
+ if (!this.isFieldEligibleForUserSearch(field, ftxEnabled)) {
4844
+ droppedIneligibleCount += 1;
4845
+ continue;
4846
+ }
4847
+ if (isNarrativeFieldName(name) && !ftxEnabled) {
4848
+ droppedNarrativeCount += 1;
4849
+ continue;
4850
+ }
4851
+ eligible.push(name);
4852
+ }
4853
+ // 3. Per-entity cap.
4854
+ const { accepted, dropped: droppedByCap } = applySearchableFieldsCap(eligible);
4855
+ result.searchableFields = accepted;
4856
+ // 4. Predicate normalization. Build a fresh searchPredicates list keyed
4857
+ // on the accepted fields. The LLM's proposed predicates are the input
4858
+ // if present; missing entries get defaults.
4859
+ const proposedByName = new Map();
4860
+ for (const sp of result.searchPredicates ?? []) {
4861
+ if (sp && typeof sp.field === 'string') {
4862
+ proposedByName.set(sp.field, sp.predicate);
4863
+ }
4864
+ }
4865
+ const ftsFieldSet = new Set(result.fullTextSearchFields ?? []);
4866
+ const normalizedPredicates = [];
4867
+ let predicatesRewrittenCount = 0;
4868
+ for (const fieldName of accepted) {
4869
+ const { predicate, rewritten } = normalizePredicate({
4870
+ fieldName,
4871
+ proposed: proposedByName.get(fieldName),
4872
+ isInFullTextSearchFields: ftsFieldSet.has(fieldName),
4873
+ entityFullTextSearchEnabled: ftxEnabled || result.enableFullTextSearch === true,
4874
+ });
4875
+ if (rewritten)
4876
+ predicatesRewrittenCount += 1;
4877
+ normalizedPredicates.push({ field: fieldName, predicate });
4878
+ }
4879
+ result.searchPredicates = normalizedPredicates;
4880
+ // If the cap or guardrails left us with no searchable fields, the
4881
+ // entity-level enable can't survive — re-run shape cleanup so
4882
+ // applyEntitySearchConfig sees a coherent result.
4883
+ if (accepted.length === 0 && result.allowUserSearch === true) {
4884
+ result.allowUserSearch = false;
4885
+ }
4886
+ // 5. Telemetry.
4887
+ const reporter = CodeGenReporter.Instance;
4888
+ reporter.counter('search.searchableFieldsProposed', proposedSearchableCount);
4889
+ reporter.counter('search.searchableFieldsAccepted', accepted.length);
4890
+ reporter.counter('search.searchableFieldsDroppedIneligible', droppedIneligibleCount);
4891
+ reporter.counter('search.searchableFieldsDroppedNarrative', droppedNarrativeCount);
4892
+ reporter.counter('search.searchableFieldsDroppedByCap', droppedByCap.length);
4893
+ reporter.counter('search.predicatesRewritten', predicatesRewrittenCount);
4894
+ if (proposedAllowUserSearch)
4895
+ reporter.counter('search.allowUserSearchProposed', 1);
4896
+ if (result.allowUserSearch === true)
4897
+ reporter.counter('search.allowUserSearchAcceptedSoFar', 1);
4898
+ }
4799
4899
  /**
4800
4900
  * Generate SQL UPDATEs for IsNameField — SINGLE-WINNER semantics.
4801
4901
  *
@@ -5035,10 +5135,26 @@ export class ManageMetadataBase {
5035
5135
  }
5036
5136
  const newValue = result.allowUserSearch;
5037
5137
  const currentValue = !!entity.AllowUserSearchAPI;
5038
- // If the LLM is proposing to ENABLE search on a log/audit/run-history-style
5039
- // entity, drop the proposal. We never block a proposal to DISABLE search.
5040
- if (newValue && !currentValue && this.isLikelyLogOrAuditEntity(entity.Name, entity.SchemaName)) {
5041
- return;
5138
+ // We never block a proposal to DISABLE search.
5139
+ if (newValue && !currentValue) {
5140
+ // Default-off semantics: a 0 → 1 flip requires the LLM to be highly
5141
+ // confident, requires at least one searchable field to have survived
5142
+ // the field-level guardrails, and refuses log/audit and detail/line-
5143
+ // item-shaped entities. Anything blocked here gets a one-line note in
5144
+ // the CodeGen run report so the team can audit proposals.
5145
+ const blocked = entityLevelEnableBlockedReason({
5146
+ entityName: entity.Name,
5147
+ confidence: result.confidence,
5148
+ acceptedSearchableFieldsCount: (result.searchableFields ?? []).length,
5149
+ });
5150
+ const reporter = CodeGenReporter.Instance;
5151
+ if (blocked) {
5152
+ reporter.counter('search.allowUserSearchEnableBlocked', 1);
5153
+ reporter.note(`[search] refused AllowUserSearchAPI 0→1 on '${entity.Name}': ${blocked}`);
5154
+ return;
5155
+ }
5156
+ reporter.counter('search.allowUserSearchEnableAccepted', 1);
5157
+ reporter.note(`[search] enabled AllowUserSearchAPI on '${entity.Name}': ${result.allowUserSearchReason ?? 'no reason given'}`);
5042
5158
  }
5043
5159
  if (newValue !== currentValue) {
5044
5160
  sqlStatements.push(`