@memberjunction/server 6.1.2 → 6.2.0-edge.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 (66) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/auth/exampleNewUserSubClass.js +2 -1
  3. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  4. package/dist/generated/graphql-schemas/__mj.d.ts +255 -4
  5. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  6. package/dist/generated/graphql-schemas/__mj.js +1256 -0
  7. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  8. package/dist/generic/ResolverBase.d.ts +26 -0
  9. package/dist/generic/ResolverBase.d.ts.map +1 -1
  10. package/dist/generic/ResolverBase.js +59 -1
  11. package/dist/generic/ResolverBase.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +12 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
  16. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  17. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  18. package/dist/resolvers/QuerySystemUserResolver.js +2 -1
  19. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  20. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +37 -0
  21. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  22. package/dist/resolvers/RealtimeClientSessionResolver.js +98 -8
  23. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  24. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  25. package/dist/resolvers/RunAIAgentResolver.js +5 -4
  26. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  27. package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -1
  28. package/dist/resolvers/SearchEntitiesResolver.js +3 -1
  29. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
  30. package/dist/resolvers/SearchKnowledgeResolver.d.ts +1 -0
  31. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  32. package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
  33. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  34. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
  35. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
  36. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
  37. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  38. package/dist/resolvers/TransactionGroupResolver.js +53 -4
  39. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  40. package/dist/services/MJStorageBlobStore.d.ts +41 -0
  41. package/dist/services/MJStorageBlobStore.d.ts.map +1 -0
  42. package/dist/services/MJStorageBlobStore.js +160 -0
  43. package/dist/services/MJStorageBlobStore.js.map +1 -0
  44. package/dist/util.d.ts +6 -3
  45. package/dist/util.d.ts.map +1 -1
  46. package/dist/util.js +6 -3
  47. package/dist/util.js.map +1 -1
  48. package/package.json +92 -92
  49. package/src/__tests__/RealtimeClientSessionResolver.test.ts +229 -0
  50. package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
  51. package/src/__tests__/SearchEntitiesResolver.readOnlyFallback.test.ts +78 -0
  52. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +290 -0
  53. package/src/auth/exampleNewUserSubClass.ts +2 -2
  54. package/src/generated/graphql-schemas/__mj.ts +896 -5
  55. package/src/generic/ResolverBase.ts +61 -0
  56. package/src/index.ts +14 -1
  57. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
  58. package/src/resolvers/QuerySystemUserResolver.ts +2 -1
  59. package/src/resolvers/RealtimeClientSessionResolver.ts +123 -10
  60. package/src/resolvers/RunAIAgentResolver.ts +5 -4
  61. package/src/resolvers/SearchEntitiesResolver.ts +3 -1
  62. package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
  63. package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
  64. package/src/resolvers/TransactionGroupResolver.ts +57 -4
  65. package/src/services/MJStorageBlobStore.ts +209 -0
  66. package/src/util.ts +6 -3
