@memberjunction/server 6.1.5 → 6.1.6

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 (189) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/agentSessions/SessionManager.d.ts +5 -2
  3. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  4. package/dist/agentSessions/SessionManager.js +17 -8
  5. package/dist/agentSessions/SessionManager.js.map +1 -1
  6. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  7. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  8. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  9. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  10. package/dist/agentSessions/index.d.ts +1 -0
  11. package/dist/agentSessions/index.d.ts.map +1 -1
  12. package/dist/agentSessions/index.js +1 -0
  13. package/dist/agentSessions/index.js.map +1 -1
  14. package/dist/auth/actionAuthorization.d.ts +40 -0
  15. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  16. package/dist/auth/actionAuthorization.js +94 -0
  17. package/dist/auth/actionAuthorization.js.map +1 -0
  18. package/dist/auth/exampleNewUserSubClass.js +2 -1
  19. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  20. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  21. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  22. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  23. package/dist/auth/newUsers.d.ts.map +1 -1
  24. package/dist/auth/newUsers.js +15 -11
  25. package/dist/auth/newUsers.js.map +1 -1
  26. package/dist/context.d.ts +6 -0
  27. package/dist/context.d.ts.map +1 -1
  28. package/dist/context.js +13 -11
  29. package/dist/context.js.map +1 -1
  30. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  31. package/dist/generated/graphql-schemas/__mj.js +3417 -3428
  32. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  33. package/dist/generic/CacheInvalidationResolver.d.ts +44 -8
  34. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  35. package/dist/generic/CacheInvalidationResolver.js +59 -19
  36. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  37. package/dist/generic/ResolverBase.d.ts +95 -11
  38. package/dist/generic/ResolverBase.d.ts.map +1 -1
  39. package/dist/generic/ResolverBase.js +186 -76
  40. package/dist/generic/ResolverBase.js.map +1 -1
  41. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  42. package/dist/generic/RunViewResolver.js +12 -0
  43. package/dist/generic/RunViewResolver.js.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +8 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/isolatedProvider.d.ts +20 -0
  49. package/dist/isolatedProvider.d.ts.map +1 -0
  50. package/dist/isolatedProvider.js +22 -0
  51. package/dist/isolatedProvider.js.map +1 -0
  52. package/dist/multiTenancy/index.d.ts.map +1 -1
  53. package/dist/multiTenancy/index.js +22 -2
  54. package/dist/multiTenancy/index.js.map +1 -1
  55. package/dist/realtimeWidget/widgetGuestElevation.d.ts +4 -2
  56. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  57. package/dist/realtimeWidget/widgetGuestElevation.js +4 -2
  58. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  59. package/dist/resolvers/ActionResolver.d.ts +31 -5
  60. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  61. package/dist/resolvers/ActionResolver.js +75 -14
  62. package/dist/resolvers/ActionResolver.js.map +1 -1
  63. package/dist/resolvers/AdhocQueryResolver.d.ts +28 -0
  64. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  65. package/dist/resolvers/AdhocQueryResolver.js +46 -0
  66. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  67. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  68. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  69. package/dist/resolvers/DatasetResolver.js +57 -8
  70. package/dist/resolvers/DatasetResolver.js.map +1 -1
  71. package/dist/resolvers/EntityRecordNameResolver.d.ts +8 -23
  72. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  73. package/dist/resolvers/EntityRecordNameResolver.js +11 -48
  74. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  75. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
  76. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  77. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  78. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  79. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  80. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  81. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  82. package/dist/resolvers/GetDataResolver.js +13 -1
  83. package/dist/resolvers/GetDataResolver.js.map +1 -1
  84. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  85. package/dist/resolvers/QuerySystemUserResolver.js +2 -1
  86. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  87. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +39 -16
  88. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  89. package/dist/resolvers/RealtimeClientSessionResolver.js +84 -38
  90. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  91. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  92. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  93. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  94. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  95. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  96. package/dist/resolvers/RunTemplateResolver.js +12 -11
  97. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  98. package/dist/resolvers/SearchKnowledgeResolver.d.ts +1 -0
  99. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  100. package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
  101. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  102. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
  103. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
  104. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
  105. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  106. package/dist/resolvers/SyncDataResolver.js +6 -2
  107. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  108. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  109. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  110. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  111. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  112. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  113. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  114. package/dist/rest/ViewOperationsHandler.js +29 -17
  115. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  116. package/dist/services/TaskGraphActionRunner.d.ts +8 -0
  117. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  118. package/dist/services/TaskGraphActionRunner.js +27 -1
  119. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  120. package/package.json +92 -92
  121. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  122. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +4 -2
  123. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  124. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  125. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  126. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  127. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  128. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  129. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  130. package/src/__tests__/RealtimeClientSessionResolver.test.ts +106 -41
  131. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  132. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +227 -0
  133. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  134. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  135. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  136. package/src/__tests__/ResolverBase.filterEscaping.test.ts +24 -2
  137. package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
  138. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  139. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  140. package/src/__tests__/SessionManager.test.ts +94 -17
  141. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  142. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -0
  143. package/src/__tests__/TaskGraphActionRunner.storableOutputs.test.ts +76 -0
  144. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  145. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  146. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  147. package/src/__tests__/actionAuthorization.test.ts +130 -0
  148. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  149. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  150. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  151. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  152. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  153. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  154. package/src/__tests__/multiTenancy.test.ts +1 -1
  155. package/src/__tests__/newUsers.test.ts +62 -5
  156. package/src/agentSessions/SessionManager.ts +23 -25
  157. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  158. package/src/agentSessions/index.ts +1 -0
  159. package/src/auth/actionAuthorization.ts +119 -0
  160. package/src/auth/exampleNewUserSubClass.ts +2 -2
  161. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  162. package/src/auth/newUsers.ts +15 -11
  163. package/src/context.ts +14 -10
  164. package/src/generated/graphql-schemas/__mj.ts +3417 -3428
  165. package/src/generic/CacheInvalidationResolver.ts +74 -19
  166. package/src/generic/ResolverBase.ts +207 -80
  167. package/src/generic/RunViewResolver.ts +17 -0
  168. package/src/index.ts +9 -1
  169. package/src/isolatedProvider.ts +22 -0
  170. package/src/multiTenancy/index.ts +24 -2
  171. package/src/realtimeWidget/widgetGuestElevation.ts +4 -2
  172. package/src/resolvers/ActionResolver.ts +84 -21
  173. package/src/resolvers/AdhocQueryResolver.ts +49 -1
  174. package/src/resolvers/DatasetResolver.ts +70 -10
  175. package/src/resolvers/EntityRecordNameResolver.ts +11 -52
  176. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
  177. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  178. package/src/resolvers/GetDataResolver.ts +14 -1
  179. package/src/resolvers/QuerySystemUserResolver.ts +2 -1
  180. package/src/resolvers/RealtimeClientSessionResolver.ts +103 -42
  181. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  182. package/src/resolvers/RunTemplateResolver.ts +13 -12
  183. package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
  184. package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
  185. package/src/resolvers/SyncDataResolver.ts +6 -2
  186. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  187. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  188. package/src/rest/ViewOperationsHandler.ts +37 -18
  189. package/src/services/TaskGraphActionRunner.ts +29 -1
