@memberjunction/server 6.1.0-edge.6 → 6.1.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 (177) hide show
  1. package/README.md +24 -9
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/config.d.ts +36 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +9 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts +30 -0
  8. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
  9. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +58 -5
  10. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
  11. package/dist/generated/graphql-schemas/__mj.d.ts +2842 -2144
  12. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  13. package/dist/generated/graphql-schemas/__mj.js +5334 -2143
  14. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  15. package/dist/generic/ResolverBase.d.ts +119 -4
  16. package/dist/generic/ResolverBase.d.ts.map +1 -1
  17. package/dist/generic/ResolverBase.js +264 -11
  18. package/dist/generic/ResolverBase.js.map +1 -1
  19. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  20. package/dist/generic/RunViewResolver.js +26 -2
  21. package/dist/generic/RunViewResolver.js.map +1 -1
  22. package/dist/generic/refusalExtensions.d.ts +52 -0
  23. package/dist/generic/refusalExtensions.d.ts.map +1 -0
  24. package/dist/generic/refusalExtensions.js +16 -0
  25. package/dist/generic/refusalExtensions.js.map +1 -0
  26. package/dist/index.d.ts +9 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +115 -91
  29. package/dist/index.js.map +1 -1
  30. package/dist/resolvers/EntityRecordNameResolver.d.ts +34 -5
  31. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  32. package/dist/resolvers/EntityRecordNameResolver.js +64 -7
  33. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  34. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -2
  35. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  36. package/dist/resolvers/RealtimeClientSessionResolver.js +80 -4
  37. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  38. package/dist/rest/RealtimeSdpBrokerHandler.d.ts +23 -0
  39. package/dist/rest/RealtimeSdpBrokerHandler.d.ts.map +1 -0
  40. package/dist/rest/RealtimeSdpBrokerHandler.js +78 -0
  41. package/dist/rest/RealtimeSdpBrokerHandler.js.map +1 -0
  42. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  43. package/dist/serverExtensionReservedRoots.js +2 -3
  44. package/dist/serverExtensionReservedRoots.js.map +1 -1
  45. package/package.json +92 -92
  46. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +118 -0
  47. package/src/__tests__/RealtimeClientSessionResolver.test.ts +20 -0
  48. package/src/__tests__/RealtimeSdpBrokerHandler.test.ts +191 -0
  49. package/src/__tests__/ResolverBase.filterEscaping.test.ts +63 -0
  50. package/src/__tests__/ResolverBase.readableFieldsTransport.test.ts +106 -0
  51. package/src/__tests__/generatedGraphQLSchema.structural.test.ts +46 -0
  52. package/src/__tests__/resolverBase.fls.test.ts +483 -0
  53. package/src/__tests__/resolverBase.recordChangesFls.test.ts +358 -0
  54. package/src/__tests__/resolverBase.refusalExtensions.test.ts +101 -0
  55. package/src/__tests__/serverExtensionReservedRoots.test.ts +79 -11
  56. package/src/config.ts +12 -0
  57. package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +65 -6
  58. package/src/generated/graphql-schemas/__mj.ts +6590 -4287
  59. package/src/generic/ResolverBase.ts +313 -10
  60. package/src/generic/RunViewResolver.ts +28 -2
  61. package/src/generic/refusalExtensions.ts +59 -0
  62. package/src/index.ts +137 -100
  63. package/src/resolvers/EntityRecordNameResolver.ts +74 -8
  64. package/src/resolvers/RealtimeClientSessionResolver.ts +128 -2
  65. package/src/rest/RealtimeSdpBrokerHandler.ts +101 -0
  66. package/src/serverExtensionReservedRoots.ts +2 -3
  67. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +0 -27
  68. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +0 -1
  69. package/dist/resolvers/RingCentralTelephonyResolver.js +0 -87
  70. package/dist/resolvers/RingCentralTelephonyResolver.js.map +0 -1
  71. package/dist/resolvers/TeamsMeetingsResolver.d.ts +0 -28
  72. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +0 -1
  73. package/dist/resolvers/TeamsMeetingsResolver.js +0 -91
  74. package/dist/resolvers/TeamsMeetingsResolver.js.map +0 -1
  75. package/dist/resolvers/TelephonyResolver.d.ts +0 -26
  76. package/dist/resolvers/TelephonyResolver.d.ts.map +0 -1
  77. package/dist/resolvers/TelephonyResolver.js +0 -86
  78. package/dist/resolvers/TelephonyResolver.js.map +0 -1
  79. package/dist/resolvers/VonageTelephonyResolver.d.ts +0 -26
  80. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +0 -1
  81. package/dist/resolvers/VonageTelephonyResolver.js +0 -86
  82. package/dist/resolvers/VonageTelephonyResolver.js.map +0 -1
  83. package/dist/telephony/RingCentralTelephonyService.d.ts +0 -115
  84. package/dist/telephony/RingCentralTelephonyService.d.ts.map +0 -1
  85. package/dist/telephony/RingCentralTelephonyService.js +0 -262
  86. package/dist/telephony/RingCentralTelephonyService.js.map +0 -1
  87. package/dist/telephony/TeamsMeetingsRouter.d.ts +0 -40
  88. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +0 -1
  89. package/dist/telephony/TeamsMeetingsRouter.js +0 -112
  90. package/dist/telephony/TeamsMeetingsRouter.js.map +0 -1
  91. package/dist/telephony/TeamsMeetingsService.d.ts +0 -113
  92. package/dist/telephony/TeamsMeetingsService.d.ts.map +0 -1
  93. package/dist/telephony/TeamsMeetingsService.js +0 -196
  94. package/dist/telephony/TeamsMeetingsService.js.map +0 -1
  95. package/dist/telephony/TwilioTelephonyRouter.d.ts +0 -36
  96. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +0 -1
  97. package/dist/telephony/TwilioTelephonyRouter.js +0 -143
  98. package/dist/telephony/TwilioTelephonyRouter.js.map +0 -1
  99. package/dist/telephony/TwilioTelephonyService.d.ts +0 -95
  100. package/dist/telephony/TwilioTelephonyService.d.ts.map +0 -1
  101. package/dist/telephony/TwilioTelephonyService.js +0 -193
  102. package/dist/telephony/TwilioTelephonyService.js.map +0 -1
  103. package/dist/telephony/VonageTelephonyRouter.d.ts +0 -41
  104. package/dist/telephony/VonageTelephonyRouter.d.ts.map +0 -1
  105. package/dist/telephony/VonageTelephonyRouter.js +0 -201
  106. package/dist/telephony/VonageTelephonyRouter.js.map +0 -1
  107. package/dist/telephony/VonageTelephonyService.d.ts +0 -90
  108. package/dist/telephony/VonageTelephonyService.d.ts.map +0 -1
  109. package/dist/telephony/VonageTelephonyService.js +0 -191
  110. package/dist/telephony/VonageTelephonyService.js.map +0 -1
  111. package/dist/telephony/calendar-scheduler.d.ts +0 -67
  112. package/dist/telephony/calendar-scheduler.d.ts.map +0 -1
  113. package/dist/telephony/calendar-scheduler.js +0 -133
  114. package/dist/telephony/calendar-scheduler.js.map +0 -1
  115. package/dist/telephony/index.d.ts +0 -29
  116. package/dist/telephony/index.d.ts.map +0 -1
  117. package/dist/telephony/index.js +0 -38
  118. package/dist/telephony/index.js.map +0 -1
  119. package/dist/telephony/media-upgrade-router.d.ts +0 -47
  120. package/dist/telephony/media-upgrade-router.d.ts.map +0 -1
  121. package/dist/telephony/media-upgrade-router.js +0 -72
  122. package/dist/telephony/media-upgrade-router.js.map +0 -1
  123. package/dist/telephony/ringcentral-runtime.d.ts +0 -14
  124. package/dist/telephony/ringcentral-runtime.d.ts.map +0 -1
  125. package/dist/telephony/ringcentral-runtime.js +0 -18
  126. package/dist/telephony/ringcentral-runtime.js.map +0 -1
  127. package/dist/telephony/teams-meetings-runtime.d.ts +0 -16
  128. package/dist/telephony/teams-meetings-runtime.d.ts.map +0 -1
  129. package/dist/telephony/teams-meetings-runtime.js +0 -20
  130. package/dist/telephony/teams-meetings-runtime.js.map +0 -1
  131. package/dist/telephony/teamsAcsMediaRegistry.d.ts +0 -69
  132. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +0 -1
  133. package/dist/telephony/teamsAcsMediaRegistry.js +0 -118
  134. package/dist/telephony/teamsAcsMediaRegistry.js.map +0 -1
  135. package/dist/telephony/telephony-runtime.d.ts +0 -14
  136. package/dist/telephony/telephony-runtime.d.ts.map +0 -1
  137. package/dist/telephony/telephony-runtime.js +0 -18
  138. package/dist/telephony/telephony-runtime.js.map +0 -1
  139. package/dist/telephony/twilioMediaRegistry.d.ts +0 -60
  140. package/dist/telephony/twilioMediaRegistry.d.ts.map +0 -1
  141. package/dist/telephony/twilioMediaRegistry.js +0 -106
  142. package/dist/telephony/twilioMediaRegistry.js.map +0 -1
  143. package/dist/telephony/vonage-runtime.d.ts +0 -14
  144. package/dist/telephony/vonage-runtime.d.ts.map +0 -1
  145. package/dist/telephony/vonage-runtime.js +0 -18
  146. package/dist/telephony/vonage-runtime.js.map +0 -1
  147. package/dist/telephony/vonageMediaRegistry.d.ts +0 -78
  148. package/dist/telephony/vonageMediaRegistry.d.ts.map +0 -1
  149. package/dist/telephony/vonageMediaRegistry.js +0 -158
  150. package/dist/telephony/vonageMediaRegistry.js.map +0 -1
  151. package/src/__tests__/media-upgrade-router.test.ts +0 -43
  152. package/src/__tests__/teams-meetings-service.test.ts +0 -65
  153. package/src/__tests__/teamsAcsMediaRegistry.test.ts +0 -82
  154. package/src/__tests__/twilio-telephony-service.test.ts +0 -23
  155. package/src/__tests__/twilioMediaRegistry.test.ts +0 -103
  156. package/src/__tests__/vonageMediaRegistry.test.ts +0 -180
  157. package/src/resolvers/RingCentralTelephonyResolver.ts +0 -64
  158. package/src/resolvers/TeamsMeetingsResolver.ts +0 -68
  159. package/src/resolvers/TelephonyResolver.ts +0 -63
  160. package/src/resolvers/VonageTelephonyResolver.ts +0 -63
  161. package/src/telephony/RingCentralTelephonyService.ts +0 -342
  162. package/src/telephony/TeamsMeetingsRouter.ts +0 -143
  163. package/src/telephony/TeamsMeetingsService.ts +0 -268
  164. package/src/telephony/TwilioTelephonyRouter.ts +0 -184
  165. package/src/telephony/TwilioTelephonyService.ts +0 -261
  166. package/src/telephony/VonageTelephonyRouter.ts +0 -239
  167. package/src/telephony/VonageTelephonyService.ts +0 -259
  168. package/src/telephony/calendar-scheduler.ts +0 -176
  169. package/src/telephony/index.ts +0 -43
  170. package/src/telephony/media-upgrade-router.ts +0 -79
  171. package/src/telephony/ringcentral-runtime.ts +0 -22
  172. package/src/telephony/teams-meetings-runtime.ts +0 -24
  173. package/src/telephony/teamsAcsMediaRegistry.ts +0 -152
  174. package/src/telephony/telephony-runtime.ts +0 -22
  175. package/src/telephony/twilioMediaRegistry.ts +0 -137
  176. package/src/telephony/vonage-runtime.ts +0 -22
  177. package/src/telephony/vonageMediaRegistry.ts +0 -200
