@memberjunction/core-entities-server 6.2.0-edge.6 → 6.2.0-edge.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -100
- package/dist/custom/AnchoredConversationReadSecurityHook.d.ts +22 -0
- package/dist/custom/AnchoredConversationReadSecurityHook.d.ts.map +1 -0
- package/dist/custom/AnchoredConversationReadSecurityHook.js +293 -0
- package/dist/custom/AnchoredConversationReadSecurityHook.js.map +1 -0
- package/dist/custom/MJAIAgentExampleEntityServer.server.d.ts +2 -0
- package/dist/custom/MJAIAgentExampleEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJAIAgentExampleEntityServer.server.js +8 -5
- package/dist/custom/MJAIAgentExampleEntityServer.server.js.map +1 -1
- package/dist/custom/MJAIAgentNoteEntityServer.server.d.ts +2 -0
- package/dist/custom/MJAIAgentNoteEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJAIAgentNoteEntityServer.server.js +8 -5
- package/dist/custom/MJAIAgentNoteEntityServer.server.js.map +1 -1
- package/dist/custom/MJAIAgentRelationshipEntityServer.server.d.ts +17 -0
- package/dist/custom/MJAIAgentRelationshipEntityServer.server.d.ts.map +1 -0
- package/dist/custom/MJAIAgentRelationshipEntityServer.server.js +71 -0
- package/dist/custom/MJAIAgentRelationshipEntityServer.server.js.map +1 -0
- package/dist/custom/MJAIAgentSessionEntityServer.server.d.ts +8 -22
- package/dist/custom/MJAIAgentSessionEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJAIAgentSessionEntityServer.server.js +24 -1
- package/dist/custom/MJAIAgentSessionEntityServer.server.js.map +1 -1
- package/dist/custom/MJAIAgentUserPolicyEntityServer.server.d.ts +23 -0
- package/dist/custom/MJAIAgentUserPolicyEntityServer.server.d.ts.map +1 -0
- package/dist/custom/MJAIAgentUserPolicyEntityServer.server.js +53 -0
- package/dist/custom/MJAIAgentUserPolicyEntityServer.server.js.map +1 -0
- package/dist/custom/MJAIPromptEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJAIPromptEntityServer.server.js +2 -1
- package/dist/custom/MJAIPromptEntityServer.server.js.map +1 -1
- package/dist/custom/MJBrandKitEntityServer.server.d.ts +14 -0
- package/dist/custom/MJBrandKitEntityServer.server.d.ts.map +1 -0
- package/dist/custom/MJBrandKitEntityServer.server.js +75 -0
- package/dist/custom/MJBrandKitEntityServer.server.js.map +1 -0
- package/dist/custom/MJCompanyIntegrationEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJCompanyIntegrationEntityServer.server.js +1 -0
- package/dist/custom/MJCompanyIntegrationEntityServer.server.js.map +1 -1
- package/dist/custom/MJComponentEntityServer.server.d.ts +2 -0
- package/dist/custom/MJComponentEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJComponentEntityServer.server.js +5 -1
- package/dist/custom/MJComponentEntityServer.server.js.map +1 -1
- package/dist/custom/MJConversationDetailEntityServer.server.d.ts +20 -30
- package/dist/custom/MJConversationDetailEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJConversationDetailEntityServer.server.js +89 -35
- package/dist/custom/MJConversationDetailEntityServer.server.js.map +1 -1
- package/dist/custom/MJConversationEntityServer.server.d.ts +19 -4
- package/dist/custom/MJConversationEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJConversationEntityServer.server.js +169 -11
- package/dist/custom/MJConversationEntityServer.server.js.map +1 -1
- package/dist/custom/MJEntityDocumentEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJEntityDocumentEntityServer.server.js +1 -0
- package/dist/custom/MJEntityDocumentEntityServer.server.js.map +1 -1
- package/dist/custom/MJListDetailEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJListDetailEntityServer.server.js +1 -0
- package/dist/custom/MJListDetailEntityServer.server.js.map +1 -1
- package/dist/custom/MJProjectEntityServer.server.d.ts +21 -0
- package/dist/custom/MJProjectEntityServer.server.d.ts.map +1 -0
- package/dist/custom/MJProjectEntityServer.server.js +106 -0
- package/dist/custom/MJProjectEntityServer.server.js.map +1 -0
- package/dist/custom/MJQueryEntityServer.server.d.ts +177 -47
- package/dist/custom/MJQueryEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJQueryEntityServer.server.js +479 -164
- package/dist/custom/MJQueryEntityServer.server.js.map +1 -1
- package/dist/custom/MJQuerySQLEntityServer.server.d.ts +15 -18
- package/dist/custom/MJQuerySQLEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJQuerySQLEntityServer.server.js +34 -42
- package/dist/custom/MJQuerySQLEntityServer.server.js.map +1 -1
- package/dist/custom/MJTagEntityServer.server.d.ts +2 -0
- package/dist/custom/MJTagEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJTagEntityServer.server.js +13 -7
- package/dist/custom/MJTagEntityServer.server.js.map +1 -1
- package/dist/custom/MJUserRoutineEntityServer.server.d.ts.map +1 -1
- package/dist/custom/MJUserRoutineEntityServer.server.js +8 -1
- package/dist/custom/MJUserRoutineEntityServer.server.js.map +1 -1
- package/dist/custom/RestrictedFieldCommentClassifier.d.ts +73 -0
- package/dist/custom/RestrictedFieldCommentClassifier.d.ts.map +1 -0
- package/dist/custom/RestrictedFieldCommentClassifier.js +240 -0
- package/dist/custom/RestrictedFieldCommentClassifier.js.map +1 -0
- package/dist/custom/query-extraction/childRowWriter.d.ts +77 -0
- package/dist/custom/query-extraction/childRowWriter.d.ts.map +1 -0
- package/dist/custom/query-extraction/childRowWriter.js +277 -0
- package/dist/custom/query-extraction/childRowWriter.js.map +1 -0
- package/dist/custom/query-extraction/compositionTargets.d.ts +40 -0
- package/dist/custom/query-extraction/compositionTargets.d.ts.map +1 -0
- package/dist/custom/query-extraction/compositionTargets.js +99 -0
- package/dist/custom/query-extraction/compositionTargets.js.map +1 -0
- package/dist/custom/query-extraction/declared.d.ts +79 -0
- package/dist/custom/query-extraction/declared.d.ts.map +1 -0
- package/dist/custom/query-extraction/declared.js +91 -0
- package/dist/custom/query-extraction/declared.js.map +1 -0
- package/dist/custom/query-extraction/describeFacts.d.ts +40 -0
- package/dist/custom/query-extraction/describeFacts.d.ts.map +1 -0
- package/dist/custom/query-extraction/describeFacts.js +225 -0
- package/dist/custom/query-extraction/describeFacts.js.map +1 -0
- package/dist/custom/query-extraction/descriptionPass.d.ts +23 -0
- package/dist/custom/query-extraction/descriptionPass.d.ts.map +1 -0
- package/dist/custom/query-extraction/descriptionPass.js +95 -0
- package/dist/custom/query-extraction/descriptionPass.js.map +1 -0
- package/dist/custom/query-extraction/extractQuery.d.ts +25 -0
- package/dist/custom/query-extraction/extractQuery.d.ts.map +1 -0
- package/dist/custom/query-extraction/extractQuery.js +146 -0
- package/dist/custom/query-extraction/extractQuery.js.map +1 -0
- package/dist/custom/query-extraction/extractionTypes.d.ts +133 -0
- package/dist/custom/query-extraction/extractionTypes.d.ts.map +1 -0
- package/dist/custom/query-extraction/extractionTypes.js +2 -0
- package/dist/custom/query-extraction/extractionTypes.js.map +1 -0
- package/dist/custom/query-extraction/index.d.ts +16 -0
- package/dist/custom/query-extraction/index.d.ts.map +1 -1
- package/dist/custom/query-extraction/index.js +11 -1
- package/dist/custom/query-extraction/index.js.map +1 -1
- package/dist/custom/query-extraction/pipeline.d.ts +6 -2
- package/dist/custom/query-extraction/pipeline.d.ts.map +1 -1
- package/dist/custom/query-extraction/pipeline.js +21 -14
- package/dist/custom/query-extraction/pipeline.js.map +1 -1
- package/dist/custom/query-extraction/resolve.d.ts +7 -2
- package/dist/custom/query-extraction/resolve.d.ts.map +1 -1
- package/dist/custom/query-extraction/resolve.js +17 -14
- package/dist/custom/query-extraction/resolve.js.map +1 -1
- package/dist/custom/query-extraction/sourceBinding.d.ts +57 -0
- package/dist/custom/query-extraction/sourceBinding.d.ts.map +1 -0
- package/dist/custom/query-extraction/sourceBinding.js +104 -0
- package/dist/custom/query-extraction/sourceBinding.js.map +1 -0
- package/dist/custom/query-extraction/sync.d.ts +27 -3
- package/dist/custom/query-extraction/sync.d.ts.map +1 -1
- package/dist/custom/query-extraction/sync.js +69 -44
- package/dist/custom/query-extraction/sync.js.map +1 -1
- package/dist/custom/query-extraction/templateFacts.d.ts +62 -0
- package/dist/custom/query-extraction/templateFacts.d.ts.map +1 -0
- package/dist/custom/query-extraction/templateFacts.js +229 -0
- package/dist/custom/query-extraction/templateFacts.js.map +1 -0
- package/dist/custom/query-extraction/types.d.ts +4 -0
- package/dist/custom/query-extraction/types.d.ts.map +1 -1
- package/dist/custom/query-refresh/QueryExtractionService.d.ts +54 -0
- package/dist/custom/query-refresh/QueryExtractionService.d.ts.map +1 -0
- package/dist/custom/query-refresh/QueryExtractionService.js +207 -0
- package/dist/custom/query-refresh/QueryExtractionService.js.map +1 -0
- package/dist/custom/query-refresh/QueryRefreshExtractionServerOperation.d.ts +28 -0
- package/dist/custom/query-refresh/QueryRefreshExtractionServerOperation.d.ts.map +1 -0
- package/dist/custom/query-refresh/QueryRefreshExtractionServerOperation.js +106 -0
- package/dist/custom/query-refresh/QueryRefreshExtractionServerOperation.js.map +1 -0
- package/dist/custom/query-refresh/index.d.ts +14 -0
- package/dist/custom/query-refresh/index.d.ts.map +1 -0
- package/dist/custom/query-refresh/index.js +10 -0
- package/dist/custom/query-refresh/index.js.map +1 -0
- package/dist/custom/query-refresh/queryRebind.d.ts +33 -0
- package/dist/custom/query-refresh/queryRebind.d.ts.map +1 -0
- package/dist/custom/query-refresh/queryRebind.js +85 -0
- package/dist/custom/query-refresh/queryRebind.js.map +1 -0
- package/dist/custom/query-refresh/refreshAuthorization.d.ts +29 -0
- package/dist/custom/query-refresh/refreshAuthorization.d.ts.map +1 -0
- package/dist/custom/query-refresh/refreshAuthorization.js +66 -0
- package/dist/custom/query-refresh/refreshAuthorization.js.map +1 -0
- package/dist/custom/query-refresh/refreshFormat.d.ts +16 -0
- package/dist/custom/query-refresh/refreshFormat.d.ts.map +1 -0
- package/dist/custom/query-refresh/refreshFormat.js +67 -0
- package/dist/custom/query-refresh/refreshFormat.js.map +1 -0
- package/dist/custom/query-refresh/refreshOrder.d.ts +25 -0
- package/dist/custom/query-refresh/refreshOrder.d.ts.map +1 -0
- package/dist/custom/query-refresh/refreshOrder.js +95 -0
- package/dist/custom/query-refresh/refreshOrder.js.map +1 -0
- package/dist/custom/query-refresh/refreshSelection.d.ts +15 -0
- package/dist/custom/query-refresh/refreshSelection.d.ts.map +1 -0
- package/dist/custom/query-refresh/refreshSelection.js +99 -0
- package/dist/custom/query-refresh/refreshSelection.js.map +1 -0
- package/dist/custom/query-refresh/refreshTypes.d.ts +81 -0
- package/dist/custom/query-refresh/refreshTypes.d.ts.map +1 -0
- package/dist/custom/query-refresh/refreshTypes.js +5 -0
- package/dist/custom/query-refresh/refreshTypes.js.map +1 -0
- package/dist/custom/util.d.ts +8 -0
- package/dist/custom/util.d.ts.map +1 -1
- package/dist/custom/util.js +18 -1
- package/dist/custom/util.js.map +1 -1
- package/dist/engines/QueryEngineServer.d.ts +8 -5
- package/dist/engines/QueryEngineServer.d.ts.map +1 -1
- package/dist/engines/QueryEngineServer.js +16 -23
- package/dist/engines/QueryEngineServer.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/package.json +30 -30
|
@@ -4,21 +4,56 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { BaseEntity, LogError, } from "@memberjunction/core";
|
|
7
|
+
import { BaseEntity, BaseEntityResult, DatabaseProviderBase, LogError, ProviderBase, ProviderType, } from "@memberjunction/core";
|
|
8
8
|
import { ComputeQueryFingerprintAsync, MJQueryEntityExtended, QueryEngine, } from "@memberjunction/core-entities";
|
|
9
9
|
import { EscapeSQLString, Float32VectorToBase64, RegisterClass, MJGlobal, UUIDsEqual } from "@memberjunction/global";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { resolveDbPlatformFromEnv } from "@memberjunction/generic-database-provider";
|
|
10
|
+
import { GenericDatabaseProvider, UserCache } from "@memberjunction/generic-database-provider";
|
|
11
|
+
import { CompositionFullPath } from "@memberjunction/sql-parser";
|
|
12
|
+
import { EmbedTextLocalHelper, IsLocalEmbeddingStaleHelper } from "./util.js";
|
|
13
|
+
import { CleanupQueryData, ConvertTSQLToPostgreSQL, CollectDeclaredQueryChildren, HasDeclaredQueryChildren, FindReplacedRows, QueueReplacedRowDeletion, QueryChildNameKey, QueryEntityKey, NO_DECLARED_QUERY_CHILDREN, ComputeQueryExtraction, WriteExtractedChildRows, ApplyFieldFacts, RunDescriptionPass, SQLUsesTemplate, ReadCompositionTargetsAsync, MergeCompositionCandidates, } from "./query-extraction/index.js";
|
|
15
14
|
import { ApplyApprovalRulesAsync, ApprovalRefusalResult } from "./approvalStamp.js";
|
|
15
|
+
/** Whether `provider` can read views and create entity objects, which is all extraction's writes need. */
|
|
16
|
+
function IsExtractionProvider(provider) {
|
|
17
|
+
if (provider === null || typeof provider !== 'object')
|
|
18
|
+
return false;
|
|
19
|
+
return typeof Reflect.get(provider, 'RunView') === 'function' && typeof Reflect.get(provider, 'GetEntityObject') === 'function';
|
|
20
|
+
}
|
|
16
21
|
/**
|
|
17
|
-
* Server-side query entity with embedding generation, SQL extraction
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* Server-side query entity with embedding generation, SQL extraction, and dialect auto-conversion.
|
|
23
|
+
* Extends `MJQueryEntityExtended`, which provides the child-relationship getters and business
|
|
24
|
+
* logic available on both client and server.
|
|
25
|
+
*
|
|
26
|
+
* **Extraction.** A save of a new query, of a query whose SQL changed, or with
|
|
27
|
+
* {@link ExtractionOverride} `Force`, establishes the query's facts (`ComputeQueryExtraction`):
|
|
28
|
+
* its parameters, compositions and the entities it reads from MJ's parser, and its output fields
|
|
29
|
+
* from the database server's description of each render. Save-time rule errors and repeated
|
|
30
|
+
* output names refuse a new or edited query, with the reasons in the save's result. Once the save
|
|
31
|
+
* of the query row succeeds, the child rows are brought in line with the facts as the system user,
|
|
32
|
+
* keeping what people authored (`WriteExtractedChildRows`), and the query's `UsesTemplate`,
|
|
33
|
+
* `ExtractedSQLHash`, `ExtractionStatus` and `ExtractionMessage` record the run. Empty
|
|
34
|
+
* descriptions are then filled by an LLM after the save's transaction commits, unless the save
|
|
35
|
+
* skips AI actions.
|
|
20
36
|
*/
|
|
21
37
|
let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtended {
|
|
38
|
+
constructor() {
|
|
39
|
+
super(...arguments);
|
|
40
|
+
/**
|
|
41
|
+
* What the next save does about extraction: `Auto` (the default) extracts when the query is new
|
|
42
|
+
* or its SQL changed; `Skip` does not extract, so the save-time rules are not applied and the
|
|
43
|
+
* child rows, the Extraction columns and `UsesTemplate` stay as they are (for bulk loaders and
|
|
44
|
+
* fixtures that write those themselves); `Force` extracts even when the SQL did not change.
|
|
45
|
+
* Reset to `Auto` after each save.
|
|
46
|
+
*/
|
|
47
|
+
this.ExtractionOverride = 'Auto';
|
|
48
|
+
/** The SQL a re-extraction reads instead of the cached platform variant (set by {@link RerunExtraction}). */
|
|
49
|
+
this.platformSQLOverride = null;
|
|
50
|
+
/** The facts an explicit refresh already established, which its save writes instead of extracting again. */
|
|
51
|
+
this.presetExtraction = null;
|
|
52
|
+
/** Whether the running save is an explicit refresh ({@link ApplyRefreshAsync}). */
|
|
53
|
+
this.refreshing = false;
|
|
54
|
+
/** What the last save's child-row writes changed; `null` when it wrote none. */
|
|
55
|
+
this.lastWriteSummary = null;
|
|
56
|
+
}
|
|
22
57
|
// ─── Embedding Methods ───────────────────────────────────────────────────────
|
|
23
58
|
/**
|
|
24
59
|
* Simple proxy to local helper method for embeddings. Needed for BaseEntity sub-classes that want to use embeddings built into BaseEntity
|
|
@@ -26,6 +61,10 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
|
|
|
26
61
|
async EmbedTextLocal(textToEmbed) {
|
|
27
62
|
return EmbedTextLocalHelper(this, textToEmbed);
|
|
28
63
|
}
|
|
64
|
+
/** A stored vector from a model other than the default local one is re-embedded on save. */
|
|
65
|
+
async IsEmbeddingStale(embeddingModelID) {
|
|
66
|
+
return IsLocalEmbeddingStaleHelper(this, embeddingModelID);
|
|
67
|
+
}
|
|
29
68
|
/**
|
|
30
69
|
* Generates an embedding from composite text (Name + UserQuestion + Description) for richer semantic search.
|
|
31
70
|
* Stores the vector in EmbeddingVector (JSON) and EmbeddingVectorBinary (float32 bytes, base64), and the
|
|
@@ -35,8 +74,17 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
|
|
|
35
74
|
* embedder error — all three columns are cleared, so a query whose text changed never keeps a vector
|
|
36
75
|
* computed from its old text. An embedder error is logged and does not fail the save; the query simply
|
|
37
76
|
* has no semantic match until its next successful embedding.
|
|
77
|
+
*
|
|
78
|
+
* Does nothing when {@link SkipEmbeddings} is set (a bulk load computes them later), so the stored
|
|
79
|
+
* vector is left as it is.
|
|
80
|
+
*
|
|
81
|
+
* @param textChanged - false when the text is unchanged and only the stored vector's model is stale:
|
|
82
|
+
* a failed embed then keeps the existing vector, which still matches the text.
|
|
38
83
|
*/
|
|
39
|
-
async GenerateCompositeEmbedding() {
|
|
84
|
+
async GenerateCompositeEmbedding(textChanged = true) {
|
|
85
|
+
if (this.SkipEmbeddings) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
40
88
|
const parts = [
|
|
41
89
|
this.Name || '',
|
|
42
90
|
this.UserQuestion || '',
|
|
@@ -59,7 +107,9 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
|
|
|
59
107
|
catch (error) {
|
|
60
108
|
LogError(`[MJQueryEntityServer] Embedding refresh failed for query "${this.Name}": ${error instanceof Error ? error.message : String(error)}`);
|
|
61
109
|
}
|
|
62
|
-
|
|
110
|
+
if (textChanged) {
|
|
111
|
+
this.clearEmbedding();
|
|
112
|
+
}
|
|
63
113
|
}
|
|
64
114
|
/** Clears the stored vector (both forms) and the model it was computed with. */
|
|
65
115
|
clearEmbedding() {
|
|
@@ -67,8 +117,22 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
|
|
|
67
117
|
this.EmbeddingVectorBinary = null;
|
|
68
118
|
this.EmbeddingModelID = null;
|
|
69
119
|
}
|
|
120
|
+
/** Re-embeds when the embedded text changed, or when the stored vector came from a model other than the default local one. */
|
|
121
|
+
async refreshEmbeddingAsync() {
|
|
122
|
+
if (this.SkipEmbeddings)
|
|
123
|
+
return;
|
|
124
|
+
const textChanged = !this.IsSaved || ['Name', 'Description', 'UserQuestion'].some(f => this.GetFieldByName(f).Dirty);
|
|
125
|
+
if (textChanged || (this.EmbeddingVector != null && await this.IsEmbeddingStale(this.EmbeddingModelID))) {
|
|
126
|
+
await this.GenerateCompositeEmbedding(textChanged);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
70
129
|
// ─── Save / Delete Overrides ─────────────────────────────────────────────────
|
|
71
130
|
async Save(options) {
|
|
131
|
+
// Over a client provider (a process that loads this package but saves over GraphQL), the server
|
|
132
|
+
// the save reaches applies the approval rules and extracts; doing it here too would write the
|
|
133
|
+
// child rows twice, the second time from the client.
|
|
134
|
+
if (this.savesOverNetwork)
|
|
135
|
+
return super.Save(options);
|
|
72
136
|
let approving;
|
|
73
137
|
try {
|
|
74
138
|
// Approving is a server act; a ReplayOnly save writes nothing, so it has nothing to stamp.
|
|
@@ -79,47 +143,185 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
|
|
|
79
143
|
return false;
|
|
80
144
|
}
|
|
81
145
|
try {
|
|
82
|
-
|
|
83
|
-
const nameField = this.GetFieldByName('Name');
|
|
84
|
-
const descriptionField = this.GetFieldByName('Description');
|
|
85
|
-
const userQuestionField = this.GetFieldByName('UserQuestion');
|
|
86
|
-
const shouldExtractData = !this.IsSaved || sqlField.Dirty;
|
|
87
|
-
const shouldGenerateEmbedding = !this.IsSaved || nameField.Dirty || descriptionField.Dirty || userQuestionField.Dirty;
|
|
88
|
-
// Generate embedding from composite text for better semantic search
|
|
89
|
-
if (shouldGenerateEmbedding) {
|
|
90
|
-
await this.GenerateCompositeEmbedding();
|
|
91
|
-
}
|
|
92
|
-
else if (!this.Description || this.Description.trim().length === 0) {
|
|
93
|
-
this.EmbeddingVector = null;
|
|
94
|
-
this.EmbeddingVectorBinary = null;
|
|
95
|
-
this.EmbeddingModelID = null;
|
|
96
|
-
}
|
|
97
|
-
// Save the query first without AI processing
|
|
98
|
-
const saveResult = await super.Save(options);
|
|
99
|
-
if (!saveResult) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
// Extract and sync parameters AFTER saving, outside of any transaction
|
|
103
|
-
if (shouldExtractData && this.SQL && this.SQL.trim().length > 0) {
|
|
104
|
-
await this.extractAndSyncDataAsync(options?.SkipEntityAIActions === true);
|
|
105
|
-
await this.autoConvertDialectsAsync();
|
|
106
|
-
}
|
|
107
|
-
else if (!this.SQL || this.SQL.trim().length === 0) {
|
|
108
|
-
this.UsesTemplate = false;
|
|
109
|
-
await this.cleanupEmptyQueryAsync();
|
|
110
|
-
}
|
|
111
|
-
// The parameter and dialect rows are rewritten after the Query row, so the approved
|
|
112
|
-
// fingerprint is taken last, from the definition as stored.
|
|
113
|
-
if (approving) {
|
|
114
|
-
await this.stampApprovalFingerprintAsync(options);
|
|
115
|
-
}
|
|
116
|
-
return true;
|
|
146
|
+
return await this.saveWithExtractionAsync(options, approving);
|
|
117
147
|
}
|
|
118
148
|
catch (e) {
|
|
119
149
|
LogError('Failed to save query:', e);
|
|
120
150
|
this.LatestResult?.Errors.push(e);
|
|
121
151
|
return false;
|
|
122
152
|
}
|
|
153
|
+
finally {
|
|
154
|
+
this.ExtractionOverride = 'Auto';
|
|
155
|
+
this.platformSQLOverride = null;
|
|
156
|
+
this.presetExtraction = null;
|
|
157
|
+
this.refreshing = false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Whether this object saves through a network (client) provider rather than to a database. */
|
|
161
|
+
get savesOverNetwork() {
|
|
162
|
+
const provider = this.ProviderToUse;
|
|
163
|
+
return provider instanceof ProviderBase && provider.ProviderType === ProviderType.Network;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The save: facts first (a refusal writes nothing), then the query row (a graph save with any
|
|
167
|
+
* declared children), then the child rows extraction derives, then the approval fingerprint.
|
|
168
|
+
*/
|
|
169
|
+
async saveWithExtractionAsync(options, approving) {
|
|
170
|
+
const declared = await this.PrepareDeclaredChildrenAsync(options);
|
|
171
|
+
const plan = this.planExtraction(options, HasDeclaredQueryChildren(declared));
|
|
172
|
+
const extraction = plan.Run ? await this.computeExtractionAsync(plan.Mode) : null;
|
|
173
|
+
if (extraction?.Refusal) {
|
|
174
|
+
this.RegisterResultHistoryEntry(this.refusalResult(extraction.Refusal));
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
this.applyRunColumns(extraction);
|
|
178
|
+
if (extraction)
|
|
179
|
+
this.applyFactsToDeclaredFields(extraction);
|
|
180
|
+
await this.refreshEmbeddingAsync();
|
|
181
|
+
const postCommit = this.capturePostCommitToken();
|
|
182
|
+
this.lastWriteSummary = null;
|
|
183
|
+
if (!await super.Save(options)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (extraction) {
|
|
187
|
+
await this.writeExtractionAsync(extraction, options, postCommit);
|
|
188
|
+
// A refresh brings the derived rows in line with the SQL; it never writes SQL, a dialect variant's included.
|
|
189
|
+
if (!this.refreshing)
|
|
190
|
+
await this.autoConvertDialectsAsync();
|
|
191
|
+
}
|
|
192
|
+
else if (!this.hasSQL) {
|
|
193
|
+
await this.cleanupEmptyQueryAsync();
|
|
194
|
+
}
|
|
195
|
+
// The child and dialect rows are rewritten after the Query row, so the approved fingerprint is
|
|
196
|
+
// taken last, from the definition as stored.
|
|
197
|
+
if (approving) {
|
|
198
|
+
await this.stampApprovalFingerprintAsync(options);
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
get hasSQL() {
|
|
203
|
+
return !!this.SQL && this.SQL.trim().length > 0;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Whether this save extracts, and under which rules: a new or edited query extracts under the
|
|
207
|
+
* save-time rules; a save that writes declared child rows, or a `Force` override, re-extracts an
|
|
208
|
+
* unchanged query under the refresh rules, so the facts of rows it writes are corrected again.
|
|
209
|
+
*/
|
|
210
|
+
planExtraction(options, writesDeclaredChildren) {
|
|
211
|
+
const edited = !this.IsSaved || this.GetFieldByName('SQL').Dirty;
|
|
212
|
+
if (options?.ReplayOnly || !this.hasSQL || this.ExtractionOverride === 'Skip') {
|
|
213
|
+
return { Run: false, Mode: 'Save' };
|
|
214
|
+
}
|
|
215
|
+
return { Run: edited || writesDeclaredChildren || this.ExtractionOverride === 'Force', Mode: edited ? 'Save' : 'Refresh' };
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Sets the columns a save records about the SQL before the row is written. `UsesTemplate` is a
|
|
219
|
+
* lexical fact and follows every SQL change; it is never changed by a failed extraction. A save
|
|
220
|
+
* with {@link ExtractionOverride} `Skip` leaves it as the caller set it, like the other
|
|
221
|
+
* extraction results.
|
|
222
|
+
*/
|
|
223
|
+
applyRunColumns(extraction) {
|
|
224
|
+
if (extraction) {
|
|
225
|
+
this.UsesTemplate = extraction.UsesTemplate;
|
|
226
|
+
this.ExtractedSQLHash = extraction.Hash;
|
|
227
|
+
this.ExtractionStatus = extraction.Status;
|
|
228
|
+
this.ExtractionMessage = extraction.Messages.length > 0 ? extraction.Messages.join('\n') : null;
|
|
229
|
+
}
|
|
230
|
+
else if (!this.hasSQL) {
|
|
231
|
+
this.UsesTemplate = false;
|
|
232
|
+
}
|
|
233
|
+
else if (this.ExtractionOverride !== 'Skip' && (!this.IsSaved || this.GetFieldByName('SQL').Dirty)) {
|
|
234
|
+
this.UsesTemplate = this.lexicalUsesTemplate();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Gives each field row this save declares the facts extraction established for it, before the
|
|
239
|
+
* graph is written. Facts follow the SQL and the database server whatever a caller declares, so
|
|
240
|
+
* a declaration that carries facts (types, nullability, sequence, source) for another platform is
|
|
241
|
+
* corrected before it is written instead of being written and then corrected, and a new declared
|
|
242
|
+
* row, which may declare only its name and annotations, gets the facts its columns require.
|
|
243
|
+
*/
|
|
244
|
+
applyFactsToDeclaredFields(extraction) {
|
|
245
|
+
if (!extraction.Fields)
|
|
246
|
+
return;
|
|
247
|
+
const facts = new Map(extraction.Fields.map(f => [f.Name.toLowerCase(), f]));
|
|
248
|
+
for (const row of this.DeclaredFields?.Items ?? []) {
|
|
249
|
+
const fact = facts.get((row.Name ?? '').toLowerCase());
|
|
250
|
+
if (fact)
|
|
251
|
+
ApplyFieldFacts(row, fact);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Whether the SQL holds a template tag outside comments, read by the lexer. */
|
|
255
|
+
lexicalUsesTemplate() {
|
|
256
|
+
return SQLUsesTemplate(this.SQL ?? '', this.extractionPlatform);
|
|
257
|
+
}
|
|
258
|
+
/** The save's result entry for a refused save, carrying the reasons. */
|
|
259
|
+
refusalResult(reason) {
|
|
260
|
+
const result = new BaseEntityResult();
|
|
261
|
+
result.Success = false;
|
|
262
|
+
result.Type = this.IsSaved ? 'update' : 'create';
|
|
263
|
+
result.Message = reason;
|
|
264
|
+
result.StartedAt = new Date();
|
|
265
|
+
result.EndedAt = new Date();
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The declared-rows-win hook, run before the save writes anything.
|
|
270
|
+
*
|
|
271
|
+
* A caller can save a query together with child rows it declares, through the `Parameters`,
|
|
272
|
+
* `Fields` and `Entities` collections. When a NEW declared row has the key of a stored row with a
|
|
273
|
+
* different ID whose `DetectionMethod` is not `Manual` (a row extraction created), the stored row
|
|
274
|
+
* is deleted inside the same graph save, before the declared row is inserted. The declared row
|
|
275
|
+
* keeps the ID the caller gave it, which is what makes a metadata file that names its child IDs
|
|
276
|
+
* converge instead of colliding with rows extraction wrote earlier. A stored `Manual` row is never
|
|
277
|
+
* deleted here; a declared row that collides with one fails the save on the database's own
|
|
278
|
+
* uniqueness rule where there is one.
|
|
279
|
+
*
|
|
280
|
+
* Extraction then runs after `super.Save()`, sees the declared rows already written, and treats
|
|
281
|
+
* them like any stored row: their facts are corrected from the SQL, their authored annotations
|
|
282
|
+
* kept.
|
|
283
|
+
*
|
|
284
|
+
* @param options - The save's options. A `ReplayOnly` save, or one that skips related
|
|
285
|
+
* collections, writes no declared rows, so nothing is declared.
|
|
286
|
+
* @returns The keys of the child rows this save declares.
|
|
287
|
+
*/
|
|
288
|
+
async PrepareDeclaredChildrenAsync(options) {
|
|
289
|
+
if (options?.ReplayOnly || options?.SkipRelatedCollections) {
|
|
290
|
+
return NO_DECLARED_QUERY_CHILDREN;
|
|
291
|
+
}
|
|
292
|
+
const collections = { Parameters: this.DeclaredParameters, Fields: this.DeclaredFields, Entities: this.DeclaredEntities };
|
|
293
|
+
const declared = CollectDeclaredQueryChildren(collections);
|
|
294
|
+
if (this.IsSaved && HasDeclaredQueryChildren(declared)) {
|
|
295
|
+
await this.queueReplacedRowDeletionsAsync();
|
|
296
|
+
}
|
|
297
|
+
return declared;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Reads the stored child rows from the database (never a cache, which inside a transaction can
|
|
301
|
+
* miss rows written earlier in it) and queues the extraction-created rows each new declared row
|
|
302
|
+
* replaces for deletion in the graph save.
|
|
303
|
+
*/
|
|
304
|
+
async queueReplacedRowDeletionsAsync() {
|
|
305
|
+
const filter = `QueryID='${EscapeSQLString(this.ID)}'`;
|
|
306
|
+
const [parameters, fields, entities] = await this.RunViewProviderToUse.RunViews([
|
|
307
|
+
{ EntityName: 'MJ: Query Parameters', ExtraFilter: filter, ResultType: 'entity_object', BypassCache: true, IgnoreMaxRows: true },
|
|
308
|
+
{ EntityName: 'MJ: Query Fields', ExtraFilter: filter, ResultType: 'entity_object', BypassCache: true, IgnoreMaxRows: true },
|
|
309
|
+
{ EntityName: 'MJ: Query Entities', ExtraFilter: filter, ResultType: 'entity_object', BypassCache: true, IgnoreMaxRows: true },
|
|
310
|
+
], this.ContextCurrentUser);
|
|
311
|
+
if (!parameters?.Success || !fields?.Success || !entities?.Success) {
|
|
312
|
+
throw new Error(`could not read the query's stored child rows: ${parameters?.ErrorMessage || fields?.ErrorMessage || entities?.ErrorMessage}`);
|
|
313
|
+
}
|
|
314
|
+
const newItems = (items) => items.filter(i => !i.IsSaved);
|
|
315
|
+
const byName = (row) => QueryChildNameKey(row.Name);
|
|
316
|
+
for (const row of FindReplacedRows(newItems(this.DeclaredParameters.Items), parameters.Results, byName)) {
|
|
317
|
+
QueueReplacedRowDeletion(this.DeclaredParameters, row);
|
|
318
|
+
}
|
|
319
|
+
for (const row of FindReplacedRows(newItems(this.DeclaredFields.Items), fields.Results, byName)) {
|
|
320
|
+
QueueReplacedRowDeletion(this.DeclaredFields, row);
|
|
321
|
+
}
|
|
322
|
+
for (const row of FindReplacedRows(newItems(this.DeclaredEntities.Items), entities.Results, QueryEntityKey)) {
|
|
323
|
+
QueueReplacedRowDeletion(this.DeclaredEntities, row);
|
|
324
|
+
}
|
|
123
325
|
}
|
|
124
326
|
/**
|
|
125
327
|
* Stores the fingerprint of the definition just approved: this row's SQL, template flag and
|
|
@@ -151,11 +353,7 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
|
|
|
151
353
|
}
|
|
152
354
|
async Delete(options) {
|
|
153
355
|
try {
|
|
154
|
-
|
|
155
|
-
if (!deleteResult) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
return deleteResult;
|
|
356
|
+
return await super.Delete(options);
|
|
159
357
|
}
|
|
160
358
|
catch (e) {
|
|
161
359
|
LogError('Failed to delete query:', e);
|
|
@@ -163,131 +361,251 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
|
|
|
163
361
|
return false;
|
|
164
362
|
}
|
|
165
363
|
}
|
|
166
|
-
// ───
|
|
364
|
+
// ─── Extraction ──────────────────────────────────────────────────────────────
|
|
167
365
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
366
|
+
* The platform the query is extracted for: the platform of the provider this entity saves
|
|
367
|
+
* through, so two providers on different platforms in one process each extract for their own.
|
|
170
368
|
*/
|
|
171
|
-
get
|
|
172
|
-
|
|
369
|
+
get extractionPlatform() {
|
|
370
|
+
const provider = this.ProviderToUse;
|
|
371
|
+
return provider instanceof DatabaseProviderBase ? provider.PlatformKey : 'sqlserver';
|
|
372
|
+
}
|
|
373
|
+
/** The provider as extraction reads and writes through it; `null` when it cannot read views and create entity objects. */
|
|
374
|
+
get extractionProvider() {
|
|
375
|
+
const provider = this.ProviderToUse;
|
|
376
|
+
return IsExtractionProvider(provider) ? provider : null;
|
|
173
377
|
}
|
|
174
378
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* 1. MJ: Query SQLs record matching the current platform's dialect → use that SQL
|
|
179
|
-
* 2. Base Query.SQL field → fall back if no dialect variant exists
|
|
180
|
-
*
|
|
181
|
-
* After resolution, validates the SQL is compatible with the current platform's
|
|
182
|
-
* dialect by attempting a parse. Logs a warning if the SQL appears to be for a
|
|
183
|
-
* different dialect (e.g., T-SQL bracket identifiers on a PostgreSQL database).
|
|
379
|
+
* The user derived child rows are read and written as: the system user, once the caller's save
|
|
380
|
+
* of the query is authorized (the rows mirror SQL the caller was allowed to change, like CodeGen
|
|
381
|
+
* output). The caller when no system user is loaded.
|
|
184
382
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
383
|
+
get extractionUser() {
|
|
384
|
+
if (this.refreshing)
|
|
385
|
+
return this.ContextCurrentUser;
|
|
386
|
+
return UserCache.Instance?.GetSystemUser?.() ?? this.ContextCurrentUser;
|
|
387
|
+
}
|
|
388
|
+
/** The SQL extraction reads: a re-extraction's override, else the query's SQL for the platform. */
|
|
389
|
+
get extractionSQL() {
|
|
390
|
+
return this.platformSQLOverride ?? (this.GetPlatformSQL(this.extractionPlatform) || this.SQL || '');
|
|
391
|
+
}
|
|
392
|
+
/** Establishes the query's facts (nothing is written); an explicit refresh's facts when it already has them. */
|
|
393
|
+
async computeExtractionAsync(mode) {
|
|
394
|
+
if (this.presetExtraction)
|
|
395
|
+
return this.presetExtraction;
|
|
396
|
+
const provider = this.ProviderToUse;
|
|
397
|
+
await this.ensureQueryEngineLoadedAsync();
|
|
398
|
+
const targets = await this.readCompositionTargetsAsync();
|
|
399
|
+
return ComputeQueryExtraction({
|
|
400
|
+
QueryID: this.IsSaved ? this.ID : null,
|
|
401
|
+
QueryName: this.Name,
|
|
402
|
+
QueryPath: CompositionFullPath({ Name: this.Name, CategoryPath: this.CategoryID ? this.BuildCategoryPath(this.CategoryID) : '' }),
|
|
403
|
+
SQL: this.extractionSQL,
|
|
404
|
+
Platform: this.extractionPlatform,
|
|
405
|
+
Mode: mode,
|
|
406
|
+
IsExternal: !!this.ExternalDataSourceID,
|
|
407
|
+
KnownParameters: await this.knownParametersAsync(),
|
|
408
|
+
ParameterHints: this.ParameterHints,
|
|
409
|
+
ContextUser: this.ContextCurrentUser,
|
|
410
|
+
Entities: provider instanceof DatabaseProviderBase ? provider.Entities : [],
|
|
411
|
+
Queries: MergeCompositionCandidates(targets.Candidates, QueryEngine.Instance.Queries),
|
|
412
|
+
CompositionDependencies: targets.InlineDependencies,
|
|
413
|
+
Describe: provider instanceof GenericDatabaseProvider ? (sql) => provider.DescribeStatement(sql, this.ContextCurrentUser) : null,
|
|
414
|
+
});
|
|
197
415
|
}
|
|
198
416
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* dialect-specific functions (ISNULL, GETDATE pass in both dialects), so a passing
|
|
204
|
-
* parse does not prove the SQL is correct for this dialect. But a failing parse is a
|
|
205
|
-
* strong signal of incompatibility.
|
|
417
|
+
* The queries the SQL composes, read on this save's provider with every cache bypassed, so a
|
|
418
|
+
* target saved earlier in the same transaction (one metadata push creating both queries) is
|
|
419
|
+
* found before the shared query cache hears of it. Nothing is read for SQL that composes
|
|
420
|
+
* nothing, or on a provider that cannot run views.
|
|
206
421
|
*/
|
|
207
|
-
|
|
422
|
+
async readCompositionTargetsAsync() {
|
|
423
|
+
const provider = this.ProviderToUse;
|
|
424
|
+
if (!(provider instanceof DatabaseProviderBase))
|
|
425
|
+
return { Candidates: [], InlineDependencies: [] };
|
|
426
|
+
const engine = QueryEngine.Instance;
|
|
427
|
+
const cachedIDs = new Set(engine.Queries.map(q => q.ID.toLowerCase()));
|
|
428
|
+
return ReadCompositionTargetsAsync(this.extractionSQL, this.extractionPlatform, provider, this.extractionUser, cachedIDs, engine.SQLDialects);
|
|
429
|
+
}
|
|
430
|
+
/** Loads QueryEngine when this process has not yet (a CLI or sync context); compositions resolve against it. */
|
|
431
|
+
async ensureQueryEngineLoadedAsync() {
|
|
432
|
+
if (QueryEngine.Instance.Loaded)
|
|
433
|
+
return;
|
|
208
434
|
try {
|
|
209
|
-
|
|
210
|
-
// Attempt to parse — will throw on strong dialect markers
|
|
211
|
-
// (e.g., [bracket] identifiers fail on PG, :: casts fail on T-SQL)
|
|
212
|
-
SQLParser.ExtractSelectColumns(sql, dialect);
|
|
435
|
+
await QueryEngine.Instance.Config(false, this.ContextCurrentUser);
|
|
213
436
|
}
|
|
214
|
-
catch {
|
|
215
|
-
|
|
216
|
-
console.warn(`[MJQueryEntityServer] Query "${this.Name}" — SQL may be incompatible with the ` +
|
|
217
|
-
`current database platform "${platform}". The SQL appears to use ${otherDialect} syntax. ` +
|
|
218
|
-
`Consider adding a dialect-specific variant via MJ: Query SQLs.`);
|
|
437
|
+
catch (e) {
|
|
438
|
+
LogError(`[MJQueryEntityServer] QueryEngine could not be loaded for extraction of "${this.Name}": ${e instanceof Error ? e.message : String(e)}`);
|
|
219
439
|
}
|
|
220
440
|
}
|
|
221
441
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
442
|
+
* The parameter rows extraction knows before it runs: the stored rows, read from the database
|
|
443
|
+
* (never the cache), overlaid by the rows this save declares.
|
|
224
444
|
*/
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
445
|
+
async knownParametersAsync() {
|
|
446
|
+
const byName = new Map();
|
|
447
|
+
for (const row of await this.storedParametersAsync())
|
|
448
|
+
byName.set(row.Name.toLowerCase(), row);
|
|
449
|
+
for (const row of this.DeclaredParameters?.Items ?? [])
|
|
450
|
+
byName.set(row.Name.toLowerCase(), row);
|
|
451
|
+
return [...byName.values()].map(row => ({
|
|
452
|
+
Name: row.Name, Type: row.Type, IsRequired: row.IsRequired === true, IsServerSet: row.IsServerSet === true,
|
|
453
|
+
SampleValue: row.SampleValue, DefaultValue: row.DefaultValue, ValidationFilters: row.ValidationFilters,
|
|
454
|
+
IsAuthored: row.DetectionMethod === 'Manual',
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
async storedParametersAsync() {
|
|
458
|
+
const provider = this.extractionProvider;
|
|
459
|
+
if (!this.IsSaved || !provider)
|
|
460
|
+
return [];
|
|
461
|
+
const result = await provider.RunView({
|
|
462
|
+
EntityName: 'MJ: Query Parameters', ExtraFilter: `QueryID='${EscapeSQLString(this.ID)}'`, ResultType: 'entity_object', BypassCache: true,
|
|
463
|
+
}, this.extractionUser);
|
|
464
|
+
if (!result.Success)
|
|
465
|
+
throw new Error(`could not read the query's stored parameters: ${result.ErrorMessage}`);
|
|
466
|
+
return result.Results ?? [];
|
|
238
467
|
}
|
|
239
468
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
|
|
469
|
+
* Writes the derived child rows as the system user, inside the caller's transaction when there
|
|
470
|
+
* is one, then queues the description pass. A failed write is recorded on the query
|
|
471
|
+
* (`ExtractionStatus` `Error`) rather than failing a save whose row is already written.
|
|
472
|
+
*/
|
|
473
|
+
async writeExtractionAsync(extraction, options, postCommit) {
|
|
474
|
+
const provider = this.extractionProvider;
|
|
475
|
+
try {
|
|
476
|
+
if (!provider)
|
|
477
|
+
throw new Error('the query is not saved through a database provider');
|
|
478
|
+
const summary = await WriteExtractedChildRows({ QueryID: this.ID, Provider: provider, User: this.extractionUser, EntityName: id => this.entityNameOf(id) }, extraction);
|
|
479
|
+
this.lastWriteSummary = summary;
|
|
480
|
+
this.queueDescriptionPass(summary, options, postCommit);
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
// An explicit refresh runs in its own transaction, which a failed write rolls back.
|
|
484
|
+
if (this.refreshing)
|
|
485
|
+
throw e;
|
|
486
|
+
await this.recordWriteFailureAsync(e);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** An entity's name from the saving provider's catalog, for reporting; the ID when it is not there. */
|
|
490
|
+
entityNameOf(entityID) {
|
|
491
|
+
const provider = this.ProviderToUse;
|
|
492
|
+
const entities = provider instanceof DatabaseProviderBase ? provider.Entities : [];
|
|
493
|
+
return entities.find(e => UUIDsEqual(e.ID, entityID))?.Name ?? entityID;
|
|
494
|
+
}
|
|
495
|
+
async recordWriteFailureAsync(error) {
|
|
496
|
+
const reason = `The extracted child rows could not be written: ${error instanceof Error ? error.message : String(error)}`;
|
|
497
|
+
LogError(`[MJQueryEntityServer] Query "${this.Name}": ${reason}`);
|
|
498
|
+
this.ExtractionStatus = 'Error';
|
|
499
|
+
this.ExtractionMessage = this.ExtractionMessage ? `${this.ExtractionMessage}\n${reason}` : reason;
|
|
500
|
+
if (!await super.Save()) {
|
|
501
|
+
LogError(`[MJQueryEntityServer] Query "${this.Name}": the extraction failure could not be recorded: ${this.LatestResult?.CompleteMessage}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/** The post-commit token of the caller's transaction, captured before the save writes anything. */
|
|
505
|
+
capturePostCommitToken() {
|
|
506
|
+
const provider = this.ProviderToUse;
|
|
507
|
+
return provider instanceof DatabaseProviderBase ? provider.CapturePostCommitToken() : undefined;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Queues the LLM description pass to run after the caller's transaction commits, when some row
|
|
511
|
+
* has no description and the save does not skip AI actions. No LLM call is made inside the
|
|
512
|
+
* transaction. A save-triggered extraction that changed no child row does not queue it: the
|
|
513
|
+
* rows are as the last run left them, so a repeated save (a re-push of unchanged metadata) does
|
|
514
|
+
* not call the LLM again, and a pass that failed is not retried, and recorded, on every save.
|
|
515
|
+
* An explicit refresh that asks for the pass always queues it.
|
|
516
|
+
*/
|
|
517
|
+
queueDescriptionPass(summary, options, postCommit) {
|
|
518
|
+
const provider = this.ProviderToUse;
|
|
519
|
+
const undescribed = summary.FieldsWithoutDescription.length + summary.ParametersWithoutDescription.length;
|
|
520
|
+
if (options?.SkipEntityAIActions || undescribed === 0 || !(provider instanceof DatabaseProviderBase))
|
|
521
|
+
return;
|
|
522
|
+
if (!this.refreshing && summary.Changes.length === 0)
|
|
523
|
+
return;
|
|
524
|
+
const request = { QueryID: this.ID, QueryName: this.Name, SQL: this.extractionSQL, Provider: provider, User: this.extractionUser };
|
|
525
|
+
provider.RunAfterCommit(() => RunDescriptionPass(request), `query description pass for "${this.Name}"`, postCommit);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Re-extracts this query: a save with {@link ExtractionOverride} `Force`. Called by
|
|
529
|
+
* `MJQuerySQLEntityServer` when a dialect variant for the current platform is saved.
|
|
244
530
|
*
|
|
245
|
-
* @param skipAIEnrichment Skip the
|
|
531
|
+
* @param skipAIEnrichment Skip the description pass, as `SkipEntityAIActions` does for a save.
|
|
532
|
+
* @param platformSQL The SQL to extract from (the variant just saved), instead of the cached
|
|
533
|
+
* platform variant, which inside a transaction may not hold it yet.
|
|
534
|
+
*/
|
|
535
|
+
async RerunExtraction(skipAIEnrichment = false, platformSQL) {
|
|
536
|
+
this.ExtractionOverride = 'Force';
|
|
537
|
+
this.platformSQLOverride = platformSQL ?? null;
|
|
538
|
+
if (!await this.Save({ SkipEntityAIActions: skipAIEnrichment })) {
|
|
539
|
+
LogError(`[MJQueryEntityServer] Re-extraction of query "${this.Name}" failed: ${this.LatestResult?.CompleteMessage}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// ─── Explicit refresh ────────────────────────────────────────────────────────
|
|
543
|
+
/**
|
|
544
|
+
* Establishes this saved query's facts under the refresh rules (a save-time rule error is
|
|
545
|
+
* recorded, never a refusal), as the save would, and writes nothing. The facts are read on the
|
|
546
|
+
* provider this entity was loaded with, for its platform.
|
|
246
547
|
*/
|
|
247
|
-
async
|
|
248
|
-
|
|
548
|
+
async ComputeRefreshExtractionAsync() {
|
|
549
|
+
return this.computeExtractionAsync('Refresh');
|
|
249
550
|
}
|
|
250
551
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
552
|
+
* The child-row changes `extraction` would make, worked out against the rows stored now and
|
|
553
|
+
* read as this entity's context user. Nothing is written.
|
|
554
|
+
*/
|
|
555
|
+
async PreviewRefreshAsync(extraction, entityName) {
|
|
556
|
+
const provider = this.extractionProvider;
|
|
557
|
+
if (!provider)
|
|
558
|
+
throw new Error('the query is not loaded through a database provider');
|
|
559
|
+
return WriteExtractedChildRows({ QueryID: this.ID, Provider: provider, User: this.ContextCurrentUser, DryRun: true, EntityName: entityName ?? (id => this.entityNameOf(id)) }, extraction);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Writes `extraction` (from {@link ComputeRefreshExtractionAsync}): a save with
|
|
563
|
+
* {@link ExtractionOverride} `Force` that records the run on the query and brings the child
|
|
564
|
+
* rows in line with the facts. Unlike a save-triggered extraction, the child rows are written
|
|
565
|
+
* as this entity's context user, so each write is checked against that user's permissions, and
|
|
566
|
+
* a failed write fails the refresh instead of being recorded on the query. SQL, dialect
|
|
567
|
+
* variants and the embedding are never written.
|
|
253
568
|
*
|
|
254
|
-
* @param
|
|
569
|
+
* @param withAI Queue the LLM description pass for rows still without a description.
|
|
570
|
+
* @returns What the child-row writes changed.
|
|
571
|
+
* @throws Error when the save or a child-row write fails; the caller's transaction should be rolled back.
|
|
255
572
|
*/
|
|
256
|
-
async
|
|
573
|
+
async ApplyRefreshAsync(extraction, withAI) {
|
|
574
|
+
this.ExtractionOverride = 'Force';
|
|
575
|
+
this.presetExtraction = extraction;
|
|
576
|
+
this.refreshing = true;
|
|
577
|
+
// A refresh changes the derived rows, not the text the embedding is computed from.
|
|
578
|
+
const skipEmbeddings = this.SkipEmbeddings;
|
|
579
|
+
this.SkipEmbeddings = true;
|
|
257
580
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
this.UsesTemplate = pipelineResult.UsesTemplate;
|
|
261
|
-
// Save the query again to persist UsesTemplate and any changes from AI processing
|
|
262
|
-
const updateResult = await super.Save();
|
|
263
|
-
if (!updateResult) {
|
|
264
|
-
LogError('Failed to save query after AI processing completed');
|
|
581
|
+
if (!await this.Save({ SkipEntityAIActions: !withAI })) {
|
|
582
|
+
throw new Error(this.LatestResult?.CompleteMessage || `the refresh of query "${this.Name}" could not be saved`);
|
|
265
583
|
}
|
|
266
584
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.UsesTemplate = false;
|
|
270
|
-
try {
|
|
271
|
-
await super.Save();
|
|
272
|
-
}
|
|
273
|
-
catch (saveError) {
|
|
274
|
-
LogError('Failed to save query after AI processing error:', saveError);
|
|
275
|
-
}
|
|
585
|
+
finally {
|
|
586
|
+
this.SkipEmbeddings = skipEmbeddings;
|
|
276
587
|
}
|
|
588
|
+
return this.lastWriteSummary;
|
|
277
589
|
}
|
|
278
590
|
/**
|
|
279
|
-
* Cleans up all extraction data when SQL is empty
|
|
591
|
+
* Cleans up all extraction data when SQL is empty.
|
|
280
592
|
*/
|
|
281
593
|
async cleanupEmptyQueryAsync() {
|
|
594
|
+
const provider = this.extractionProvider;
|
|
595
|
+
if (!provider)
|
|
596
|
+
return;
|
|
282
597
|
try {
|
|
283
|
-
const ctx =
|
|
598
|
+
const ctx = {
|
|
599
|
+
queryID: this.ID,
|
|
600
|
+
queryName: this.Name,
|
|
601
|
+
sql: '',
|
|
602
|
+
isSaved: this.IsSaved,
|
|
603
|
+
contextUser: this.extractionUser,
|
|
604
|
+
metadataProvider: provider,
|
|
605
|
+
runViewProvider: provider,
|
|
606
|
+
platform: this.extractionPlatform,
|
|
607
|
+
};
|
|
284
608
|
await CleanupQueryData(ctx);
|
|
285
|
-
// Also remove dialect records (not managed by the pipeline)
|
|
286
|
-
await this.removeAllQuerySQLRecordsAsync();
|
|
287
|
-
const updateResult = await super.Save();
|
|
288
|
-
if (!updateResult) {
|
|
289
|
-
LogError('Failed to save query after cleanup');
|
|
290
|
-
}
|
|
291
609
|
}
|
|
292
610
|
catch (e) {
|
|
293
611
|
LogError('Error in async cleanup:', e);
|
|
@@ -352,9 +670,23 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
|
|
|
352
670
|
* Creates or updates a QuerySQL record for the given dialect.
|
|
353
671
|
*/
|
|
354
672
|
async upsertQuerySQLRecord(targetDialect, convertedSQL) {
|
|
355
|
-
const md = this.
|
|
356
|
-
|
|
357
|
-
|
|
673
|
+
const md = this.extractionProvider;
|
|
674
|
+
if (!md)
|
|
675
|
+
return;
|
|
676
|
+
// Read through this entity's own provider rather than QueryEngine's cache. A cached record is
|
|
677
|
+
// bound to whichever provider loaded the engine, so saving it here would write outside the
|
|
678
|
+
// caller's transaction, blind to what that transaction has written.
|
|
679
|
+
const existingResult = await md.RunView({
|
|
680
|
+
EntityName: 'MJ: Query SQLs',
|
|
681
|
+
ExtraFilter: `QueryID='${EscapeSQLString(this.ID)}' AND SQLDialectID='${EscapeSQLString(targetDialect.ID)}'`,
|
|
682
|
+
ResultType: 'entity_object',
|
|
683
|
+
BypassCache: true,
|
|
684
|
+
}, this.ContextCurrentUser);
|
|
685
|
+
if (!existingResult.Success) {
|
|
686
|
+
console.warn(`Query "${this.Name}" - Could not read QuerySQL record for dialect "${targetDialect.Name}": ${existingResult.ErrorMessage}`);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const existing = existingResult.Results?.[0];
|
|
358
690
|
let record;
|
|
359
691
|
if (existing) {
|
|
360
692
|
record = existing;
|
|
@@ -370,23 +702,6 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
|
|
|
370
702
|
console.warn(`Query "${this.Name}" - Failed to save QuerySQL record for dialect "${targetDialect.Name}"`);
|
|
371
703
|
}
|
|
372
704
|
}
|
|
373
|
-
/**
|
|
374
|
-
* Removes all QuerySQL records for this query. Called during cleanup operations.
|
|
375
|
-
*/
|
|
376
|
-
async removeAllQuerySQLRecordsAsync() {
|
|
377
|
-
try {
|
|
378
|
-
if (!this.IsSaved)
|
|
379
|
-
return;
|
|
380
|
-
const records = QueryEngine.Instance.QuerySQLs.filter(qs => UUIDsEqual(qs.QueryID, this.ID));
|
|
381
|
-
const deletePromises = records.map(r => r.Delete());
|
|
382
|
-
if (deletePromises.length > 0) {
|
|
383
|
-
await Promise.all(deletePromises);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
catch (e) {
|
|
387
|
-
console.warn(`Query "${this.Name}" - Failed to remove QuerySQL records:`, e);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
705
|
};
|
|
391
706
|
MJQueryEntityServer = __decorate([
|
|
392
707
|
RegisterClass(BaseEntity, 'MJ: Queries')
|