@memberjunction/server 5.34.1 → 5.36.0

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 (41) hide show
  1. package/dist/generated/generated.d.ts +49 -4
  2. package/dist/generated/generated.d.ts.map +1 -1
  3. package/dist/generated/generated.js +236 -14
  4. package/dist/generated/generated.js.map +1 -1
  5. package/dist/generic/ResolverBase.d.ts +1 -1
  6. package/dist/generic/ResolverBase.d.ts.map +1 -1
  7. package/dist/generic/ResolverBase.js +7 -4
  8. package/dist/generic/ResolverBase.js.map +1 -1
  9. package/dist/generic/RunViewResolver.d.ts +4 -0
  10. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  11. package/dist/generic/RunViewResolver.js +28 -0
  12. package/dist/generic/RunViewResolver.js.map +1 -1
  13. package/dist/resolvers/AdhocQueryResolver.d.ts +2 -0
  14. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  15. package/dist/resolvers/AdhocQueryResolver.js +42 -8
  16. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  17. package/dist/resolvers/ListOperationsResolver.d.ts +175 -0
  18. package/dist/resolvers/ListOperationsResolver.d.ts.map +1 -0
  19. package/dist/resolvers/ListOperationsResolver.js +930 -0
  20. package/dist/resolvers/ListOperationsResolver.js.map +1 -0
  21. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  22. package/dist/resolvers/RunAIAgentResolver.js +111 -58
  23. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  24. package/dist/resolvers/artifact-routing.d.ts +39 -0
  25. package/dist/resolvers/artifact-routing.d.ts.map +1 -0
  26. package/dist/resolvers/artifact-routing.js +40 -0
  27. package/dist/resolvers/artifact-routing.js.map +1 -0
  28. package/dist/types.d.ts +6 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/types.js.map +1 -1
  31. package/package.json +70 -68
  32. package/src/__tests__/ListOperationsResolver.test.ts +182 -0
  33. package/src/generated/generated.ts +173 -14
  34. package/src/generic/ResolverBase.ts +13 -4
  35. package/src/generic/RunViewResolver.ts +28 -0
  36. package/src/resolvers/AdhocQueryResolver.ts +41 -8
  37. package/src/resolvers/ListOperationsResolver.ts +607 -0
  38. package/src/resolvers/RunAIAgentResolver.ts +119 -61
  39. package/src/resolvers/__tests__/artifact-routing.test.ts +88 -0
  40. package/src/resolvers/artifact-routing.ts +80 -0
  41. package/src/types.ts +6 -0