@@ -1,4 +1,4 @@
1
- import { AggregateExpression, BaseEntity, CompositeKey, DatabaseProviderBase, EntityPermissionType, IMetadataProvider, RunViewResult, UserInfo } from '@memberjunction/core';
1
+ import { AggregateExpression, BaseEntity, CompositeKey, DatabaseProviderBase, EntityInfo, EntityPermissionType, IMetadataProvider, RecordChangeFieldSecurityProjector, RunViewResult, UserInfo } from '@memberjunction/core';
2
2
  import { MJUserViewEntityExtended } from '@memberjunction/core-entities';
3
3
  import { PubSubEngine } from 'type-graphql';
4
4
  import { RunViewGenericParams, UserPayload } from '../types.js';
@@ -33,7 +33,7 @@ export declare class ResolverBase {
33
33
  * @param contextUser - Optional user context for decryption (required for encrypted fields)
34
34
  * @returns A new object in transport shape, or null when there is nothing to map
35
35
  */
36
- protected MapFieldNamesToCodeNames(entityName: string, dataObject: any, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<any>;
36
+ protected MapFieldNamesToCodeNames(entityName: string, dataObject: any, contextUser?: UserInfo, provider?: IMetadataProvider, deniedReadFields?: Set<string>, recordChangeProjector?: RecordChangeFieldSecurityProjector): Promise<any>;
37
37
  /**
38
38
  * Reverse-maps GraphQL-safe field names back to entity CodeNames in a mutation input object.
39
39
  * For example, `_mj__integration_SyncStatus` is mapped back to `__mj_integration_SyncStatus`.
@@ -63,6 +63,17 @@ export declare class ResolverBase {
63
63
  * returning the original) is what makes this safe on cache-served input.
64
64
  */
65
65
  protected ArrayMapFieldNamesToCodeNames(entityName: string, dataObjectArray: any[], contextUser?: UserInfo, provider?: IMetadataProvider): Promise<any[]>;
66
+ /**
67
+ * The CodeNames this user may read on this entity, for the field-security transport key — or
68
+ * null when there is nothing to state (no denials, so every field is readable and the client
69
+ * needs no help distinguishing a withheld null from a genuine one).
70
+ *
71
+ * Carries entity field NAMES, not CodeNames and not the `_mj__` transport shape. The client
72
+ * consumes this after it has already reversed the transport mapping, and it matches names
73
+ * against `EntityFieldInfo.Name` — the same key the denied set itself is built from. Sending
74
+ * the one shape both sides already agree on avoids a second mapping that could drift.
75
+ */
76
+ protected BuildReadableFieldsTransportValue(entityInfo: EntityInfo, deniedReadFields: Set<string> | null | undefined): string[] | null;
66
77
  /**
67
78
  * Loads a single external-data-source-backed entity record by primary key and returns it in
68
79
  * GraphQL field-name (CodeName) shape, or null if not found.
@@ -161,8 +172,25 @@ export declare class ResolverBase {
161
172
  protected assertClientClauseUsesEntityBaseViews(clause: string | undefined | null, label: string, provider?: IMetadataProvider): void;
162
173
  /**
163
174
  * Applies {@link assertClientClauseUsesEntityBaseViews} to every client-supplied clause
164
- * a view request can carry. GraphQL entry points (RunViewByName, RunViewByID,
165
- * RunDynamicView, RunViews) funnel through RunViewGenericInternal / RunViewsGenericInternal.
175
+ * a view request can carry that is actually a SQL fragment. GraphQL entry points
176
+ * (RunViewByName, RunViewByID, RunDynamicView, RunViews) funnel through
177
+ * RunViewGenericInternal / RunViewsGenericInternal.
178
+ *
179
+ * 🚨 `UserSearchString` is deliberately NOT screened here (#4392). It is not a clause — it is
180
+ * the free text a person typed into a search box, and it never reaches SQL as a fragment.
181
+ * `GenericDatabaseProvider.createViewUserSearchSQL` builds the predicate itself from
182
+ * `IncludeInUserSearchAPI` metadata and lands the text only as a literal, doubling single
183
+ * quotes and escaping LIKE metacharacters with an explicit `ESCAPE`. Running it through a
184
+ * screen that parses its argument as SQL and fails closed rejected every term that is not
185
+ * coincidentally valid SQL: `Marcus Chen` parses as nothing, `O'Leary` as an unterminated
186
+ * literal — so essentially every real name search returned 0 rows.
187
+ *
188
+ * The provider-level denylist does NOT back this up — it was removed from `UserSearchString`
189
+ * in the same change, for the same reason (it refused `Union Pacific`). What protects the value
190
+ * is that it is never SQL: it reaches the database only as a quoted, quote-doubled literal.
191
+ * The single place that is not true is a field carrying `UserSearchParamFormatAPI`, whose
192
+ * admin-authored format may splice the term in unquoted; `createViewUserSearchSQL` re-applies
193
+ * `ValidateUserProvidedSQLClause` for exactly those entities.
166
194
  */
167
195
  protected screenClientViewClauses(clauses: {
168
196
  extraFilter?: string | null;
@@ -217,6 +245,93 @@ export declare class ResolverBase {
217
245
  protected BeforeCreate(provider: DatabaseProviderBase, input: any): Promise<boolean>;
218
246
  protected AfterCreate(provider: DatabaseProviderBase, input: any): Promise<void>;
219
247
  protected UpdateRecord(entityName: string, input: any, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine): Promise<any>;
248
+ /**
249
+ * Whether `UpdateRecord` must hydrate the entity from the DATABASE rather than from the
250
+ * client's `OldValues___`.
251
+ *
252
+ * The `OldValues___` path exists as an optimization: when nothing needs the true prior state,
253
+ * the client already holds it and a round trip is wasted. Each condition below is a reason
254
+ * that assumption fails.
255
+ *
256
+ * `EnableFieldLevelSecurity` is the security-critical one, and it is NOT redundant with
257
+ * `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
258
+ * Read Allow + Update Deny — which leaves it false. Such a caller would be hydrated from its own
259
+ * `OldValues___`, and a value it supplies there for an update-denied field arrives through
260
+ * `LoadFromData`, which the `EntityField` setter records as that field's INITIAL value. The
261
+ * field is therefore not dirty, `BaseEntity.CheckFieldLevelUpdatePermissions` only rejects
262
+ * `field.Dirty && denied`, and `GenerateSaveSQL` sends every `IsSPParameter` field regardless of
263
+ * dirtiness (it skips only `NotLoaded`). The fabricated value reached `spUpdate` having passed
264
+ * every check: pinning a value in `OldValues___` was a write to a field the caller may not write.
265
+ *
266
+ * Forcing the truth-load closes that rather than relocating it. The entity is hydrated by
267
+ * `InnerLoad` from the real row, and `TestAndSetClientOldValuesToDBValues` reads the client's
268
+ * OldValues only to detect concurrent-edit overlap before ending in `SetMany(clientNewValues)` —
269
+ * it never applies them to the entity. An update-denied field can then only become dirty by being
270
+ * named in the mutation input itself, which is precisely the case that check does catch.
271
+ *
272
+ * Ordered so the boolean flag is evaluated last: the extra load lands only on entities that have
273
+ * the feature switched on, which is almost none of them.
274
+ */
275
+ protected MustLoadTruthFromDatabase(entityInfo: EntityInfo, input: {
276
+ OldValues___?: Array<{
277
+ Key: string;
278
+ Value: unknown;
279
+ }>;
280
+ }, hasDeniedReadFields: boolean, hasNarrowedAuditPayload: boolean): boolean;
281
+ /**
282
+ * Field-level security guard for the update path.
283
+ *
284
+ * Every denied-read field is stripped — new values AND OldValues. A field the user cannot
285
+ * read was absent from every payload that client ever received, so any value coming back for
286
+ * it is the transport's invention rather than user intent, and applying it would silently
287
+ * overwrite the real column. Stripping is what makes "load a record, edit an unrelated field,
288
+ * save" safe for a restricted user.
289
+ *
290
+ * There is no split by update permission: Read is required for Update, so a user denied read
291
+ * is denied update too and denied-read ∩ denied-update is just denied-read.
292
+ *
293
+ * Silent narrowing, not rejection — consistent with the output projection the client already
294
+ * experiences, and with the ambiguous-error rule (naming the field would confirm it exists
295
+ * and is restricted).
296
+ *
297
+ * Returns true when the user has a non-empty denied-read set on this entity. The caller
298
+ * must then hydrate the entity from the DATABASE (never from client OldValues), so denied
299
+ * fields hold true values that an omitted key leaves untouched.
300
+ */
301
+ protected StripDeniedReadFieldsFromClientInput(entityInfo: EntityInfo, userInfo: UserInfo, input: {
302
+ OldValues___?: Array<{
303
+ Key: string;
304
+ Value: unknown;
305
+ }>;
306
+ } & Record<string, unknown>, clientNewValues: Record<string, unknown>): boolean;
307
+ /**
308
+ * Refuses a client-sent `MJ: Record Changes` payload column from a caller who carries field
309
+ * denials, and forces the update onto the load-truth-from-DB branch.
310
+ *
311
+ * This is the write half of the audit-trail projection, and without it that projection would
312
+ * itself destroy audit history. A restricted caller is served a NARROWED `ChangesJSON` /
313
+ * `FullRecordJSON` and no `ChangesDescription`. Those values hydrate a client-side entity as
314
+ * ordinary loaded values, and `GenerateSaveSQL` writes EVERY `IsSPParameter` field rather than
315
+ * only dirty ones — so a user who edits `Comments` on that record and saves would silently
316
+ * overwrite the stored payload with the narrowed one they were shown. The row would keep looking
317
+ * like a complete audit entry while the pruned fields were gone for everyone, permanently.
318
+ *
319
+ * The rule is the one field security already applies elsewhere: **a value a client could not
320
+ * have seen in full is not user intent, it is a transport artifact, and it is ignored.** Here
321
+ * that is widened from "fields the caller cannot read" to "the audit payload of a caller who
322
+ * carries any denial", because the column is readable — it is its CONTENTS that were narrowed,
323
+ * and which row's target entity did the narrowing is not knowable from the input alone.
324
+ *
325
+ * Returning true routes the caller through `InnerLoad`, so the stored values are restored from
326
+ * the database before the save. Legitimate payload writes are unaffected: they happen
327
+ * server-side (`SnapshotBuilder`, replay) and never through this resolver.
328
+ */
329
+ protected StripRecordChangePayloadFromClientInput(entityInfo: EntityInfo, userInfo: UserInfo, input: {
330
+ OldValues___?: Array<{
331
+ Key: string;
332
+ Value: unknown;
333
+ }>;
334
+ } & Record<string, unknown>, clientNewValues: Record<string, unknown>, provider?: IMetadataProvider): boolean;
220
335
  /**
221
336
  * 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
222
337
  * 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
@@ -1 +1 @@
1
- {"version":3,"file":"ResolverBase.d.ts","sourceRoot":"","sources":["../../src/generic/ResolverBase.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ResolverBase.d.ts","sourceRoot":"","sources":["../../src/generic/ResolverBase.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
- import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, LogDebug, LogError, LogStatus, Metadata, } from '@memberjunction/core';
1
+ import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, LogDebug, LogError, LogStatus, Metadata, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
2
2
  import { UserCache } from '@memberjunction/generic-database-provider';
3
3
  import { AuthorizationError } from 'type-graphql';
4
4
  import { GraphQLError } from 'graphql';
5
+ import { RefusalExtensions } from './refusalExtensions.js';
5
6
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
6
7
  import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
7
8
  import { DeleteOptionsInput } from './DeleteOptionsInput.js';
@@ -50,7 +51,7 @@ export class ResolverBase {
50
51
  * @param contextUser - Optional user context for decryption (required for encrypted fields)
51
52
  * @returns A new object in transport shape, or null when there is nothing to map
52
53
  */
53
- async MapFieldNamesToCodeNames(entityName, dataObject, contextUser, provider) {
54
+ async MapFieldNamesToCodeNames(entityName, dataObject, contextUser, provider, deniedReadFields, recordChangeProjector) {
54
55
  // Return null for empty objects (e.g. when no rows found due to RLS filtering)
55
56
  if (!dataObject || Object.keys(dataObject).length === 0) {
56
57
  return null;
@@ -68,17 +69,63 @@ export class ResolverBase {
68
69
  if (!entityInfo)
69
70
  throw new Error(`Entity ${entityName} not found in metadata`);
70
71
  // const fields = entityInfo.Fields.filter((f) => f.Name !== f.CodeName || f.Name.startsWith('__mj_'));
72
+ // FIELD-LEVEL SECURITY — this is the authoritative read boundary for every GraphQL
73
+ // return path (single-record resolvers, external-data-source loads, and anything else
74
+ // routed through here), sitting exactly where encryption masking already lives.
75
+ //
76
+ // The denied set is normally computed ONCE per (entity, user) by the caller and passed
77
+ // in: this method runs once per ROW, so resolving it here would cost fields x rows
78
+ // aggregations. We fall back to computing it when a caller doesn't supply it (the
79
+ // single-record resolvers, where "once per row" and "once per request" are the same
80
+ // thing) rather than failing open — a missing argument must never mean missing security.
81
+ const denied = deniedReadFields ?? (entityInfo.EnableFieldLevelSecurity && contextUser
82
+ ? entityInfo.GetDeniedReadFields(contextUser)
83
+ : null);
71
84
  const mapper = new FieldMapper();
72
85
  entityInfo.Fields.forEach((f) => {
73
86
  if (dataObject.hasOwnProperty(f.Name)) {
74
87
  // GraphQL doesn't allow us to pass back fields with __ so we are mapping our special field cases that start with __mj_ to _mj__ for transport - they are converted back on the other side automatically
75
88
  const mappedFieldName = mapper.MapFieldName(f.CodeName);
89
+ if (denied?.has(f.Name.trim().toLowerCase())) {
90
+ // Omit entirely rather than nulling — a null is indistinguishable from a real
91
+ // null value, and the client should see the field as absent, not as empty.
92
+ // Both key shapes are removed because callers reach this method with rows keyed
93
+ // either way depending on whether mapping has already run.
94
+ delete dataObject[f.Name];
95
+ delete dataObject[mappedFieldName];
96
+ return;
97
+ }
76
98
  if (mappedFieldName !== f.Name) {
77
99
  dataObject[mappedFieldName] = dataObject[f.Name];
78
100
  delete dataObject[f.Name];
79
101
  }
80
102
  }
81
103
  });
104
+ // Field security, second half: say IN-BAND which fields this caller may read. Deleting the
105
+ // key above is not enough on its own — GraphQL emits every SELECTED field regardless, so a
106
+ // denied field the client asked for arrives as an explicit null it cannot tell apart from a
107
+ // genuine one. The client must not settle that from its own metadata: during the window
108
+ // after a permission change its copy disagrees with ours, and once metadata filtering lands
109
+ // (issue #3485) it may not hold the permission rows at all. Null for unrestricted callers,
110
+ // which is nearly every request.
111
+ const readableFields = this.BuildReadableFieldsTransportValue(entityInfo, denied);
112
+ if (readableFields) {
113
+ dataObject[ReadableFieldsTransportKey] = readableFields;
114
+ }
115
+ // Field security, third part — the audit trail. A 'MJ: Record Changes' row carries the old
116
+ // and new values of ANOTHER entity's fields, and its own EnableFieldLevelSecurity flag is
117
+ // off, so everything above is a no-op for it and the payload would flow out untouched. The
118
+ // projector recomputes the denied set against the entity each row is about; see
119
+ // RecordChangeFieldSecurityProjector for what each payload column gets.
120
+ //
121
+ // Same "a missing argument must never mean missing security" rule as the denied set above:
122
+ // the projector is normally built ONCE per request by the array caller and passed in, and
123
+ // we build one here when it wasn't — the single-record resolvers, where once-per-row and
124
+ // once-per-request are the same thing.
125
+ if (contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
126
+ const projector = recordChangeProjector ?? new RecordChangeFieldSecurityProjector(md, contextUser);
127
+ dataObject = projector.ProjectRow(dataObject);
128
+ }
82
129
  // Handle encrypted fields - data from raw SQL queries is still encrypted
83
130
  const encryptedFields = entityInfo.EncryptedFields;
84
131
  if (encryptedFields.length > 0) {
@@ -177,12 +224,47 @@ export class ResolverBase {
177
224
  if (!dataObjectArray || dataObjectArray.length === 0) {
178
225
  return dataObjectArray;
179
226
  }
227
+ // Resolve the field-security denied set ONCE for the whole array and pass it into the
228
+ // per-row mapper. MapFieldNamesToCodeNames runs once per row, so computing this inside
229
+ // the loop would be fields x rows aggregations — 40,000 for a 1,000-row x 40-column
230
+ // result, each re-scanning the user's roles and allocating. Gated on the entity-level
231
+ // flag so non-FLS entities (nearly all of them) don't even resolve the entity twice.
232
+ const md = provider ?? new Metadata();
233
+ const entityInfo = md.EntityByName(entityName);
234
+ const deniedReadFields = entityInfo?.EnableFieldLevelSecurity && contextUser
235
+ ? entityInfo.GetDeniedReadFields(contextUser)
236
+ : undefined;
237
+ // Record Changes payload security is resolved per DISTINCT target entity, and one array can
238
+ // span many — so the projector, which owns that memo, must outlive the row loop. Built once
239
+ // here for the same reason the denied set above is: GetDeniedReadFields walks every field on
240
+ // an entity and must never be called per row.
241
+ const recordChangeProjector = contextUser && RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)
242
+ ? new RecordChangeFieldSecurityProjector(md, contextUser)
243
+ : undefined;
180
244
  const mapped = [];
181
245
  for (const element of dataObjectArray) {
182
- mapped.push(await this.MapFieldNamesToCodeNames(entityName, element, contextUser, provider));
246
+ mapped.push(await this.MapFieldNamesToCodeNames(entityName, element, contextUser, provider, deniedReadFields, recordChangeProjector));
183
247
  }
184
248
  return mapped;
185
249
  }
250
+ /**
251
+ * The CodeNames this user may read on this entity, for the field-security transport key — or
252
+ * null when there is nothing to state (no denials, so every field is readable and the client
253
+ * needs no help distinguishing a withheld null from a genuine one).
254
+ *
255
+ * Carries entity field NAMES, not CodeNames and not the `_mj__` transport shape. The client
256
+ * consumes this after it has already reversed the transport mapping, and it matches names
257
+ * against `EntityFieldInfo.Name` — the same key the denied set itself is built from. Sending
258
+ * the one shape both sides already agree on avoids a second mapping that could drift.
259
+ */
260
+ BuildReadableFieldsTransportValue(entityInfo, deniedReadFields) {
261
+ if (!deniedReadFields || deniedReadFields.size === 0) {
262
+ return null;
263
+ }
264
+ return entityInfo.Fields
265
+ .filter((f) => !deniedReadFields.has(f.Name.trim().toLowerCase()))
266
+ .map((f) => f.Name);
267
+ }
186
268
  /**
187
269
  * Loads a single external-data-source-backed entity record by primary key and returns it in
188
270
  * GraphQL field-name (CodeName) shape, or null if not found.
@@ -680,13 +762,29 @@ export class ResolverBase {
680
762
  }
681
763
  /**
682
764
  * Applies {@link assertClientClauseUsesEntityBaseViews} to every client-supplied clause
683
- * a view request can carry. GraphQL entry points (RunViewByName, RunViewByID,
684
- * RunDynamicView, RunViews) funnel through RunViewGenericInternal / RunViewsGenericInternal.
765
+ * a view request can carry that is actually a SQL fragment. GraphQL entry points
766
+ * (RunViewByName, RunViewByID, RunDynamicView, RunViews) funnel through
767
+ * RunViewGenericInternal / RunViewsGenericInternal.
768
+ *
769
+ * 🚨 `UserSearchString` is deliberately NOT screened here (#4392). It is not a clause — it is
770
+ * the free text a person typed into a search box, and it never reaches SQL as a fragment.
771
+ * `GenericDatabaseProvider.createViewUserSearchSQL` builds the predicate itself from
772
+ * `IncludeInUserSearchAPI` metadata and lands the text only as a literal, doubling single
773
+ * quotes and escaping LIKE metacharacters with an explicit `ESCAPE`. Running it through a
774
+ * screen that parses its argument as SQL and fails closed rejected every term that is not
775
+ * coincidentally valid SQL: `Marcus Chen` parses as nothing, `O'Leary` as an unterminated
776
+ * literal — so essentially every real name search returned 0 rows.
777
+ *
778
+ * The provider-level denylist does NOT back this up — it was removed from `UserSearchString`
779
+ * in the same change, for the same reason (it refused `Union Pacific`). What protects the value
780
+ * is that it is never SQL: it reaches the database only as a quoted, quote-doubled literal.
781
+ * The single place that is not true is a field carrying `UserSearchParamFormatAPI`, whose
782
+ * admin-authored format may splice the term in unquoted; `createViewUserSearchSQL` re-applies
783
+ * `ValidateUserProvidedSQLClause` for exactly those entities.
685
784
  */
686
785
  screenClientViewClauses(clauses, provider) {
687
786
  this.assertClientClauseUsesEntityBaseViews(clauses.extraFilter, 'ExtraFilter', provider);
688
787
  this.assertClientClauseUsesEntityBaseViews(clauses.orderBy, 'OrderBy', provider);
689
- this.assertClientClauseUsesEntityBaseViews(clauses.userSearchString, 'UserSearchString', provider);
690
788
  this.assertClientClauseUsesEntityBaseViews(clauses.overrideExcludeFilter, 'OverrideExcludeFilter', provider);
691
789
  }
692
790
  /** Same write-node walk as EDS `sqlReadOnlyScreen.astContainsWriteNode`. */
@@ -1160,8 +1258,10 @@ export class ResolverBase {
1160
1258
  }
1161
1259
  // save failed, throw error with message
1162
1260
  else {
1261
+ // The message is the prose a toast shows; `validationErrors` is the same refusal with its
1262
+ // field names intact, so the client can paint the fields a server-side ValidateAsync named.
1163
1263
  throw new GraphQLError(entityObject.LatestResult?.CompleteMessage ?? 'Unknown error creating record', {
1164
- extensions: { code: 'CREATE_ENTITY_ERROR', entityName },
1264
+ extensions: RefusalExtensions('CREATE_ENTITY_ERROR', entityName, entityObject.LatestResult),
1165
1265
  });
1166
1266
  }
1167
1267
  }
@@ -1190,8 +1290,20 @@ export class ResolverBase {
1190
1290
  clientNewValues[key] = input[key];
1191
1291
  }
1192
1292
  });
1193
- if (entityInfo.TrackRecordChanges || !input.OldValues___) {
1194
- // We get here because EITHER the entity tracks record changes OR the client did not provide OldValues, so we need to load the old values from the DB
1293
+ // Field-level security: any value the client sends for a field it cannot READ is
1294
+ // fabricated by construction (the field was stripped from every payload the client ever
1295
+ // received), so strip such values before anything applies them. When any field is
1296
+ // denied we also force the truth-load branch below — client OldValues for denied fields
1297
+ // are equally fabricated, so hydrating from them (the no-DB-load path) would write
1298
+ // fabricated state into the denied columns on save.
1299
+ const hasDeniedReadFields = this.StripDeniedReadFieldsFromClientInput(entityInfo, userInfo, input, clientNewValues);
1300
+ // The audit-trail equivalent. Its `true` matters most on THIS entity: 'MJ: Record Changes'
1301
+ // has TrackRecordChanges off, so without it a client sending OldValues takes the branch below
1302
+ // that hydrates from those values — never loading what the database actually holds.
1303
+ const hasNarrowedAuditPayload = this.StripRecordChangePayloadFromClientInput(entityInfo, userInfo, input, clientNewValues);
1304
+ if (this.MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload)) {
1305
+ // We get here because the entity tracks record changes, OR the client did not provide OldValues,
1306
+ // OR field-level security is in play — in every case we need the true old values from the DB
1195
1307
  const cKey = new CompositeKey(entityInfo.PrimaryKeys.map((pk) => {
1196
1308
  return {
1197
1309
  FieldName: pk.Name,
@@ -1248,7 +1360,7 @@ export class ResolverBase {
1248
1360
  // lost, and it still yields undefined when there is genuinely nothing to say, leaving the
1249
1361
  // fallback below to fire rather than showing the user a blank error.
1250
1362
  throw new GraphQLError(entityObject.LatestResult?.CompleteMessage ?? 'Unknown error', {
1251
- extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
1363
+ extensions: RefusalExtensions('SAVE_ENTITY_ERROR', entityName, entityObject.LatestResult),
1252
1364
  });
1253
1365
  }
1254
1366
  }
@@ -1257,6 +1369,147 @@ export class ResolverBase {
1257
1369
  extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
1258
1370
  });
1259
1371
  }
1372
+ /**
1373
+ * Whether `UpdateRecord` must hydrate the entity from the DATABASE rather than from the
1374
+ * client's `OldValues___`.
1375
+ *
1376
+ * The `OldValues___` path exists as an optimization: when nothing needs the true prior state,
1377
+ * the client already holds it and a round trip is wasted. Each condition below is a reason
1378
+ * that assumption fails.
1379
+ *
1380
+ * `EnableFieldLevelSecurity` is the security-critical one, and it is NOT redundant with
1381
+ * `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
1382
+ * Read Allow + Update Deny — which leaves it false. Such a caller would be hydrated from its own
1383
+ * `OldValues___`, and a value it supplies there for an update-denied field arrives through
1384
+ * `LoadFromData`, which the `EntityField` setter records as that field's INITIAL value. The
1385
+ * field is therefore not dirty, `BaseEntity.CheckFieldLevelUpdatePermissions` only rejects
1386
+ * `field.Dirty && denied`, and `GenerateSaveSQL` sends every `IsSPParameter` field regardless of
1387
+ * dirtiness (it skips only `NotLoaded`). The fabricated value reached `spUpdate` having passed
1388
+ * every check: pinning a value in `OldValues___` was a write to a field the caller may not write.
1389
+ *
1390
+ * Forcing the truth-load closes that rather than relocating it. The entity is hydrated by
1391
+ * `InnerLoad` from the real row, and `TestAndSetClientOldValuesToDBValues` reads the client's
1392
+ * OldValues only to detect concurrent-edit overlap before ending in `SetMany(clientNewValues)` —
1393
+ * it never applies them to the entity. An update-denied field can then only become dirty by being
1394
+ * named in the mutation input itself, which is precisely the case that check does catch.
1395
+ *
1396
+ * Ordered so the boolean flag is evaluated last: the extra load lands only on entities that have
1397
+ * the feature switched on, which is almost none of them.
1398
+ */
1399
+ MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload) {
1400
+ return (entityInfo.TrackRecordChanges ||
1401
+ !input.OldValues___ ||
1402
+ hasDeniedReadFields ||
1403
+ hasNarrowedAuditPayload ||
1404
+ entityInfo.EnableFieldLevelSecurity);
1405
+ }
1406
+ /**
1407
+ * Field-level security guard for the update path.
1408
+ *
1409
+ * Every denied-read field is stripped — new values AND OldValues. A field the user cannot
1410
+ * read was absent from every payload that client ever received, so any value coming back for
1411
+ * it is the transport's invention rather than user intent, and applying it would silently
1412
+ * overwrite the real column. Stripping is what makes "load a record, edit an unrelated field,
1413
+ * save" safe for a restricted user.
1414
+ *
1415
+ * There is no split by update permission: Read is required for Update, so a user denied read
1416
+ * is denied update too and denied-read ∩ denied-update is just denied-read.
1417
+ *
1418
+ * Silent narrowing, not rejection — consistent with the output projection the client already
1419
+ * experiences, and with the ambiguous-error rule (naming the field would confirm it exists
1420
+ * and is restricted).
1421
+ *
1422
+ * Returns true when the user has a non-empty denied-read set on this entity. The caller
1423
+ * must then hydrate the entity from the DATABASE (never from client OldValues), so denied
1424
+ * fields hold true values that an omitted key leaves untouched.
1425
+ */
1426
+ StripDeniedReadFieldsFromClientInput(entityInfo, userInfo, input, clientNewValues) {
1427
+ if (!entityInfo.EnableFieldLevelSecurity || !userInfo) {
1428
+ return false;
1429
+ }
1430
+ const deniedReadNames = entityInfo.GetDeniedReadFields(userInfo);
1431
+ if (deniedReadNames.size === 0) {
1432
+ return false;
1433
+ }
1434
+ // Input keys are entity CodeNames (post ReverseMapInputFieldNames); the denied set holds
1435
+ // lowercased field Names — bridge via the field metadata once.
1436
+ const deniedReadCodeNames = new Set();
1437
+ for (const field of entityInfo.Fields) {
1438
+ if (deniedReadNames.has(field.Name.trim().toLowerCase())) {
1439
+ deniedReadCodeNames.add(field.CodeName.trim().toLowerCase());
1440
+ }
1441
+ }
1442
+ const stripped = [];
1443
+ for (const key of Object.keys(clientNewValues)) {
1444
+ if (deniedReadCodeNames.has(key.trim().toLowerCase())) {
1445
+ delete clientNewValues[key];
1446
+ delete input[key];
1447
+ stripped.push(key);
1448
+ }
1449
+ }
1450
+ if (Array.isArray(input.OldValues___)) {
1451
+ input.OldValues___ = input.OldValues___.filter((item) => !deniedReadCodeNames.has(String(item.Key).trim().toLowerCase()));
1452
+ }
1453
+ if (stripped.length > 0) {
1454
+ LogDebug(`[FieldSecurity] UpdateRecord on '${entityInfo.Name}' for user ${userInfo.Email}: ` +
1455
+ `stripped client-sent value(s) for denied-read field(s) ${stripped.join(', ')}`);
1456
+ }
1457
+ return true;
1458
+ }
1459
+ /**
1460
+ * Refuses a client-sent `MJ: Record Changes` payload column from a caller who carries field
1461
+ * denials, and forces the update onto the load-truth-from-DB branch.
1462
+ *
1463
+ * This is the write half of the audit-trail projection, and without it that projection would
1464
+ * itself destroy audit history. A restricted caller is served a NARROWED `ChangesJSON` /
1465
+ * `FullRecordJSON` and no `ChangesDescription`. Those values hydrate a client-side entity as
1466
+ * ordinary loaded values, and `GenerateSaveSQL` writes EVERY `IsSPParameter` field rather than
1467
+ * only dirty ones — so a user who edits `Comments` on that record and saves would silently
1468
+ * overwrite the stored payload with the narrowed one they were shown. The row would keep looking
1469
+ * like a complete audit entry while the pruned fields were gone for everyone, permanently.
1470
+ *
1471
+ * The rule is the one field security already applies elsewhere: **a value a client could not
1472
+ * have seen in full is not user intent, it is a transport artifact, and it is ignored.** Here
1473
+ * that is widened from "fields the caller cannot read" to "the audit payload of a caller who
1474
+ * carries any denial", because the column is readable — it is its CONTENTS that were narrowed,
1475
+ * and which row's target entity did the narrowing is not knowable from the input alone.
1476
+ *
1477
+ * Returning true routes the caller through `InnerLoad`, so the stored values are restored from
1478
+ * the database before the save. Legitimate payload writes are unaffected: they happen
1479
+ * server-side (`SnapshotBuilder`, replay) and never through this resolver.
1480
+ */
1481
+ StripRecordChangePayloadFromClientInput(entityInfo, userInfo, input, clientNewValues, provider) {
1482
+ if (!userInfo || !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
1483
+ return false;
1484
+ }
1485
+ const md = provider ?? new Metadata();
1486
+ if (!new RecordChangeFieldSecurityProjector(md, userInfo).CallerCarriesAnyDenial()) {
1487
+ return false; // this caller was served the payload whole, so what they send back is theirs
1488
+ }
1489
+ const payloadKeys = new Set(RecordChangePayloadFields.map((f) => f.toLowerCase()));
1490
+ const stripped = [];
1491
+ for (const key of Object.keys(clientNewValues)) {
1492
+ if (payloadKeys.has(key.trim().toLowerCase())) {
1493
+ delete clientNewValues[key];
1494
+ delete input[key];
1495
+ stripped.push(key);
1496
+ }
1497
+ }
1498
+ // OldValues too: a narrowed payload in the client's old values is not what the database holds,
1499
+ // so leaving it would read as a concurrent-edit conflict on a field nobody edited.
1500
+ if (Array.isArray(input.OldValues___)) {
1501
+ input.OldValues___ = input.OldValues___.filter((item) => !payloadKeys.has(String(item.Key).trim().toLowerCase()));
1502
+ }
1503
+ if (stripped.length > 0) {
1504
+ LogDebug(`[FieldSecurity] UpdateRecord on '${entityInfo.Name}' for user ${userInfo.Email}: ` +
1505
+ `ignored client-sent audit payload column(s) ${stripped.join(', ')} — the caller carries field ` +
1506
+ `denials, so the value they hold may be a narrowed projection rather than the stored one`);
1507
+ }
1508
+ // True even when nothing was stripped: the DB truth-load is what guarantees the payload the
1509
+ // save writes is the stored one, and a client that sent no payload key at all would otherwise
1510
+ // fall through to the OldValues branch and write whatever it did send.
1511
+ return true;
1512
+ }
1260
1513
  /**
1261
1514
  * 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
1262
1515
  * 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
@@ -1462,7 +1715,7 @@ export class ResolverBase {
1462
1715
  // reason on LatestResult, and #3971 proposes a first-class delete-validation seam. Every one
1463
1716
  // of those reasons was being replaced by 'Unknown error' at the API boundary.
1464
1717
  throw new GraphQLError(entityObject.LatestResult?.CompleteMessage ?? 'Unknown error', {
1465
- extensions: { code: 'DELETE_ENTITY_ERROR', entityName },
1718
+ extensions: RefusalExtensions('DELETE_ENTITY_ERROR', entityName, entityObject.LatestResult),
1466
1719
  });
1467
1720
  }
1468
1721
  }