@@ -0,0 +1,119 @@
1
+ import { AuthorizationInfo, AuthorizationRoleInfo, AuthorizationRoleType, IMetadataProvider, UserInfo } from '@memberjunction/core';
2
+ import { NormalizeUUID, UUIDsEqual } from '@memberjunction/global';
3
+
4
+ /** Authorizations keyed by their normalized ID. */
5
+ type AuthorizationsByID = Map<string, AuthorizationInfo>;
6
+
7
+ /** The outcome of {@link EvaluateActionRunAuthorization}. */
8
+ export interface ActionRunAuthorization {
9
+ /** True when the user may run the action. */
10
+ Allowed: boolean;
11
+ /** Why the run is allowed or refused. */
12
+ Reason: string;
13
+ }
14
+
15
+ /** One provider's authorization tree and the role grants on it. */
16
+ export type AuthorizationMetadata = Pick<IMetadataProvider, 'Authorizations' | 'AuthorizationRoles'>;
17
+
18
+ /**
19
+ * True when the user is an Owner, MJ's superuser type. The column is nchar, so the stored value is
20
+ * padded.
21
+ */
22
+ export function IsOwner(user: UserInfo): boolean {
23
+ return user.Type?.trim().toLowerCase() === 'owner';
24
+ }
25
+
26
+ /**
27
+ * Decides whether a user may run an action through a public entry point such as the `RunAction`
28
+ * mutation. It fails closed:
29
+ *
30
+ * - An Owner may run any action.
31
+ * - An action with `MJ: Action Authorizations` rows runs for a user who holds at least one of the
32
+ * linked authorizations. A linked authorization counts only when it is active and none of the
33
+ * user's roles is denied it; the user may then hold it directly or through an ancestor.
34
+ * - An action with no such rows runs for any user when its class authorizes its caller itself
35
+ * (`BaseAction.AuthorizesCaller`), so the action's own check decides. Otherwise it runs for
36
+ * Owners only.
37
+ *
38
+ * The authorizations, their parents and their role grants are read only from `metadata`, never
39
+ * from the global provider. Pass the provider the `MJ: Action Authorizations` rows were read from.
40
+ *
41
+ * Refuse scope-limited sessions before calling this; see `IsScopeLimitedPrincipal`.
42
+ *
43
+ * @param user the caller
44
+ * @param actionAuthorizationIDs the AuthorizationID of each `MJ: Action Authorizations` row for the action
45
+ * @param metadata the provider whose authorizations and role grants decide
46
+ * @param actionAuthorizesCaller true when the action's class authorizes its caller itself
47
+ * @returns whether the run is allowed, and why
48
+ */
49
+ export function EvaluateActionRunAuthorization(
50
+ user: UserInfo,
51
+ actionAuthorizationIDs: readonly string[],
52
+ metadata: AuthorizationMetadata,
53
+ actionAuthorizesCaller: boolean
54
+ ): ActionRunAuthorization {
55
+ if (IsOwner(user)) {
56
+ return { Allowed: true, Reason: 'The user is an Owner.' };
57
+ }
58
+ if (actionAuthorizationIDs.length === 0) {
59
+ return actionAuthorizesCaller
60
+ ? { Allowed: true, Reason: 'The action authorizes its caller itself.' }
61
+ : { Allowed: false, Reason: 'No Action Authorization grants this action to any role, and the action does not authorize its caller itself; only an Owner may run it.' };
62
+ }
63
+ const authorizationsByID: AuthorizationsByID = new Map(metadata.Authorizations.map((auth) => [NormalizeUUID(auth.ID), auth]));
64
+ const userGrants = grantsToUserRoles(user, metadata.AuthorizationRoles);
65
+ const granted = actionAuthorizationIDs
66
+ .map((id) => authorizationsByID.get(NormalizeUUID(id)))
67
+ .filter((auth): auth is AuthorizationInfo => auth !== undefined)
68
+ .find((auth) => holdsLinkedAuthorization(auth, authorizationsByID, userGrants));
69
+ return granted
70
+ ? { Allowed: true, Reason: `The user holds the "${granted.Name}" authorization.` }
71
+ : { Allowed: false, Reason: 'None of your roles holds an authorization this action requires.' };
72
+ }
73
+
74
+ /** The role grants that name one of the user's roles. */
75
+ function grantsToUserRoles(user: UserInfo, grants: readonly AuthorizationRoleInfo[]): AuthorizationRoleInfo[] {
76
+ const userRoleIDs = new Set((user.UserRoles ?? []).map((userRole) => NormalizeUUID(userRole.RoleID)));
77
+ return grants.filter((grant) => userRoleIDs.has(NormalizeUUID(grant.RoleID)));
78
+ }
79
+
80
+ /**
81
+ * True when the user holds a linked authorization: it is active, none of the user's roles is denied
82
+ * it, and the user holds it directly or through an ancestor. The first two are checked on the linked
83
+ * authorization itself, because the ancestor walk would otherwise grant it through the parent.
84
+ */
85
+ function holdsLinkedAuthorization(auth: AuthorizationInfo, authorizationsByID: AuthorizationsByID, userGrants: readonly AuthorizationRoleInfo[]): boolean {
86
+ if (!auth.IsActive || grantTypes(auth, userGrants).includes(AuthorizationRoleType.Deny)) {
87
+ return false;
88
+ }
89
+ return holdsDirectlyOrThroughAncestor(auth, authorizationsByID, userGrants);
90
+ }
91
+
92
+ /**
93
+ * True when the user holds the authorization or one of its ancestors, following `ParentID` within
94
+ * `authorizationsByID`. This is the walk `AuthorizationEvaluator.UserCanExecuteWithAncestors` does,
95
+ * with the role grants taken from the same provider as the authorizations.
96
+ */
97
+ function holdsDirectlyOrThroughAncestor(auth: AuthorizationInfo, authorizationsByID: AuthorizationsByID, userGrants: readonly AuthorizationRoleInfo[]): boolean {
98
+ const visited = new Set<string>();
99
+ let node: AuthorizationInfo | undefined = auth;
100
+ while (node && !visited.has(NormalizeUUID(node.ID))) {
101
+ if (holdsDirectly(node, userGrants)) {
102
+ return true;
103
+ }
104
+ visited.add(NormalizeUUID(node.ID));
105
+ node = node.ParentID ? authorizationsByID.get(NormalizeUUID(node.ParentID)) : undefined;
106
+ }
107
+ return false;
108
+ }
109
+
110
+ /** True when the authorization is active and the user's roles have an Allow and no Deny on it. */
111
+ function holdsDirectly(auth: AuthorizationInfo, userGrants: readonly AuthorizationRoleInfo[]): boolean {
112
+ const types = grantTypes(auth, userGrants);
113
+ return auth.IsActive && types.includes(AuthorizationRoleType.Allow) && !types.includes(AuthorizationRoleType.Deny);
114
+ }
115
+
116
+ /** Allow or Deny, for each of the user's role grants on the authorization. */
117
+ function grantTypes(auth: AuthorizationInfo, userGrants: readonly AuthorizationRoleInfo[]): AuthorizationRoleType[] {
118
+ return userGrants.filter((grant) => UUIDsEqual(grant.AuthorizationID, auth.ID)).map((grant) => grant.AuthorizationType());
119
+ }
@@ -1,4 +1,4 @@
1
- import { RegisterClass } from '@memberjunction/global';
1
+ import { EscapeSQLString, RegisterClass } from '@memberjunction/global';
2
2
  import { Metadata, RunView, LogError, EntitySaveOptions } from '@memberjunction/core';