@@ -0,0 +1,209 @@
1
+ import { LogError, UserInfo, IMetadataProvider, Metadata } from '@memberjunction/core';
2
+ import { MJGlobal } from '@memberjunction/global';
3
+ import type { MJFileEntity, MJFileStorageProviderEntity } from '@memberjunction/core-entities';
4
+ import { FileStorageBase, FileStorageEngine } from '@memberjunction/storage';
5
+ import type {
6
+ AttachmentBlobUploadInput,
7
+ AttachmentBlobUploadResult,
8
+ IAttachmentBlobStore,
9
+ } from '@memberjunction/aiengine';
10
+
11
+ /**
12
+ * @fileoverview MJStorage-backed implementation of the conversation-attachment blob seam.
13
+ *
14
+ * This is where `@memberjunction/storage` — and with it the AWS, Azure and Dropbox SDKs — now
15
+ * lives, instead of inside `@memberjunction/aiengine`. That single import previously made the
16
+ * whole attachment service server-only: no browser or React Native client could consume it, which
17
+ * is why the same attachment rules ended up reimplemented three times across the estate.
18
+ *
19
+ * The bodies here are the ones lifted out of `ConversationAttachmentService`, unchanged in
20
+ * behaviour. Only two things differ, both forced by the seam being runtime-neutral:
21
+ *
22
+ * - content crosses as **base64**, not `Buffer`;
23
+ * - account-versus-provider resolution is this implementation's business, because only it knows
24
+ * how its credentials work.
25
+ */
26
+ export class MJStorageBlobStore implements IAttachmentBlobStore {
27
+ /**
28
+ * Uploads bytes and creates the `MJ: Files` record.
29
+ *
30
+ * Prefers account-scoped upload through `FileStorageEngine` when an account id is supplied —
31
+ * that path handles OAuth credentials properly. Falls back to provider-only resolution, which
32
+ * relies on environment configuration and has no account credentials.
33
+ */
34
+ public async Upload(
35
+ input: AttachmentBlobUploadInput,
36
+ contextUser: UserInfo,
37
+ provider?: IMetadataProvider
38
+ ): Promise<AttachmentBlobUploadResult> {
39
+ const md = provider ?? Metadata.Provider;
40
+
41
+ if (input.StorageAccountID) {
42
+ try {
43
+ await FileStorageEngine.Instance.Config(false, contextUser);
44
+ const result = await FileStorageEngine.Instance.UploadFile({
45
+ content: Buffer.from(input.Base64Data, 'base64'),
46
+ fileName: input.FileName,
47
+ mimeType: input.MimeType,
48
+ contextUser,
49
+ storageAccountId: input.StorageAccountID,
50
+ provider: md,
51
+ pathPrefix: input.PathPrefix ?? undefined,
52
+ });
53
+ return { Success: true, FileID: result.FileID };
54
+ } catch (err) {
55
+ return { Success: false, Error: (err as Error).message };
56
+ }
57
+ }
58
+
59
+ if (!input.StorageProviderID) {
60
+ return { Success: false, Error: 'No storage provider configured for attachments' };
61
+ }
62
+
63
+ const storageProviderEntity = await md.GetEntityObject<MJFileStorageProviderEntity>(
64
+ 'MJ: File Storage Providers',
65
+ contextUser
66
+ );
67
+ if (!(await storageProviderEntity.Load(input.StorageProviderID))) {
68
+ return { Success: false, Error: 'Failed to load storage provider' };
69
+ }
70
+
71
+ const driver = MJGlobal.Instance.ClassFactory.CreateInstance<FileStorageBase>(
72
+ FileStorageBase,
73
+ storageProviderEntity.ServerDriverKey
74
+ );
75
+
76
+ const objectName = `${input.PathPrefix ?? 'conversation-attachments'}/${input.FileName}`;
77
+ const uploaded = await driver.PutObject(objectName, Buffer.from(input.Base64Data, 'base64'), input.MimeType);
78
+ if (!uploaded) {
79
+ return { Success: false, Error: 'Failed to upload file to storage' };
80
+ }
81
+
82
+ const file = await md.GetEntityObject<MJFileEntity>('MJ: Files', contextUser);
83
+ file.NewRecord();
84
+ file.Name = input.FileName;
85
+ file.ProviderID = input.StorageProviderID;
86
+ file.ContentType = input.MimeType;
87
+ file.ProviderKey = objectName;
88
+ file.Status = 'Uploaded';
89
+ if (!(await file.Save())) {
90
+ // The bytes are already in the bucket and nothing will ever point at them, so remove
91
+ // them before reporting the failure. Without this compensation an RLS rejection or a
92
+ // transient SQL error leaves a permanently orphaned object with no catalog row — the
93
+ // failure mode this seam's contract calls out by name.
94
+ const compensated = await driver.DeleteObject(objectName);
95
+ if (!compensated) {
96
+ LogError(
97
+ `[MJStorageBlobStore] Orphaned object '${objectName}': the MJ: Files row failed to save ` +
98
+ `and the compensating delete also failed.`
99
+ );
100
+ }
101
+ return {
102
+ Success: false,
103
+ Error: file.LatestResult?.CompleteMessage ?? 'Failed to create the file record',
104
+ };
105
+ }
106
+ return { Success: true, FileID: file.ID };
107
+ }
108
+
109
+ /** @inheritdoc */
110
+ public async Download(
111
+ fileId: string,
112
+ contextUser: UserInfo,
113
+ provider?: IMetadataProvider
114
+ ): Promise<string | null> {
115
+ try {
116
+ const resolved = await this.resolveFileAndDriver(fileId, contextUser, provider);
117
+ if (!resolved) return null;
118
+
119
+ const content = await resolved.driver.GetObject({ fullPath: resolved.objectKey });
120
+ // The seam is base64 so it stays runtime-neutral; the Buffer never leaves this file.
121
+ return content ? content.toString('base64') : null;
122
+ } catch (err) {
123
+ LogError(`[MJStorageBlobStore] Failed to download file ${fileId}: ${err}`);
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /** @inheritdoc */
129
+ public async GetDownloadUrl(
130
+ fileId: string,
131
+ contextUser: UserInfo,
132
+ provider?: IMetadataProvider
133
+ ): Promise<string | null> {
134
+ try {
135
+ const resolved = await this.resolveFileAndDriver(fileId, contextUser, provider);
136
+ if (!resolved) return null;
137
+ return resolved.driver.CreatePreAuthDownloadUrl(resolved.objectKey);
138
+ } catch (err) {
139
+ LogError(`[MJStorageBlobStore] Failed to create download URL for ${fileId}: ${err}`);
140
+ return null;
141
+ }
142
+ }
143
+
144
+ /** @inheritdoc */
145
+ public async Delete(
146
+ fileId: string,
147
+ contextUser: UserInfo,
148
+ provider?: IMetadataProvider
149
+ ): Promise<boolean> {
150
+ try {
151
+ const resolved = await this.resolveFileAndDriver(fileId, contextUser, provider);
152
+ if (!resolved) return false;
153
+
154
+ const deleted = await resolved.driver.DeleteObject(resolved.objectKey);
155
+ // Only remove the catalog row once the bytes are gone — a row without content is
156
+ // recoverable, content without a row is orphaned storage nobody will find again.
157
+ if (deleted) {
158
+ await resolved.file.Delete();
159
+ }
160
+ return deleted;
161
+ } catch (err) {
162
+ LogError(`[MJStorageBlobStore] Failed to delete file ${fileId}: ${err}`);
163
+ return false;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Loads a file, its storage provider, and a driver able to act on it.
169
+ *
170
+ * Shared by download, URL and delete, which previously repeated this block three times with
171
+ * subtly different fallbacks — the account-credential path existed in only one of them.
172
+ */
173
+ private async resolveFileAndDriver(
174
+ fileId: string,
175
+ contextUser: UserInfo,
176
+ provider?: IMetadataProvider
177
+ ): Promise<{ file: MJFileEntity; driver: FileStorageBase; objectKey: string } | null> {
178
+ const md = provider ?? Metadata.Provider;
179
+
180
+ // Load the engine before asking it anything. `Accounts` returns an empty array when it has
181
+ // not been configured, so on a cold process the account-credential branch below would be
182
+ // skipped silently and the driver would fall back to environment-only credentials — which
183
+ // then fails to read a file that is perfectly readable. Every other MJServer call site
184
+ // configures immediately before use for the same reason.
185
+ await FileStorageEngine.Instance.Config(false, contextUser);
186
+
187
+ const file = await md.GetEntityObject<MJFileEntity>('MJ: Files', contextUser);
188
+ if (!(await file.Load(fileId))) return null;
189
+
190
+ const storageProvider = await md.GetEntityObject<MJFileStorageProviderEntity>(
191
+ 'MJ: File Storage Providers',
192
+ contextUser
193
+ );
194
+ if (!(await storageProvider.Load(file.ProviderID))) return null;
195
+
196
+ // Prefer account-scoped credentials when an account links to this provider; fall back to
197
+ // environment-configured credentials otherwise.
198
+ const matchingAccounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
199
+ const driver =
200
+ matchingAccounts.length > 0
201
+ ? await FileStorageEngine.Instance.GetDriver(matchingAccounts[0].ID, contextUser)
202
+ : MJGlobal.Instance.ClassFactory.CreateInstance<FileStorageBase>(
203
+ FileStorageBase,
204
+ storageProvider.ServerDriverKey
205
+ );
206
+
207
+ return { file, driver, objectKey: file.ProviderKey || file.Name };
208
+ }
209
+ }
package/src/util.ts CHANGED
@@ -168,9 +168,12 @@ export async function sendPostRequest(url: string, payload: any, useCompression:
168
168
  }
169
169
 
170
170
  /**
171
- * Returns the read-only provider if it exists, otherwise returns the original provider if options is not provided or if options.allowFallbackToReadWrite is true.
172
- * @param options
173
- * @returns
171
+ * Returns the read-only provider if one is configured. Otherwise returns the first (read-write)
172
+ * provider ONLY when `options.allowFallbackToReadWrite` is true, and `null` when it is omitted.
173
+ *
174
+ * The read-only pool exists only when `dbReadOnlyUsername`/`dbReadOnlyPassword` are set, which
175
+ * most deployments leave unset — so a caller that omits the flag gets `null` on those servers.
176
+ * Pass it for any pure read.
174
177
  */
175
178
  export function GetReadOnlyProvider(providers: Array<ProviderInfo>, options?: {allowFallbackToReadWrite: boolean}): DatabaseProviderBase {
176
179
  if (!providers || providers.length === 0)