Member Junction
    Preparing search index...

    Class MJSearchScopeProviderResolver

    Hierarchy (View Summary)

    Index

    Constructors

    Accessors

    Methods

    • Applies an inbound RestoreContext___ blob to a server-side BaseEntity. Mirrors the OldValues___ pattern — the client-side BaseEntity's _restoreContext doesn't traverse the network, so the server must reconstruct it from the mutation input before calling Save().

      Returns true when context was applied; false when no context was on the input.

      Parameters

      • entityObject: BaseEntity
      • input: { RestoreContext___?: { Reason?: string; SourceChangeID?: string } }

      Returns boolean

    • SECURITY — GraphQL-boundary screen for one client-supplied SQL clause fragment.

      ValidateUserProvidedSQLClause still blocks stacked statements, DML, comments, UNION and WAITFOR, and still permits SELECT (server-internal engines pass richer filters). A keyword ban on SELECT/EXISTS at this boundary (#4253) broke first-party clients that use IN (SELECT … FROM <entity base view>) — that is a legitimate ExtraFilter.

      This screen uses @memberjunction/sql-parser (same wrap as EDS assertReadOnlyClause): wrap the fragment as a single SELECT, fail closed if it does not parse as a read, then allow a FROM only when it is an entity BaseView. Base tables (Meeting, __mj.User) and catalogs are rejected. Server-internal RunView callers never hit this.

      RLS is applied by RunView as an outer WHERE around the entity being queried, not compiled into the view. Subqueries against another entity's BaseView therefore do not inherit that entity's RLS; they are still restricted to the view (not the table).

      Parameters

      Returns void

    • The CodeNames this user may read on this entity, for the field-security transport key — or null when there is nothing to state (no denials, so every field is readable and the client needs no help distinguishing a withheld null from a genuine one).

      Carries entity field NAMES, not CodeNames and not the _mj__ transport shape. The client consumes this after it has already reversed the transport mapping, and it matches names against EntityFieldInfo.Name — the same key the denied set itself is built from. Sending the one shape both sides already agree on avoids a second mapping that could drift.

      Parameters

      • entityInfo: EntityInfo
      • deniedReadFields: Set<string>

      Returns string[]

    • Checks API key scope authorization. Only performs check if request was authenticated via API key (apiKeyHash present in userPayload). For OAuth/JWT auth, this is a no-op.

      Parameters

      • scopePath: string

        The scope path (e.g., 'entity:read', 'agent:execute')

      • resource: string

        The resource name (e.g., entity name, agent name)

      • userPayload: UserPayload

        The user payload from context

      Returns Promise<void>

      AuthorizationError if API key lacks required scope

    • Filters encrypted field values before sending to the API client.

      For each encrypted field in the entity:

      • If AllowDecryptInAPI is true: value passes through unchanged (already decrypted by data provider)
      • If AllowDecryptInAPI is false and SendEncryptedValue is true: re-encrypt and send ciphertext
      • If AllowDecryptInAPI is false and SendEncryptedValue is false: replace with sentinel value

      Parameters

      • entityName: string

        Name of the entity

      • dataObject: Record<string, unknown>

        The data object containing field values

      • contextUser: UserInfo

        User context for encryption operations

      • Optionalprovider: IMetadataProvider

      Returns Promise<Record<string, unknown>>

      The filtered data object

    • Loads a single external-data-source-backed entity record by primary key and returns it in GraphQL field-name (CodeName) shape, or null if not found.

      External entities (Entity.ExternalDataSourceID set) have no MJ base view or sproc — their data is proxied live from a remote system — so the generated single-record resolver cannot run SELECT * FROM <baseView>. Instead it loads through a BaseEntity object, whose InnerLoad the data provider dispatches to the external read router's LoadExternalRecord (a composite-key aware, quoted, parameter-bound single-record lookup), applying the same RLS gate and field post-processing (decryption / datetime normalization) as the MJ-DB path. The caller is responsible for the CheckUserReadPermissions gate beforehand.

      Type Parameters

      • T

      Parameters

      Returns Promise<T>

    • Maps field names to their GraphQL-safe CodeNames and handles encryption for API responses.

      For encrypted fields coming from raw SQL queries (not entity objects):

      • AllowDecryptInAPI=true: Decrypt the value before sending to client
      • AllowDecryptInAPI=false + SendEncryptedValue=true: Keep encrypted ciphertext
      • AllowDecryptInAPI=false + SendEncryptedValue=false: Replace with sentinel

      Returns a COPY — dataObject is never written to. Callers routinely pass rows straight from findBy/RunView, which are the server cache's own objects held by reference, and LocalCacheManager deep-freezes them. Renaming in place therefore threw Cannot add property _mj__CreatedAt, object is not extensible on every UserByEmail / UserByID / UserByEmployeeID call and on every generated single-record resolver whose entity has caching enabled. (Before the freeze it did something worse but quieter: it rewrote the cached row's keys, so later readers were served transport-shaped rows that BaseEntity.SetMany rejects.) Copying here fixes every call site at once and makes the hazard unreachable for future ones.

      Parameters

      • entityName: string

        The entity name

      • dataObject: any

        The data object with field values. Not modified.

      • OptionalcontextUser: UserInfo

        Optional user context for decryption (required for encrypted fields)

      • Optionalprovider: IMetadataProvider
      • OptionaldeniedReadFields: Set<string>
      • OptionalrecordChangeProjector: RecordChangeFieldSecurityProjector

      Returns Promise<any>

      A new object in transport shape, or null when there is nothing to map

    • Whether UpdateRecord must hydrate the entity from the DATABASE rather than from the client's OldValues___.

      The OldValues___ path exists as an optimization: when nothing needs the true prior state, the client already holds it and a round trip is wasted. Each condition below is a reason that assumption fails.

      EnableFieldLevelSecurity is the security-critical one, and it is NOT redundant with hasDeniedReadFields. That flag reports READ denials, and the canonical FLS configuration is Read Allow + Update Deny — which leaves it false. Such a caller would be hydrated from its own OldValues___, and a value it supplies there for an update-denied field arrives through LoadFromData, which the EntityField setter records as that field's INITIAL value. The field is therefore not dirty, BaseEntity.CheckFieldLevelUpdatePermissions only rejects field.Dirty && denied, and GenerateSaveSQL sends every IsSPParameter field regardless of dirtiness (it skips only NotLoaded). The fabricated value reached spUpdate having passed every check: pinning a value in OldValues___ was a write to a field the caller may not write.

      Forcing the truth-load closes that rather than relocating it. The entity is hydrated by InnerLoad from the real row, and TestAndSetClientOldValuesToDBValues reads the client's OldValues only to detect concurrent-edit overlap before ending in SetMany(clientNewValues) — it never applies them to the entity. An update-denied field can then only become dirty by being named in the mutation input itself, which is precisely the case that check does catch.

      Ordered so the boolean flag is evaluated last: the extra load lands only on entities that have the feature switched on, which is almost none of them.

      Parameters

      • entityInfo: EntityInfo
      • input: { OldValues___?: { Key: string; Value: unknown }[] }
      • hasDeniedReadFields: boolean
      • hasNarrowedAuditPayload: boolean

      Returns boolean

    • Publishes a push-status update to the client on PUSH_STATUS_UPDATES_TOPIC, stamping the authenticated owner's user ID from userPayload so the subscription filter can bind delivery to identity (see B49 / statusUpdatesFilter). The ergonomic wrapper every resolver should use instead of calling pubSub.publish on the topic directly — it makes omitting identity impossible. Non-resolver publishers (services, the liveness heartbeat) call the shared publishStatusUpdate() function directly with an explicit ownerUserId.

      Parameters

      Returns void

    • Reverse-maps GraphQL-safe field names back to entity CodeNames in a mutation input object. For example, _mj__integration_SyncStatus is mapped back to __mj_integration_SyncStatus. Also reverse-maps keys inside the OldValues___ array if present. This is the inverse of MapFieldNamesToCodeNames and must be called before passing GraphQL input to entity SetMany() or field lookups.

      Parameters

      • input: Record<string, unknown>

      Returns Record<string, unknown>

    • Optimized RunViewGenericInternal implementation with:

      • Field filtering at source (Fix #7)
      • Improved error handling (Fix #9)

      Parameters

      • provider: DatabaseProviderBase
      • viewInfo: MJUserViewEntityExtended
      • extraFilter: string
      • orderBy: string
      • userSearchString: string
      • excludeUserViewRunID: string
      • overrideExcludeFilter: string
      • saveViewResults: boolean
      • fields: string[]
      • ignoreMaxRows: boolean
      • excludeDataFromAllPriorViewRuns: boolean
      • forceAuditLog: boolean
      • auditLogDescription: string
      • resultType: string
      • userPayload: UserPayload
      • maxRows: number
      • startRow: number
      • Optionalaggregates: AggregateExpression[]
      • OptionalafterKey: CompositeKey
      • OptionalbypassCache: boolean
      • OptionaldataSource: "Live" | "Materialized"

      Returns Promise<RunViewResult<any>>

    • Applies assertClientClauseUsesEntityBaseViews to every client-supplied clause a view request can carry that is actually a SQL fragment. GraphQL entry points (RunViewByName, RunViewByID, RunDynamicView, RunViews) funnel through RunViewGenericInternal / RunViewsGenericInternal.

      🚨 UserSearchString is deliberately NOT screened here (#4392). It is not a clause — it is the free text a person typed into a search box, and it never reaches SQL as a fragment. GenericDatabaseProvider.createViewUserSearchSQL builds the predicate itself from IncludeInUserSearchAPI metadata and lands the text only as a literal, doubling single quotes and escaping LIKE metacharacters with an explicit ESCAPE. Running it through a screen that parses its argument as SQL and fails closed rejected every term that is not coincidentally valid SQL: Marcus Chen parses as nothing, O'Leary as an unterminated literal — so essentially every real name search returned 0 rows.

      The provider-level denylist does NOT back this up — it was removed from UserSearchString in the same change, for the same reason (it refused Union Pacific). What protects the value is that it is never SQL: it reaches the database only as a quoted, quote-doubled literal. The single place that is not true is a field carrying UserSearchParamFormatAPI, whose admin-authored format may splice the term in unquoted; createViewUserSearchSQL re-applies ValidateUserProvidedSQLClause for exactly those entities.

      Parameters

      • clauses: {
            extraFilter?: string;
            orderBy?: string;
            overrideExcludeFilter?: string;
            userSearchString?: string;
        }
      • Optionalprovider: IMetadataProvider

      Returns void

    • Field-level security guard for the update path.

      Every denied-read field is stripped — new values AND OldValues. A field the user cannot read was absent from every payload that client ever received, so any value coming back for it is the transport's invention rather than user intent, and applying it would silently overwrite the real column. Stripping is what makes "load a record, edit an unrelated field, save" safe for a restricted user.

      There is no split by update permission: Read is required for Update, so a user denied read is denied update too and denied-read ∩ denied-update is just denied-read.

      Silent narrowing, not rejection — consistent with the output projection the client already experiences, and with the ambiguous-error rule (naming the field would confirm it exists and is restricted).

      Returns true when the user has a non-empty denied-read set on this entity. The caller must then hydrate the entity from the DATABASE (never from client OldValues), so denied fields hold true values that an omitted key leaves untouched.

      Parameters

      • entityInfo: EntityInfo
      • userInfo: UserInfo
      • input: { OldValues___?: { Key: string; Value: unknown }[] } & Record<string, unknown>
      • clientNewValues: Record<string, unknown>

      Returns boolean

    • Refuses a client-sent MJ: Record Changes payload column from a caller who carries field denials, and forces the update onto the load-truth-from-DB branch.

      This is the write half of the audit-trail projection, and without it that projection would itself destroy audit history. A restricted caller is served a NARROWED ChangesJSON / FullRecordJSON and no ChangesDescription. Those values hydrate a client-side entity as ordinary loaded values, and GenerateSaveSQL writes EVERY IsSPParameter field rather than only dirty ones — so a user who edits Comments on that record and saves would silently overwrite the stored payload with the narrowed one they were shown. The row would keep looking like a complete audit entry while the pruned fields were gone for everyone, permanently.

      The rule is the one field security already applies elsewhere: a value a client could not have seen in full is not user intent, it is a transport artifact, and it is ignored. Here that is widened from "fields the caller cannot read" to "the audit payload of a caller who carries any denial", because the column is readable — it is its CONTENTS that were narrowed, and which row's target entity did the narrowing is not knowable from the input alone.

      Returning true routes the caller through InnerLoad, so the stored values are restored from the database before the save. Legitimate payload writes are unaffected: they happen server-side (SnapshotBuilder, replay) and never through this resolver.

      Parameters

      • entityInfo: EntityInfo
      • userInfo: UserInfo
      • input: { OldValues___?: { Key: string; Value: unknown }[] } & Record<string, unknown>
      • clientNewValues: Record<string, unknown>
      • Optionalprovider: IMetadataProvider

      Returns boolean

    • This routine compares the OldValues property in the input object to the values in the DB that we just loaded. If there are differences, we need to check to see if the client is trying to update any of those fields (e.g. overlap). If there is overlap, we throw an error. If there is no overlap, we can proceed with the update even if the DB Values and the ClientOldValues are not 100% the same, so long as there is no overlap in the specific FIELDS that are different.

      ASSUMES: input object has an OldValues___ property that is an array of Key/Value pairs that represent the old values of the record that the client is trying to update.

      Parameters

      Returns Promise<void>