@@ -7094,7 +7094,7 @@ export class MJAIAgentRunStep_ {
7094
7094
  @Field(() => Int, {description: `Sequential number of this step within the agent run, starting from 1`})
7095
7095
  StepNumber: number;
7096
7096
 
7097
- @Field({description: `Type of execution step: Prompt, Actions, Sub-Agent, Decision, Chat, Validation`})
7097
+ @Field({description: `Type of execution step: Prompt, Actions, Sub-Agent, Decision, Chat, Validation, ForEach, While, Tool`})
7098
7098
  @MaxLength(50)
7099
7099
  StepType: string;
7100
7100
 
@@ -9453,6 +9453,9 @@ if this limit is exceeded.`})
9453
9453
  @MaxLength(20)
9454
9454
  SearchScopeAccess: string;
9455
9455
 
9456
+ @Field(() => Boolean, {description: `Per-agent opt-in to a Generic Binary fallback for file uploads whose MIME type does not match any registered Artifact Type. When false (default), unrecognized uploads are rejected at upload time with an actionable error. When true, unrecognized uploads resolve to the Generic Binary artifact type, exposing only get_full and get_metadata tools. Scoped per agent — there is no system-wide global flag.`})
9457
+ AcceptUnregisteredFiles: boolean;
9458
+
9456
9459
  @Field({nullable: true})
9457
9460
  @MaxLength(255)
9458
9461
  Parent?: string;
@@ -9763,6 +9766,9 @@ export class CreateMJAIAgentInput {
9763
9766
  @Field({ nullable: true })
9764
9767
  SearchScopeAccess?: string;
9765
9768
 
9769
+ @Field(() => Boolean, { nullable: true })
9770
+ AcceptUnregisteredFiles?: boolean;
9771
+
9766
9772
  @Field(() => RestoreContextInput, { nullable: true })
9767
9773
  RestoreContext___?: RestoreContextInput;
9768
9774
  }
@@ -9962,6 +9968,9 @@ export class UpdateMJAIAgentInput {
9962
9968
  @Field({ nullable: true })
9963
9969
  SearchScopeAccess?: string;
9964
9970
 
9971
+ @Field(() => Boolean, { nullable: true })
9972
+ AcceptUnregisteredFiles?: boolean;
9973
+
9965
9974
  @Field(() => [KeyValuePairInput], { nullable: true })
9966
9975
  OldValues___?: KeyValuePairInput[];
9967
9976
 
@@ -21569,6 +21578,16 @@ export class MJArtifactType_ {
21569
21578
  @MaxLength(100)
21570
21579
  ToolLibraryClass?: string;
21571
21580
 
21581
+ @Field(() => Int, {description: `Deterministic tiebreaker when multiple Artifact Types match the same MIME pattern. Higher values win. Within a specificity tier (exact > subtype-wildcard), the resolver sorts by Priority desc, then SystemSupplied = false beats SystemSupplied = true, then lowest ID wins.`})
21582
+ Priority: number;
21583
+
21584
+ @Field({description: `How artifacts of this type are delivered to the LLM by default. Inline: emitted as an inline content block (image_url, audio_url, small text, etc.) when the model supports the modality and the size is under the inline cap. ToolsOnly: never inlined; the agent reaches the bytes only through tool calls (get_full, library-specific tools). Per-instance override is one-way via ConversationArtifactVersion.ForceToolsOnly — an instance can opt out of inline but never opt in when the type default is ToolsOnly.`})
21585
+ @MaxLength(20)
21586
+ DefaultDeliveryMode: string;
21587
+
21588
+ @Field(() => Boolean, {description: `True for Artifact Types shipped as part of the MemberJunction default registry (JSON, PDF, Office variants, Image/Audio/Video, Generic Text, Generic Binary). False for user/org-supplied customizations. Used as a tiebreaker in MIME pattern resolution: user customizations win over shipped defaults at equal Priority.`})
21589
+ SystemSupplied: boolean;
21590
+
21572
21591
  @Field({nullable: true})
21573
21592
  @MaxLength(100)
21574
21593
  Parent?: string;
@@ -21632,6 +21651,15 @@ export class CreateMJArtifactTypeInput {
21632
21651
  @Field({ nullable: true })
21633
21652
  ToolLibraryClass: string | null;
21634
21653
 
21654
+ @Field(() => Int, { nullable: true })
21655
+ Priority?: number;
21656
+
21657
+ @Field({ nullable: true })
21658
+ DefaultDeliveryMode?: string;
21659
+
21660
+ @Field(() => Boolean, { nullable: true })
21661
+ SystemSupplied?: boolean;
21662
+
21635
21663
  @Field(() => RestoreContextInput, { nullable: true })
21636
21664
  RestoreContext___?: RestoreContextInput;
21637
21665
  }
@@ -21675,6 +21703,15 @@ export class UpdateMJArtifactTypeInput {
21675
21703
  @Field({ nullable: true })
21676
21704
  ToolLibraryClass?: string | null;
21677
21705
 
21706
+ @Field(() => Int, { nullable: true })
21707
+ Priority?: number;
21708
+
21709
+ @Field({ nullable: true })
21710
+ DefaultDeliveryMode?: string;
21711
+
21712
+ @Field(() => Boolean, { nullable: true })
21713
+ SystemSupplied?: boolean;
21714
+
21678
21715
  @Field(() => [KeyValuePairInput], { nullable: true })
21679
21716
  OldValues___?: KeyValuePairInput[];
21680
21717
 
@@ -21848,9 +21885,8 @@ export class MJArtifactUse_ {
21848
21885
  @Field()
21849
21886
  _mj__UpdatedAt: Date;
21850
21887
 
21851
- @Field({nullable: true})
21852
- @MaxLength(255)
21853
- ArtifactVersion?: string;
21888
+ @Field(() => Int)
21889
+ ArtifactVersion: number;
21854
21890
 
21855
21891
  @Field()
21856
21892
  @MaxLength(100)
@@ -22030,9 +22066,8 @@ export class MJArtifactVersionAttribute_ {
22030
22066
  @Field()
22031
22067
  _mj__UpdatedAt: Date;
22032
22068
 
22033
- @Field({nullable: true})
22034
- @MaxLength(255)
22035
- ArtifactVersion?: string;
22069
+ @Field(() => Int)
22070
+ ArtifactVersion: number;
22036
22071
 
22037
22072
  }
22038
22073
 
@@ -22245,6 +22280,9 @@ export class MJArtifactVersion_ {
22245
22280
  @Field(() => Int, {nullable: true, description: `Size of the stored file in bytes. Denormalized for display without loading the file. Only populated when ContentMode is 'File'.`})
22246
22281
  ContentSizeBytes?: number;
22247
22282
 
22283
+ @Field(() => Boolean, {description: `One-way override that forces this artifact version to be delivered via tools regardless of the Artifact Type's DefaultDeliveryMode. When true, the resolver never emits an inline content block for this version. There is no inverse override — an instance cannot be widened from ToolsOnly to Inline. Default false.`})
22284
+ ForceToolsOnly: boolean;
22285
+
22248
22286
  @Field()
22249
22287
  @MaxLength(255)
22250
22288
  Artifact: string;
@@ -22269,6 +22307,9 @@ export class MJArtifactVersion_ {
22269
22307
  @Field(() => [MJConversationDetailArtifact_])
22270
22308
  MJConversationDetailArtifacts_ArtifactVersionIDArray: MJConversationDetailArtifact_[]; // Link to MJConversationDetailArtifacts
22271
22309
 
22310
+ @Field(() => [MJConversationDetailAttachment_])
22311
+ MJConversationDetailAttachments_ArtifactVersionIDArray: MJConversationDetailAttachment_[]; // Link to MJConversationDetailAttachments
22312
+
22272
22313
  }
22273
22314
 
22274
22315
  //****************************************************************************
@@ -22321,6 +22362,9 @@ export class CreateMJArtifactVersionInput {
22321
22362
  @Field(() => Int, { nullable: true })
22322
22363
  ContentSizeBytes: number | null;
22323
22364
 
22365
+ @Field(() => Boolean, { nullable: true })
22366
+ ForceToolsOnly?: boolean;
22367
+
22324
22368
  @Field(() => RestoreContextInput, { nullable: true })
22325
22369
  RestoreContext___?: RestoreContextInput;
22326
22370
  }
@@ -22376,6 +22420,9 @@ export class UpdateMJArtifactVersionInput {
22376
22420
  @Field(() => Int, { nullable: true })
22377
22421
  ContentSizeBytes?: number | null;
22378
22422
 
22423
+ @Field(() => Boolean, { nullable: true })
22424
+ ForceToolsOnly?: boolean;
22425
+
22379
22426
  @Field(() => [KeyValuePairInput], { nullable: true })
22380
22427
  OldValues___?: KeyValuePairInput[];
22381
22428
 
@@ -22480,6 +22527,16 @@ export class MJArtifactVersionResolver extends ResolverBase {
22480
22527
  return result;
22481
22528
  }
22482
22529
 
22530
+ @FieldResolver(() => [MJConversationDetailAttachment_])
22531
+ async MJConversationDetailAttachments_ArtifactVersionIDArray(@Root() mjartifactversion_: MJArtifactVersion_, @Ctx() { userPayload, providers }: AppContext, @PubSub() pubSub: PubSubEngine) {
22532
+ this.CheckUserReadPermissions('MJ: Conversation Detail Attachments', userPayload);
22533
+ const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
22534
+ const sSQL = `SELECT * FROM ${provider.QuoteSchemaAndView(Metadata.Provider.ConfigData.MJCoreSchemaName, 'vwConversationDetailAttachments')} WHERE ${provider.QuoteIdentifier('ArtifactVersionID')}='${mjartifactversion_.ID}' ` + this.getRowLevelSecurityWhereClause(provider, 'MJ: Conversation Detail Attachments', userPayload, EntityPermissionType.Read, 'AND');
22535
+ const rows = await provider.ExecuteSQL(sSQL, undefined, undefined, this.GetUserFromPayload(userPayload));
22536
+ const result = await this.ArrayMapFieldNamesToCodeNames('MJ: Conversation Detail Attachments', rows, this.GetUserFromPayload(userPayload));
22537
+ return result;
22538
+ }
22539
+
22483
22540
  @Mutation(() => MJArtifactVersion_)
22484
22541
  async CreateMJArtifactVersion(
22485
22542
  @Arg('input', () => CreateMJArtifactVersionInput) input: CreateMJArtifactVersionInput,
@@ -23659,9 +23716,8 @@ export class MJCollectionArtifact_ {
23659
23716
  @MaxLength(255)
23660
23717
  Collection: string;
23661
23718
 
23662
- @Field({nullable: true})
23663
- @MaxLength(255)
23664
- ArtifactVersion?: string;
23719
+ @Field(() => Int)
23720
+ ArtifactVersion: number;
23665
23721
 
23666
23722
  }
23667
23723
 
@@ -32631,9 +32687,8 @@ export class MJConversationDetailArtifact_ {
32631
32687
  @Field()
32632
32688
  ConversationDetail: string;
32633
32689
 
32634
- @Field({nullable: true})
32635
- @MaxLength(255)
32636
- ArtifactVersion?: string;
32690
+ @Field(() => Int)
32691
+ ArtifactVersion: number;
32637
32692
 
32638
32693
  }
32639
32694
 
@@ -32828,6 +32883,10 @@ export class MJConversationDetailAttachment_ {
32828
32883
  @Field({nullable: true, description: `Description of the attachment providing context about its content and purpose.`})
32829
32884
  Description?: string;
32830
32885
 
32886
+ @Field({nullable: true, description: `Foreign key to the ArtifactVersion created alongside this attachment by the storage-unification path. When set, the agent resolver routes via the artifact path (manifest + tool dispatch) and skips inline embedding of the attachment to avoid double-processing. NULL for pre-v5.35 attachment rows authored before storage unification.`})
32887
+ @MaxLength(36)
32888
+ ArtifactVersionID?: string;
32889
+
32831
32890
  @Field()
32832
32891
  ConversationDetail: string;
32833
32892
 
@@ -32839,6 +32898,9 @@ export class MJConversationDetailAttachment_ {
32839
32898
  @MaxLength(500)
32840
32899
  File?: string;
32841
32900
 
32901
+ @Field(() => Int, {nullable: true})
32902
+ ArtifactVersion?: number;
32903
+
32842
32904
  }
32843
32905
 
32844
32906
  //****************************************************************************
@@ -32888,6 +32950,9 @@ export class CreateMJConversationDetailAttachmentInput {
32888
32950
  @Field({ nullable: true })
32889
32951
  Description: string | null;
32890
32952
 
32953
+ @Field({ nullable: true })
32954
+ ArtifactVersionID: string | null;
32955
+
32891
32956
  @Field(() => RestoreContextInput, { nullable: true })
32892
32957
  RestoreContext___?: RestoreContextInput;
32893
32958
  }
@@ -32940,6 +33005,9 @@ export class UpdateMJConversationDetailAttachmentInput {
32940
33005
  @Field({ nullable: true })
32941
33006
  Description?: string | null;
32942
33007
 
33008
+ @Field({ nullable: true })
33009
+ ArtifactVersionID?: string | null;
33010
+
32943
33011
  @Field(() => [KeyValuePairInput], { nullable: true })
32944
33012
  OldValues___?: KeyValuePairInput[];
32945
33013
 
@@ -51872,6 +51940,27 @@ export class MJList_ {
51872
51940
  @Field()
51873
51941
  _mj__UpdatedAt: Date;
51874
51942
 
51943
+ @Field({nullable: true, description: `Optional ID of the User View this list was materialized from. NULL for hand-built lists. When set, the list can be refreshed against this view via ListOperations.RefreshFromSource.`})
51944
+ @MaxLength(36)
51945
+ SourceViewID?: string;
51946
+
51947
+ @Field({nullable: true, description: `JSON snapshot of the source filter at materialization time. When UseSnapshot=1, refreshes re-apply this snapshot rather than re-reading the live source view. Null when no snapshot was captured.`})
51948
+ SourceFilterSnapshot?: string;
51949
+
51950
+ @Field({nullable: true, description: `Timestamp (UTC) of the most recent successful RefreshFromSource. Null when the list has never been refreshed.`})
51951
+ LastRefreshedAt?: Date;
51952
+
51953
+ @Field({nullable: true, description: `User who triggered the most recent successful RefreshFromSource. Null when the list has never been refreshed.`})
51954
+ @MaxLength(36)
51955
+ LastRefreshedByUserID?: string;
51956
+
51957
+ @Field({description: `Default refresh mode for this list. Additive only adds new members; Sync reconciles in both directions (may remove members no longer in the source — requires explicit drop-confirmation).`})
51958
+ @MaxLength(20)
51959
+ RefreshMode: string;
51960
+
51961
+ @Field(() => Boolean, {description: `When 1, RefreshFromSource uses SourceFilterSnapshot as the source. When 0 (default), it re-reads the live SourceView.`})
51962
+ UseSnapshot: boolean;
51963
+
51875
51964
  @Field()
51876
51965
  @MaxLength(255)
51877
51966
  Entity: string;
@@ -51888,6 +51977,14 @@ export class MJList_ {
51888
51977
  @MaxLength(255)
51889
51978
  CompanyIntegration?: string;
51890
51979
 
51980
+ @Field({nullable: true})
51981
+ @MaxLength(100)
51982
+ SourceView?: string;
51983
+
51984
+ @Field({nullable: true})
51985
+ @MaxLength(100)
51986
+ LastRefreshedByUser?: string;
51987
+
51891
51988
  @Field(() => [MJDuplicateRun_])
51892
51989
  MJDuplicateRuns_SourceListIDArray: MJDuplicateRun_[]; // Link to MJDuplicateRuns
51893
51990
 
@@ -51931,6 +52028,24 @@ export class CreateMJListInput {
51931
52028
  @Field({ nullable: true })
51932
52029
  CompanyIntegrationID: string | null;
51933
52030
 
52031
+ @Field({ nullable: true })
52032
+ SourceViewID: string | null;
52033
+
52034
+ @Field({ nullable: true })
52035
+ SourceFilterSnapshot: string | null;
52036
+
52037
+ @Field({ nullable: true })
52038
+ LastRefreshedAt: Date | null;
52039
+
52040
+ @Field({ nullable: true })
52041
+ LastRefreshedByUserID: string | null;
52042
+
52043
+ @Field({ nullable: true })
52044
+ RefreshMode?: string;
52045
+
52046
+ @Field(() => Boolean, { nullable: true })
52047
+ UseSnapshot?: boolean;
52048
+
51934
52049
  @Field(() => RestoreContextInput, { nullable: true })
51935
52050
  RestoreContext___?: RestoreContextInput;
51936
52051
  }
@@ -51965,6 +52080,24 @@ export class UpdateMJListInput {
51965
52080
  @Field({ nullable: true })
51966
52081
  CompanyIntegrationID?: string | null;
51967
52082
 
52083
+ @Field({ nullable: true })
52084
+ SourceViewID?: string | null;
52085
+
52086
+ @Field({ nullable: true })
52087
+ SourceFilterSnapshot?: string | null;
52088
+
52089
+ @Field({ nullable: true })
52090
+ LastRefreshedAt?: Date | null;
52091
+
52092
+ @Field({ nullable: true })
52093
+ LastRefreshedByUserID?: string | null;
52094
+
52095
+ @Field({ nullable: true })
52096
+ RefreshMode?: string;
52097
+
52098
+ @Field(() => Boolean, { nullable: true })
52099
+ UseSnapshot?: boolean;
52100
+
51968
52101
  @Field(() => [KeyValuePairInput], { nullable: true })
51969
52102
  OldValues___?: KeyValuePairInput[];
51970
52103
 
@@ -60908,7 +61041,7 @@ export class MJRecordGeoCode_ {
60908
61041
  @Field({nullable: true, description: `Timestamp of when geocoding was last attempted (success or failure).`})
60909
61042
  GeocodedAt?: Date;
60910
61043
 
60911
- @Field({nullable: true, description: `How this geocode was produced: google (Google Geocoding API), reference_data (resolved via Country/StateProvince tables), manual (user-entered), ip_geolocation (IP lookup), native (copied from entity lat/lng fields), reverse (reverse geocode from coordinates).`})
61044
+ @Field({nullable: true, description: `Source that produced this geocode. One of: google, geocodio, here, reference_data, manual, ip_geolocation, native, reverse.`})
60912
61045
  @MaxLength(30)
60913
61046
  GeocodingSource?: string;
60914
61047
 
@@ -76853,6 +76986,9 @@ export class MJUserView_ {
76853
76986
  @Field(() => [MJUserViewRun_])
76854
76987
  MJUserViewRuns_UserViewIDArray: MJUserViewRun_[]; // Link to MJUserViewRuns
76855
76988
 
76989
+ @Field(() => [MJList_])
76990
+ MJLists_SourceViewIDArray: MJList_[]; // Link to MJLists
76991
+
76856
76992
  }
76857
76993
 
76858
76994
  //****************************************************************************
@@ -77100,6 +77236,16 @@ export class MJUserViewResolverBase extends ResolverBase {
77100
77236
  return result;
77101
77237
  }
77102
77238
 
77239
+ @FieldResolver(() => [MJList_])
77240
+ async MJLists_SourceViewIDArray(@Root() mjuserview_: MJUserView_, @Ctx() { userPayload, providers }: AppContext, @PubSub() pubSub: PubSubEngine) {
77241
+ this.CheckUserReadPermissions('MJ: Lists', userPayload);
77242
+ const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
77243
+ const sSQL = `SELECT * FROM ${provider.QuoteSchemaAndView(Metadata.Provider.ConfigData.MJCoreSchemaName, 'vwLists')} WHERE ${provider.QuoteIdentifier('SourceViewID')}='${mjuserview_.ID}' ` + this.getRowLevelSecurityWhereClause(provider, 'MJ: Lists', userPayload, EntityPermissionType.Read, 'AND');
77244
+ const rows = await provider.ExecuteSQL(sSQL, undefined, undefined, this.GetUserFromPayload(userPayload));
77245
+ const result = await this.ArrayMapFieldNamesToCodeNames('MJ: Lists', rows, this.GetUserFromPayload(userPayload));
77246
+ return result;
77247
+ }
77248
+
77103
77249
  @Mutation(() => MJUserView_)
77104
77250
  async CreateMJUserView(
77105
77251
  @Arg('input', () => CreateMJUserViewInput) input: CreateMJUserViewInput,
@@ -77506,6 +77652,9 @@ export class MJUser_ {
77506
77652
  @Field(() => [MJAIAgent_])
77507
77653
  MJAIAgents_OwnerUserIDArray: MJAIAgent_[]; // Link to MJAIAgents
77508
77654
 
77655
+ @Field(() => [MJList_])
77656
+ MJLists_LastRefreshedByUserIDArray: MJList_[]; // Link to MJLists
77657
+
77509
77658
  }
77510
77659
 
77511
77660
  //****************************************************************************
@@ -78641,6 +78790,16 @@ export class MJUserResolverBase extends ResolverBase {
78641
78790
  return result;
78642
78791
  }
78643
78792
 
78793
+ @FieldResolver(() => [MJList_])
78794
+ async MJLists_LastRefreshedByUserIDArray(@Root() mjuser_: MJUser_, @Ctx() { userPayload, providers }: AppContext, @PubSub() pubSub: PubSubEngine) {
78795
+ this.CheckUserReadPermissions('MJ: Lists', userPayload);
78796
+ const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
78797
+ const sSQL = `SELECT * FROM ${provider.QuoteSchemaAndView(Metadata.Provider.ConfigData.MJCoreSchemaName, 'vwLists')} WHERE ${provider.QuoteIdentifier('LastRefreshedByUserID')}='${mjuser_.ID}' ` + this.getRowLevelSecurityWhereClause(provider, 'MJ: Lists', userPayload, EntityPermissionType.Read, 'AND');
78798
+ const rows = await provider.ExecuteSQL(sSQL, undefined, undefined, this.GetUserFromPayload(userPayload));
78799
+ const result = await this.ArrayMapFieldNamesToCodeNames('MJ: Lists', rows, this.GetUserFromPayload(userPayload));
78800
+ return result;
78801
+ }
78802
+
78644
78803
  @Mutation(() => MJUser_)
78645
78804
  async CreateMJUser(
78646
78805
  @Arg('input', () => CreateMJUserInput) input: CreateMJUserInput,
@@ -359,7 +359,8 @@ export class ResolverBase {
359
359
  viewInput.Aggregates,
360
360
  viewInput.AfterKey
361
361
  ? CompositeKey.FromKeyValuePairs((viewInput.AfterKey as { KeyValuePairs: { FieldName: string; Value: string }[] }).KeyValuePairs)
362
- : undefined
362
+ : undefined,
363
+ viewInput.BypassCache
363
364
  );
364
365
  }
365
366
  else {
@@ -400,7 +401,9 @@ export class ResolverBase {
400
401
  userPayload,
401
402
  viewInput.MaxRows,
402
403
  viewInput.StartRow,
403
- viewInput.Aggregates
404
+ viewInput.Aggregates,
405
+ undefined,
406
+ viewInput.BypassCache
404
407
  );
405
408
  } catch (err) {
406
409
  console.log(err);
@@ -444,7 +447,9 @@ export class ResolverBase {
444
447
  userPayload,
445
448
  viewInput.MaxRows,
446
449
  viewInput.StartRow,
447
- viewInput.Aggregates
450
+ viewInput.Aggregates,
451
+ undefined,
452
+ viewInput.BypassCache
448
453
  );
449
454
  } catch (err) {
450
455
  console.log(err);
@@ -517,6 +522,7 @@ export class ResolverBase {
517
522
  resultType: viewInput.ResultType,
518
523
  userPayload,
519
524
  aggregates: viewInput.Aggregates,
525
+ bypassCache: viewInput.BypassCache,
520
526
  });
521
527
  } catch (err) {
522
528
  LogError(err);
@@ -693,7 +699,8 @@ export class ResolverBase {
693
699
  maxRows: number | undefined,
694
700
  startRow: number | undefined,
695
701
  aggregates?: AggregateExpression[],
696
- afterKey?: CompositeKey
702
+ afterKey?: CompositeKey,
703
+ bypassCache?: boolean
697
704
  ) {
698
705
  try {
699
706
  if (!viewInfo || !userPayload) return null;
@@ -757,6 +764,7 @@ export class ResolverBase {
757
764
  AuditLogDescription: auditLogDescription,
758
765
  ResultType: rt,
759
766
  Aggregates: aggregates,
767
+ BypassCache: bypassCache,
760
768
  },
761
769
  user
762
770
  );
@@ -870,6 +878,7 @@ export class ResolverBase {
870
878
  AuditLogDescription: param.auditLogDescription,
871
879
  ResultType: rt,
872
880
  Aggregates: param.aggregates,
881
+ BypassCache: param.bypassCache,
873
882
  });
874
883
  }
875
884
 
@@ -170,6 +170,13 @@ export class RunViewByIDInput {
170
170
  description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
171
171
  })
172
172
  Aggregates?: AggregateExpressionInput[];
173
+
174
+ @Field(() => Boolean, {
175
+ nullable: true,
176
+ description:
177
+ 'Optional, when true bypasses ALL server-side caching for this view run — the pre-check cache lookup is skipped and the result is not stored in the cache. Use for maintenance/audit queries that must see true database state, or to force-refresh views whose filters reference rows the server cache invalidator cannot follow (e.g., cross-entity subqueries against vwListDetails).',
178
+ })
179
+ BypassCache?: boolean;
173
180
  }
174
181
 
175
182
  @InputType()
@@ -277,6 +284,13 @@ export class RunViewByNameInput {
277
284
  description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
278
285
  })
279
286
  Aggregates?: AggregateExpressionInput[];
287
+
288
+ @Field(() => Boolean, {
289
+ nullable: true,
290
+ description:
291
+ 'Optional, when true bypasses ALL server-side caching for this view run — the pre-check cache lookup is skipped and the result is not stored in the cache. Use for maintenance/audit queries that must see true database state, or to force-refresh views whose filters reference rows the server cache invalidator cannot follow (e.g., cross-entity subqueries against vwListDetails).',
292
+ })
293
+ BypassCache?: boolean;
280
294
  }
281
295
 
282
296
  @InputType()
@@ -370,6 +384,13 @@ export class RunDynamicViewInput {
370
384
  description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
371
385
  })
372
386
  Aggregates?: AggregateExpressionInput[];
387
+
388
+ @Field(() => Boolean, {
389
+ nullable: true,
390
+ description:
391
+ 'Optional, when true bypasses ALL server-side caching for this view run — the pre-check cache lookup is skipped and the result is not stored in the cache. Use for maintenance/audit queries that must see true database state, or to force-refresh views whose filters reference rows the server cache invalidator cannot follow (e.g., cross-entity subqueries against vwListDetails).',
392
+ })
393
+ BypassCache?: boolean;
373
394
  }
374
395
 
375
396
  @InputType()
@@ -492,6 +513,13 @@ export class RunViewGenericInput {
492
513
  description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
493
514
  })
494
515
  Aggregates?: AggregateExpressionInput[];
516
+
517
+ @Field(() => Boolean, {
518
+ nullable: true,
519
+ description:
520
+ 'Optional, when true bypasses ALL server-side caching for this view run — the pre-check cache lookup is skipped and the result is not stored in the cache. Use for maintenance/audit queries that must see true database state, or to force-refresh views whose filters reference rows the server cache invalidator cannot follow (e.g., cross-entity subqueries against vwListDetails).',
521
+ })
522
+ BypassCache?: boolean;
495
523
  }
496
524
 
497
525
  //****************************************************************************
@@ -18,6 +18,12 @@ class AdhocQueryInput {
18
18
 
19
19
  @Field(() => Int, { nullable: true, description: 'Query timeout in seconds. Defaults to 30.' })
20
20
  TimeoutSeconds?: number;
21
+
22
+ @Field(() => Int, { nullable: true, description: 'Maximum number of rows to return. Applied in-memory after SQL execution; SQL still runs unbounded server-side.' })
23
+ MaxRows?: number;
24
+
25
+ @Field(() => Int, { nullable: true, description: 'Zero-based offset for pagination. Used in conjunction with MaxRows.' })
26
+ StartRow?: number;
21
27
  }
22
28
 
23
29
  /**
@@ -56,28 +62,55 @@ export class AdhocQueryResolver extends ResolverBase {
56
62
  return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
57
63
  }
58
64
 
59
- // 3. Execute with timeout
65
+ // 3. Build executable SQL. When MaxRows is provided, wrap in a derived table
66
+ // with outer TOP so the engine can short-circuit at the source instead of
67
+ // scanning the full result. Skipped for SQL that begins with WITH/CTE — those
68
+ // can't be nested in a derived table on SQL Server and fall through to the
69
+ // in-memory slice below.
70
+ const startRow = input.StartRow ?? 0;
71
+ const maxRows = input.MaxRows;
72
+ const canWrap =
73
+ maxRows != null &&
74
+ Number.isInteger(maxRows) &&
75
+ maxRows > 0 &&
76
+ Number.isInteger(startRow) &&
77
+ startRow >= 0 &&
78
+ !/^\s*WITH\b/i.test(input.SQL);
79
+ const executableSql = canWrap
80
+ ? `SELECT TOP ${startRow + maxRows} * FROM (\n${input.SQL}\n) AS _adhoc_capped`
81
+ : input.SQL;
82
+
83
+ // 4. Execute with timeout
60
84
  const timeoutMs = (input.TimeoutSeconds ?? 30) * 1000;
61
85
  const request = new sql.Request(readOnlyDS);
62
86
 
63
87
  const result = await Promise.race([
64
- request.query(input.SQL),
88
+ request.query(executableSql),
65
89
  new Promise<never>((_, reject) =>
66
90
  setTimeout(() => reject(new Error('Query timeout exceeded')), timeoutMs)
67
91
  )
68
92
  ]);
69
93
  const executionTimeMs = Date.now() - startTime;
70
94
 
71
- // 4. Return as RunQueryResultType
95
+ // 5. Apply in-memory pagination. With the wrap applied this is a no-op for
96
+ // first-page reads; for StartRow > 0 (or CTE-headed SQL where the wrap was
97
+ // skipped) it carves out the requested page.
98
+ const fullRecordset = result.recordset ?? [];
99
+ const totalRowCount = fullRecordset.length;
100
+ let paginated = fullRecordset;
101
+ if (startRow > 0) paginated = paginated.slice(startRow);
102
+ if (maxRows != null && maxRows > 0) paginated = paginated.slice(0, maxRows);
103
+
104
+ // 6. Return as RunQueryResultType
72
105
  return {
73
106
  QueryID: '',
74
107
  QueryName: 'Ad-Hoc Query',
75
108
  Success: true,
76
- Results: JSON.stringify(result.recordset ?? []),
77
- RowCount: result.recordset?.length ?? 0,
78
- TotalRowCount: result.recordset?.length ?? 0,
79
- PageNumber: undefined,
80
- PageSize: undefined,
109
+ Results: JSON.stringify(paginated),
110
+ RowCount: paginated.length,
111
+ TotalRowCount: totalRowCount,
112
+ PageNumber: maxRows != null && maxRows > 0 ? Math.floor(startRow / maxRows) + 1 : undefined,
113
+ PageSize: maxRows ?? undefined,
81
114
  ExecutionTime: executionTimeMs,
82
115
  ErrorMessage: ''
83
116
  };