@memberjunction/server 6.1.3 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/auth/exampleNewUserSubClass.js +2 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +255 -4
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +1256 -0
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +26 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +59 -1
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +2 -1
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +37 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +98 -8
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +5 -4
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js +3 -1
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +1 -0
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +53 -4
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/services/MJStorageBlobStore.d.ts +41 -0
- package/dist/services/MJStorageBlobStore.d.ts.map +1 -0
- package/dist/services/MJStorageBlobStore.js +160 -0
- package/dist/services/MJStorageBlobStore.js.map +1 -0
- package/dist/util.d.ts +6 -3
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +6 -3
- package/dist/util.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +229 -0
- package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
- package/src/__tests__/SearchEntitiesResolver.readOnlyFallback.test.ts +78 -0
- package/src/__tests__/TransactionGroupResolver.refusals.test.ts +290 -0
- package/src/auth/exampleNewUserSubClass.ts +2 -2
- package/src/generated/graphql-schemas/__mj.ts +896 -5
- package/src/generic/ResolverBase.ts +61 -0
- package/src/index.ts +14 -1
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
- package/src/resolvers/QuerySystemUserResolver.ts +2 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +123 -10
- package/src/resolvers/RunAIAgentResolver.ts +5 -4
- package/src/resolvers/SearchEntitiesResolver.ts +3 -1
- package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
- package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
- package/src/resolvers/TransactionGroupResolver.ts +57 -4
- package/src/services/MJStorageBlobStore.ts +209 -0
- 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
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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)
|