3
3
  import { NewUserBase } from './newUsers.js';
4
4
  import { ResolveConfiguredPrincipal } from './principals.js';
@@ -43,7 +43,7 @@ export class ExampleNewUserSubClass extends NewUserBase {
43
43
  const viewResults = await rv.RunView(
44
44
  {
45
45
  EntityName: 'Persons',
46
- ExtraFilter: `Email = '${email}'`,
46
+ ExtraFilter: `Email = '${EscapeSQLString(email)}'`,
47
47
  },
48
48
  contextUser
49
49
  );
@@ -34,6 +34,7 @@ import { ResolveConfiguredPrincipal } from '../principals.js';
34
34
  import { CommunicationEngine } from '@memberjunction/communication-engine';
35
35
  import { Message } from '@memberjunction/communication-types';
36
36
  import { configInfo, type MagicLinkConfig } from '../../config.js';
37
+ import { CreateIsolatedProvider } from '../../isolatedProvider.js';
37
38
  import { MagicLinkKeyManager } from './MagicLinkKeys.js';
38
39
  import { generateRawToken, generateSessionId, hashToken, evaluateInvite, buildSessionClaims, buildConsumeInvitePostgresSQL, canIssueInvites, isRoleGrantable, MAGIC_LINK_TOKEN_PREFIX } from './magicLinkCore.js';
39
40
  import type {
@@ -521,9 +522,11 @@ export class MagicLinkService {
521
522
  /** Creates a new user with exactly the restricted role + single app, transactionally. */
522
523
  private async createScopedUser(invite: MJMagicLinkInviteEntity, role: RoleInfo, contextUser: UserInfo): Promise<ProvisionResult> {
523
524
  // Single provider for BOTH the transaction and the entity writes, so the
524
- // BeginTransaction/Save/Commit are provably on the same connection.
525
- // global-provider-ok: redemption runs in the pre-auth flow, no per-request provider yet.
526
- const provider = Metadata.Provider as DatabaseProviderBase; // global-provider-ok: server-side magic-link service; runs under the server's single default provider
525
+ // BeginTransaction/Save/Commit are provably on the same connection. Redemption runs
526
+ // pre-auth, with no per-request provider yet, and the global Metadata.Provider is shared
527
+ // by every concurrent request: a transaction opened there nests into anyone else's and
528
+ // swallows their writes. So it gets its own (see CreateIsolatedProvider).
529
+ const provider = await CreateIsolatedProvider();
527
530
  const email = invite.Email;
528
531
  // The invite does not persist a name; provision a sensible default. (The
529
532
  // create-invite params accept first/last name but there is no column to
@@ -1,7 +1,8 @@
1
- import { ApplicationInfo, DatabaseProviderBase, EntitySaveOptions, LogError, LogStatus, Metadata, RunView, RunViewResult, UserInfo } from "@memberjunction/core";
1
+ import { ApplicationInfo, EntitySaveOptions, LogError, LogStatus, RunView, RunViewResult, UserInfo } from "@memberjunction/core";
2
2
  import { RegisterClass } from "@memberjunction/global";
3
3
  import { ResolveConfiguredPrincipal } from "./principals.js";
4
4
  import { configInfo } from "../config.js";
5
+ import { CreateIsolatedProvider } from "../isolatedProvider.js";
5
6
  import { MJUserEntity, MJUserRoleEntity, MJUserApplicationEntity, MJUserApplicationEntityEntity, MJApplicationEntityType, MJApplicationEntityEntityType, UserInfoEngine } from "@memberjunction/core-entities";
6
7
 
7
8
  export class NewUserBase {
@@ -16,8 +17,12 @@ export class NewUserBase {
16
17
  return null;
17
18
  }
18
19
 
19
- const md: Metadata = new Metadata(); // global-provider-ok: new-user creation runs in the JWT auth flow, before AppContext.providers is built
20
- const user = await md.GetEntityObject<MJUserEntity>('MJ: Users', contextUser) // To-Do - change this to be a different defined user for the user creation process
20
+ // Everything here, metadata lookups, entity objects and the transaction, goes through ONE
21
+ // isolated provider. This runs in the JWT auth flow, before AppContext.providers exists, and
22
+ // the global Metadata.Provider is shared by every concurrent request: a transaction opened
23
+ // there nests into anyone else's and swallows their writes (see CreateIsolatedProvider).
24
+ const provider = await CreateIsolatedProvider();
25
+ const user = await provider.GetEntityObject<MJUserEntity>('MJ: Users', contextUser) // To-Do - change this to be a different defined user for the user creation process
21
26
  user.NewRecord();
22
27
  user.Name = email;
23
28
  user.IsActive = true;
@@ -38,7 +43,6 @@ export class NewUserBase {
38
43
  // Create the user and all of its role/application/app-entity records atomically.
39
44
  // If any Save fails partway through, the whole provisioning rolls back so we never
40
45
  // leave a half-created user with partial roles/applications behind.
41
- const provider = Metadata.Provider as DatabaseProviderBase; // global-provider-ok: new-user creation runs in the JWT auth flow, before AppContext.providers is built
42
46
  await provider.BeginTransaction();
43
47
  try {
44
48
  if (!await user.Save()) {
@@ -48,13 +52,13 @@ export class NewUserBase {
48
52
  if(configInfo.userHandling && configInfo.userHandling.newUserRoles){
49
53
  LogStatus(`User ${user.Email} created, assigning roles`);
50
54
  for (const role of configInfo.userHandling.newUserRoles) {
51
- const userRole = md.Roles.find(r => r.Name === role);
55
+ const userRole = provider.Roles.find(r => r.Name === role);
52
56
  if (!userRole) {
53
57
  LogError(`Role ${role} not found in the database, cannot assign to new user ${user.Name}`);
54
58
  continue;
55
59
  }
56
60
 
57
- const userRoleEntity: MJUserRoleEntity = await md.GetEntityObject<MJUserRoleEntity>('MJ: User Roles', contextUser);
61
+ const userRoleEntity: MJUserRoleEntity = await provider.GetEntityObject<MJUserRoleEntity>('MJ: User Roles', contextUser);
58
62
  userRoleEntity.NewRecord();
59
63
  userRoleEntity.UserID = user.ID;
60
64
  userRoleEntity.RoleID = userRole.ID;
@@ -73,7 +77,7 @@ export class NewUserBase {
73
77
  if (configInfo.userHandling.UserApplications && configInfo.userHandling.UserApplications.length > 0) {
74
78
  for (const appName of configInfo.userHandling.UserApplications) {
75
79
  const toLowerCase: string = appName.trim().toLocaleLowerCase();
76
- const application: ApplicationInfo | undefined = md.Applications.find(a => a.Name.trim().toLocaleLowerCase() === toLowerCase);
80
+ const application: ApplicationInfo | undefined = provider.Applications.find(a => a.Name.trim().toLocaleLowerCase() === toLowerCase);
77
81
  if (application) {
78
82
  applicationsToCreate.push(application);
79
83
  } else {
@@ -85,12 +89,12 @@ export class NewUserBase {
85
89
  // Bug F2: use the shared selector, which filters to Active apps flagged
86
90
  // DefaultForNewUser (in DefaultSequence order). This path previously omitted the
87
91
  // Status = 'Active' check, so an inactive default app could be provisioned here.
88
- applicationsToCreate = UserInfoEngine.GetDefaultApplicationsForNewUser(md);
92
+ applicationsToCreate = UserInfoEngine.GetDefaultApplicationsForNewUser(provider);
89
93
  LogStatus(`Found ${applicationsToCreate.length} active applications with DefaultForNewUser=true`);
90
94
  }
91
95
 
92
96
  for (const [appIndex, application] of applicationsToCreate.entries()) {
93
- const userApplication: MJUserApplicationEntity = await md.GetEntityObject<MJUserApplicationEntity>('MJ: User Applications', contextUser);
97
+ const userApplication: MJUserApplicationEntity = await provider.GetEntityObject<MJUserApplicationEntity>('MJ: User Applications', contextUser);
94
98
  userApplication.NewRecord();
95
99
  userApplication.UserID = user.ID;
96
100
  userApplication.ApplicationID = application.ID;
@@ -102,7 +106,7 @@ export class NewUserBase {
102
106
  }
103
107
  LogStatus(`Created User Application ${application.Name} for new user ${user.Name}`);
104
108
 
105
- const rv: RunView = new RunView();
109
+ const rv: RunView = RunView.FromMetadataProvider(provider);
106
110
  // Boolean literal goes through the active provider's dialect:
107
111
  // SQL Server emits `= 1`, PostgreSQL emits `= TRUE`.
108
112
  // This keeps the filter server-side (no client-side `.filter()`
@@ -125,7 +129,7 @@ export class NewUserBase {
125
129
  LogStatus(`Creating ${defaultForNewUserEntities.length} User Application Entities for User Application ${application.Name} for new user ${user.Name}`);
126
130
 
127
131
  for(const [index, appEntity] of defaultForNewUserEntities.entries()){
128
- const userAppEntity: MJUserApplicationEntityEntity = await md.GetEntityObject<MJUserApplicationEntityEntity>('MJ: User Application Entities', contextUser);
132
+ const userAppEntity: MJUserApplicationEntityEntity = await provider.GetEntityObject<MJUserApplicationEntityEntity>('MJ: User Application Entities', contextUser);
129
133
  userAppEntity.NewRecord();
130
134
  userAppEntity.UserApplicationID = userApplication.ID;
131
135
  userAppEntity.EntityID = appEntity.EntityID;
package/src/context.ts CHANGED
@@ -798,21 +798,25 @@ async function createPerRequestProviders(
798
798
  { provider: p, type: 'Read-Write' }
799
799
  ];
800
800
 
801
- if (isPostgres) {
802
- const rp = await tryCreateReadOnlyPostgresProvider();
803
- if (rp) {
804
- providers.push({ provider: rp, type: 'Read-Only' });
805
- }
806
- } else {
807
- const rp = await tryCreateReadOnlyProvider(dataSources);
808
- if (rp) {
809
- providers.push({ provider: rp, type: 'Read-Only' });
810
- }
801
+ const rp = await CreateReadOnlyProvider(dataSources);
802
+ if (rp) {
803
+ providers.push({ provider: rp, type: 'Read-Only' });
811
804
  }
812
805
 
813
806
  return providers;
814
807
  }
815
808
 
809
+ /**
810
+ * Opens a provider on the read-only database login, for the current platform. Resolves null when no
811
+ * read-only login is configured; it never falls back to the read-write pool.
812
+ * @param dataSources the data sources MJAPI opened at startup
813
+ */
814
+ export async function CreateReadOnlyProvider(dataSources: DataSourceInfo[]): Promise<DatabaseProviderBase | null> {
815
+ return resolveDbPlatformFromEnv() === 'postgresql'
816
+ ? tryCreateReadOnlyPostgresProvider()
817
+ : tryCreateReadOnlyProvider(dataSources);
818
+ }
819
+
816
820
  /**
817
821
  * Creates a PostgreSQL per-request provider, sharing the connection pool
818
822
  * from the primary provider to avoid pool exhaustion.