@memberjunction/server 3.1.0 → 3.2.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/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +26 -1
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/generated/generated.d.ts +384 -57
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +12066 -9955
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +130 -1
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +784 -9
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +51 -30
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts +10 -0
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +72 -7
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +36 -14
- package/dist/util.js.map +1 -1
- package/package.json +45 -44
- package/src/agents/skip-sdk.ts +31 -1
- package/src/generated/generated.ts +1558 -215
- package/src/index.ts +8 -0
- package/src/resolvers/FileResolver.ts +701 -29
- package/src/resolvers/RunAIAgentResolver.ts +56 -46
- package/src/resolvers/SqlLoggingConfigResolver.ts +86 -13
- package/src/util.ts +47 -17
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EntityPermissionType, Metadata, FieldValueCollection, EntitySaveOptions } from '@memberjunction/core';
|
|
2
|
-
import { FileEntity, FileStorageProviderEntity } from '@memberjunction/core-entities';
|
|
1
|
+
import { EntityPermissionType, Metadata, FieldValueCollection, EntitySaveOptions, RunView } from '@memberjunction/core';
|
|
2
|
+
import { FileEntity, FileStorageProviderEntity, FileStorageAccountEntity } from '@memberjunction/core-entities';
|
|
3
3
|
import {
|
|
4
4
|
AppContext,
|
|
5
5
|
Arg,
|
|
@@ -13,10 +13,26 @@ import {
|
|
|
13
13
|
ObjectType,
|
|
14
14
|
PubSub,
|
|
15
15
|
PubSubEngine,
|
|
16
|
+
Query,
|
|
16
17
|
Resolver,
|
|
17
18
|
Root,
|
|
18
19
|
} from '@memberjunction/server';
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createDownloadUrl,
|
|
22
|
+
createUploadUrl,
|
|
23
|
+
deleteObject,
|
|
24
|
+
moveObject,
|
|
25
|
+
copyObject,
|
|
26
|
+
listObjects,
|
|
27
|
+
copyObjectBetweenProviders,
|
|
28
|
+
searchAcrossAccounts,
|
|
29
|
+
AccountSearchResult,
|
|
30
|
+
AccountSearchInput,
|
|
31
|
+
FileSearchResult,
|
|
32
|
+
UserContextOptions,
|
|
33
|
+
ExtendedUserContextOptions,
|
|
34
|
+
initializeDriverWithAccountCredentials,
|
|
35
|
+
} from '@memberjunction/storage';
|
|
20
36
|
import { CreateMJFileInput, MJFileResolver as FileResolverBase, MJFile_, UpdateMJFileInput } from '../generated/generated.js';
|
|
21
37
|
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
22
38
|
import { GetReadOnlyProvider } from '../util.js';
|
|
@@ -43,27 +59,324 @@ export class FileExt extends MJFile_ {
|
|
|
43
59
|
DownloadUrl: string;
|
|
44
60
|
}
|
|
45
61
|
|
|
62
|
+
@ObjectType()
|
|
63
|
+
export class StorageObjectMetadata {
|
|
64
|
+
@Field(() => String)
|
|
65
|
+
name: string;
|
|
66
|
+
|
|
67
|
+
@Field(() => String)
|
|
68
|
+
path: string;
|
|
69
|
+
|
|
70
|
+
@Field(() => String)
|
|
71
|
+
fullPath: string;
|
|
72
|
+
|
|
73
|
+
@Field(() => Int)
|
|
74
|
+
size: number;
|
|
75
|
+
|
|
76
|
+
@Field(() => String)
|
|
77
|
+
contentType: string;
|
|
78
|
+
|
|
79
|
+
@Field(() => String)
|
|
80
|
+
lastModified: string;
|
|
81
|
+
|
|
82
|
+
@Field(() => Boolean)
|
|
83
|
+
isDirectory: boolean;
|
|
84
|
+
|
|
85
|
+
@Field(() => String, { nullable: true })
|
|
86
|
+
etag?: string;
|
|
87
|
+
|
|
88
|
+
@Field(() => String, { nullable: true })
|
|
89
|
+
cacheControl?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@ObjectType()
|
|
93
|
+
export class StorageListResult {
|
|
94
|
+
@Field(() => [StorageObjectMetadata])
|
|
95
|
+
objects: StorageObjectMetadata[];
|
|
96
|
+
|
|
97
|
+
@Field(() => [String])
|
|
98
|
+
prefixes: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@InputType()
|
|
102
|
+
export class ListStorageObjectsInput {
|
|
103
|
+
@Field(() => String)
|
|
104
|
+
AccountID: string;
|
|
105
|
+
|
|
106
|
+
@Field(() => String)
|
|
107
|
+
Prefix: string;
|
|
108
|
+
|
|
109
|
+
@Field(() => String, { nullable: true })
|
|
110
|
+
Delimiter?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@InputType()
|
|
114
|
+
export class CreatePreAuthDownloadUrlInput {
|
|
115
|
+
@Field(() => String)
|
|
116
|
+
AccountID: string;
|
|
117
|
+
|
|
118
|
+
@Field(() => String)
|
|
119
|
+
ObjectName: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@InputType()
|
|
123
|
+
export class CreatePreAuthUploadUrlInput {
|
|
124
|
+
@Field(() => String)
|
|
125
|
+
AccountID: string;
|
|
126
|
+
|
|
127
|
+
@Field(() => String)
|
|
128
|
+
ObjectName: string;
|
|
129
|
+
|
|
130
|
+
@Field(() => String, { nullable: true })
|
|
131
|
+
ContentType?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@ObjectType()
|
|
135
|
+
export class CreatePreAuthUploadUrlPayload {
|
|
136
|
+
@Field(() => String)
|
|
137
|
+
UploadUrl: string;
|
|
138
|
+
|
|
139
|
+
@Field(() => String, { nullable: true })
|
|
140
|
+
ProviderKey?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@InputType()
|
|
144
|
+
export class DeleteStorageObjectInput {
|
|
145
|
+
@Field(() => String)
|
|
146
|
+
AccountID: string;
|
|
147
|
+
|
|
148
|
+
@Field(() => String)
|
|
149
|
+
ObjectName: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@InputType()
|
|
153
|
+
export class MoveStorageObjectInput {
|
|
154
|
+
@Field(() => String)
|
|
155
|
+
AccountID: string;
|
|
156
|
+
|
|
157
|
+
@Field(() => String)
|
|
158
|
+
OldName: string;
|
|
159
|
+
|
|
160
|
+
@Field(() => String)
|
|
161
|
+
NewName: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@InputType()
|
|
165
|
+
export class CopyStorageObjectInput {
|
|
166
|
+
@Field(() => String)
|
|
167
|
+
AccountID: string;
|
|
168
|
+
|
|
169
|
+
@Field(() => String)
|
|
170
|
+
SourceName: string;
|
|
171
|
+
|
|
172
|
+
@Field(() => String)
|
|
173
|
+
DestinationName: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@InputType()
|
|
177
|
+
export class CreateDirectoryInput {
|
|
178
|
+
@Field(() => String)
|
|
179
|
+
AccountID: string;
|
|
180
|
+
|
|
181
|
+
@Field(() => String)
|
|
182
|
+
Path: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@InputType()
|
|
186
|
+
export class CopyObjectBetweenAccountsInput {
|
|
187
|
+
@Field(() => String)
|
|
188
|
+
SourceAccountID: string;
|
|
189
|
+
|
|
190
|
+
@Field(() => String)
|
|
191
|
+
DestinationAccountID: string;
|
|
192
|
+
|
|
193
|
+
@Field(() => String)
|
|
194
|
+
SourcePath: string;
|
|
195
|
+
|
|
196
|
+
@Field(() => String)
|
|
197
|
+
DestinationPath: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@ObjectType()
|
|
201
|
+
export class CopyObjectBetweenAccountsPayload {
|
|
202
|
+
@Field(() => Boolean)
|
|
203
|
+
success: boolean;
|
|
204
|
+
|
|
205
|
+
@Field(() => String)
|
|
206
|
+
message: string;
|
|
207
|
+
|
|
208
|
+
@Field(() => Int, { nullable: true })
|
|
209
|
+
bytesTransferred?: number;
|
|
210
|
+
|
|
211
|
+
@Field(() => String)
|
|
212
|
+
sourceAccount: string;
|
|
213
|
+
|
|
214
|
+
@Field(() => String)
|
|
215
|
+
destinationAccount: string;
|
|
216
|
+
|
|
217
|
+
@Field(() => String)
|
|
218
|
+
sourcePath: string;
|
|
219
|
+
|
|
220
|
+
@Field(() => String)
|
|
221
|
+
destinationPath: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@InputType()
|
|
225
|
+
export class SearchAcrossAccountsInput {
|
|
226
|
+
@Field(() => [String])
|
|
227
|
+
AccountIDs: string[];
|
|
228
|
+
|
|
229
|
+
@Field(() => String)
|
|
230
|
+
Query: string;
|
|
231
|
+
|
|
232
|
+
@Field(() => Int, { nullable: true })
|
|
233
|
+
MaxResultsPerAccount?: number;
|
|
234
|
+
|
|
235
|
+
@Field(() => [String], { nullable: true })
|
|
236
|
+
FileTypes?: string[];
|
|
237
|
+
|
|
238
|
+
@Field(() => Boolean, { nullable: true })
|
|
239
|
+
SearchContent?: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@ObjectType()
|
|
243
|
+
export class FileSearchResultPayload {
|
|
244
|
+
@Field(() => String)
|
|
245
|
+
path: string;
|
|
246
|
+
|
|
247
|
+
@Field(() => String)
|
|
248
|
+
name: string;
|
|
249
|
+
|
|
250
|
+
@Field(() => Int)
|
|
251
|
+
size: number;
|
|
252
|
+
|
|
253
|
+
@Field(() => String)
|
|
254
|
+
contentType: string;
|
|
255
|
+
|
|
256
|
+
@Field(() => String)
|
|
257
|
+
lastModified: string;
|
|
258
|
+
|
|
259
|
+
@Field(() => Number, { nullable: true })
|
|
260
|
+
relevance?: number;
|
|
261
|
+
|
|
262
|
+
@Field(() => String, { nullable: true })
|
|
263
|
+
excerpt?: string;
|
|
264
|
+
|
|
265
|
+
@Field(() => Boolean, { nullable: true })
|
|
266
|
+
matchInFilename?: boolean;
|
|
267
|
+
|
|
268
|
+
@Field(() => String, { nullable: true })
|
|
269
|
+
objectId?: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@ObjectType()
|
|
273
|
+
export class AccountSearchResultPayload {
|
|
274
|
+
@Field(() => String)
|
|
275
|
+
accountID: string;
|
|
276
|
+
|
|
277
|
+
@Field(() => String)
|
|
278
|
+
accountName: string;
|
|
279
|
+
|
|
280
|
+
@Field(() => Boolean)
|
|
281
|
+
success: boolean;
|
|
282
|
+
|
|
283
|
+
@Field(() => String, { nullable: true })
|
|
284
|
+
errorMessage?: string;
|
|
285
|
+
|
|
286
|
+
@Field(() => [FileSearchResultPayload])
|
|
287
|
+
results: FileSearchResultPayload[];
|
|
288
|
+
|
|
289
|
+
@Field(() => Int, { nullable: true })
|
|
290
|
+
totalMatches?: number;
|
|
291
|
+
|
|
292
|
+
@Field(() => Boolean)
|
|
293
|
+
hasMore: boolean;
|
|
294
|
+
|
|
295
|
+
@Field(() => String, { nullable: true })
|
|
296
|
+
nextPageToken?: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@ObjectType()
|
|
300
|
+
export class SearchAcrossAccountsPayload {
|
|
301
|
+
@Field(() => [AccountSearchResultPayload])
|
|
302
|
+
accountResults: AccountSearchResultPayload[];
|
|
303
|
+
|
|
304
|
+
@Field(() => Int)
|
|
305
|
+
totalResultsReturned: number;
|
|
306
|
+
|
|
307
|
+
@Field(() => Int)
|
|
308
|
+
successfulAccounts: number;
|
|
309
|
+
|
|
310
|
+
@Field(() => Int)
|
|
311
|
+
failedAccounts: number;
|
|
312
|
+
}
|
|
313
|
+
|
|
46
314
|
@Resolver(MJFile_)
|
|
47
315
|
export class FileResolver extends FileResolverBase {
|
|
316
|
+
/**
|
|
317
|
+
* Builds UserContextOptions for storage operations that may require OAuth authentication.
|
|
318
|
+
* This passes the current user's ID and context to allow the storage utilities to
|
|
319
|
+
* load user-specific OAuth tokens for providers like Google Drive.
|
|
320
|
+
*/
|
|
321
|
+
private buildUserContext(context: AppContext): UserContextOptions {
|
|
322
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
323
|
+
return {
|
|
324
|
+
userID: user.ID,
|
|
325
|
+
contextUser: user,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Builds ExtendedUserContextOptions that includes the account entity for enterprise model.
|
|
331
|
+
* This is required for OAuth providers using the Credential Engine to decrypt credentials.
|
|
332
|
+
*/
|
|
333
|
+
private buildExtendedUserContext(context: AppContext, accountEntity: FileStorageAccountEntity): ExtendedUserContextOptions {
|
|
334
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
335
|
+
return {
|
|
336
|
+
userID: user.ID,
|
|
337
|
+
contextUser: user,
|
|
338
|
+
accountEntity,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Loads a FileStorageAccount and its associated FileStorageProvider.
|
|
344
|
+
* This is the standard way to get provider information in the enterprise model.
|
|
345
|
+
* @param accountId The ID of the FileStorageAccount
|
|
346
|
+
* @param context The AppContext containing provider info
|
|
347
|
+
* @returns Object containing both the account and provider entities
|
|
348
|
+
*/
|
|
349
|
+
private async loadAccountAndProvider(
|
|
350
|
+
accountId: string,
|
|
351
|
+
context: AppContext,
|
|
352
|
+
): Promise<{ account: FileStorageAccountEntity; provider: FileStorageProviderEntity }> {
|
|
353
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
354
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
355
|
+
|
|
356
|
+
// Load the account entity
|
|
357
|
+
const account = await md.GetEntityObject<FileStorageAccountEntity>('MJ: File Storage Accounts', user);
|
|
358
|
+
const loaded = await account.Load(accountId);
|
|
359
|
+
if (!loaded) {
|
|
360
|
+
throw new Error(`Storage account with ID ${accountId} not found`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Load the provider entity from the account's ProviderID
|
|
364
|
+
const provider = await md.GetEntityObject<FileStorageProviderEntity>('File Storage Providers', user);
|
|
365
|
+
await provider.Load(account.ProviderID);
|
|
366
|
+
|
|
367
|
+
return { account, provider };
|
|
368
|
+
}
|
|
369
|
+
|
|
48
370
|
@Mutation(() => CreateFilePayload)
|
|
49
|
-
async CreateFile(
|
|
50
|
-
@Arg('input', () => CreateMJFileInput) input: CreateMJFileInput,
|
|
51
|
-
@Ctx() context: AppContext,
|
|
52
|
-
@PubSub() pubSub: PubSubEngine
|
|
53
|
-
) {
|
|
371
|
+
async CreateFile(@Arg('input', () => CreateMJFileInput) input: CreateMJFileInput, @Ctx() context: AppContext, @PubSub() pubSub: PubSubEngine) {
|
|
54
372
|
// Check to see if there's already an object with that name
|
|
55
|
-
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true})
|
|
373
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
56
374
|
const user = this.GetUserFromPayload(context.userPayload);
|
|
57
375
|
const fileEntity = await provider.GetEntityObject<FileEntity>('Files', user);
|
|
58
376
|
const providerEntity = await provider.GetEntityObject<FileStorageProviderEntity>('File Storage Providers', user);
|
|
59
377
|
fileEntity.CheckPermissions(EntityPermissionType.Create, true);
|
|
60
378
|
|
|
61
|
-
const [sameName] = await this.findBy(
|
|
62
|
-
provider,
|
|
63
|
-
'Files',
|
|
64
|
-
{ Name: input.Name, ProviderID: input.ProviderID },
|
|
65
|
-
context.userPayload.userRecord
|
|
66
|
-
);
|
|
379
|
+
const [sameName] = await this.findBy(provider, 'Files', { Name: input.Name, ProviderID: input.ProviderID }, context.userPayload.userRecord);
|
|
67
380
|
const NameExists = Boolean(sameName);
|
|
68
381
|
|
|
69
382
|
const success = fileEntity.NewRecord(FieldValueCollection.FromObject({ ...input, Status: 'Pending' }));
|
|
@@ -74,7 +387,8 @@ export class FileResolver extends FileResolverBase {
|
|
|
74
387
|
}
|
|
75
388
|
|
|
76
389
|
// Create the upload URL and get the record updates (provider key, content type, etc)
|
|
77
|
-
const
|
|
390
|
+
const userContext = this.buildUserContext(context);
|
|
391
|
+
const { updatedInput, UploadUrl } = await createUploadUrl(providerEntity, fileEntity, userContext);
|
|
78
392
|
|
|
79
393
|
// Save the file record with the updated input
|
|
80
394
|
const mapper = new FieldMapper();
|
|
@@ -86,28 +400,25 @@ export class FileResolver extends FileResolverBase {
|
|
|
86
400
|
}
|
|
87
401
|
|
|
88
402
|
@FieldResolver(() => String)
|
|
89
|
-
async DownloadUrl(@Root() file: MJFile_, @Ctx()
|
|
403
|
+
async DownloadUrl(@Root() file: MJFile_, @Ctx() context: AppContext) {
|
|
90
404
|
const md = new Metadata();
|
|
91
|
-
const user = this.GetUserFromPayload(userPayload);
|
|
405
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
92
406
|
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
93
407
|
fileEntity.CheckPermissions(EntityPermissionType.Read, true);
|
|
94
408
|
|
|
95
409
|
const providerEntity = await md.GetEntityObject<FileStorageProviderEntity>('File Storage Providers', user);
|
|
96
410
|
await providerEntity.Load(file.ProviderID);
|
|
97
411
|
|
|
98
|
-
const
|
|
412
|
+
const userContext = this.buildUserContext(context);
|
|
413
|
+
const url = await createDownloadUrl(providerEntity, file.ProviderKey ?? file.Name, userContext);
|
|
99
414
|
|
|
100
415
|
return url;
|
|
101
416
|
}
|
|
102
417
|
|
|
103
418
|
@Mutation(() => MJFile_)
|
|
104
|
-
async UpdateFile(
|
|
105
|
-
@Arg('input', () => UpdateMJFileInput) input: UpdateMJFileInput,
|
|
106
|
-
@Ctx() context: AppContext,
|
|
107
|
-
@PubSub() pubSub: PubSubEngine
|
|
108
|
-
) {
|
|
419
|
+
async UpdateFile(@Arg('input', () => UpdateMJFileInput) input: UpdateMJFileInput, @Ctx() context: AppContext, @PubSub() pubSub: PubSubEngine) {
|
|
109
420
|
// if the name is changing, rename the target object as well
|
|
110
|
-
const md = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
|
|
421
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
111
422
|
const user = this.GetUserFromPayload(context.userPayload);
|
|
112
423
|
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
113
424
|
fileEntity.CheckPermissions(EntityPermissionType.Update, true);
|
|
@@ -118,7 +429,8 @@ export class FileResolver extends FileResolverBase {
|
|
|
118
429
|
const providerEntity = await md.GetEntityObject<FileStorageProviderEntity>('File Storage Providers', user);
|
|
119
430
|
await providerEntity.Load(fileEntity.ProviderID);
|
|
120
431
|
|
|
121
|
-
const
|
|
432
|
+
const userContext = this.buildUserContext(context);
|
|
433
|
+
const success = await moveObject(providerEntity, fileEntity.Name, input.Name, userContext);
|
|
122
434
|
if (!success) {
|
|
123
435
|
throw new Error('Error updating object name');
|
|
124
436
|
}
|
|
@@ -133,9 +445,9 @@ export class FileResolver extends FileResolverBase {
|
|
|
133
445
|
@Arg('ID', () => String) ID: string,
|
|
134
446
|
@Arg('options___', () => DeleteOptionsInput) options: DeleteOptionsInput,
|
|
135
447
|
@Ctx() context: AppContext,
|
|
136
|
-
@PubSub() pubSub: PubSubEngine
|
|
448
|
+
@PubSub() pubSub: PubSubEngine,
|
|
137
449
|
) {
|
|
138
|
-
const md = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
|
|
450
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
139
451
|
const userInfo = this.GetUserFromPayload(context.userPayload);
|
|
140
452
|
|
|
141
453
|
const fileEntity = await md.GetEntityObject<FileEntity>('Files', userInfo);
|
|
@@ -149,9 +461,369 @@ export class FileResolver extends FileResolverBase {
|
|
|
149
461
|
if (fileEntity.Status === 'Uploaded') {
|
|
150
462
|
const providerEntity = await md.GetEntityObject<FileStorageProviderEntity>('File Storage Providers', userInfo);
|
|
151
463
|
await providerEntity.Load(fileEntity.ProviderID);
|
|
152
|
-
|
|
464
|
+
const userContext = this.buildUserContext(context);
|
|
465
|
+
await deleteObject(providerEntity, fileEntity.ProviderKey ?? fileEntity.Name, userContext);
|
|
153
466
|
}
|
|
154
467
|
|
|
155
468
|
return super.DeleteMJFile(ID, options, context, pubSub);
|
|
156
469
|
}
|
|
470
|
+
|
|
471
|
+
@Query(() => StorageListResult)
|
|
472
|
+
async ListStorageObjects(@Arg('input', () => ListStorageObjectsInput) input: ListStorageObjectsInput, @Ctx() context: AppContext) {
|
|
473
|
+
console.log('[FileResolver] ListStorageObjects called with:', {
|
|
474
|
+
AccountID: input.AccountID,
|
|
475
|
+
Prefix: input.Prefix,
|
|
476
|
+
Delimiter: input.Delimiter,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
480
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
481
|
+
|
|
482
|
+
// Load the account and its provider
|
|
483
|
+
const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
484
|
+
|
|
485
|
+
console.log('[FileResolver] Provider loaded:', {
|
|
486
|
+
Name: providerEntity.Name,
|
|
487
|
+
ServerDriverKey: providerEntity.ServerDriverKey,
|
|
488
|
+
HasConfiguration: !!providerEntity.Get('Configuration'),
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Check permissions - user must have read access to Files entity
|
|
492
|
+
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
493
|
+
fileEntity.CheckPermissions(EntityPermissionType.Read, true);
|
|
494
|
+
|
|
495
|
+
// Call the storage provider to list objects with extended user context (includes account for credential lookup)
|
|
496
|
+
const userContext = this.buildExtendedUserContext(context, account);
|
|
497
|
+
const result = await listObjects(providerEntity, input.Prefix, input.Delimiter || '/', userContext);
|
|
498
|
+
|
|
499
|
+
console.log('[FileResolver] listObjects result:', {
|
|
500
|
+
objectsCount: result.objects.length,
|
|
501
|
+
prefixesCount: result.prefixes.length,
|
|
502
|
+
objects: result.objects.map((o) => ({ name: o.name, isDirectory: o.isDirectory })),
|
|
503
|
+
prefixes: result.prefixes,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Convert Date objects to ISO strings for GraphQL
|
|
507
|
+
const objects = result.objects.map((obj) => ({
|
|
508
|
+
...obj,
|
|
509
|
+
lastModified: obj.lastModified.toISOString(),
|
|
510
|
+
}));
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
objects,
|
|
514
|
+
prefixes: result.prefixes,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
@Query(() => String)
|
|
519
|
+
async CreatePreAuthDownloadUrl(@Arg('input', () => CreatePreAuthDownloadUrlInput) input: CreatePreAuthDownloadUrlInput, @Ctx() context: AppContext) {
|
|
520
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
521
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
522
|
+
|
|
523
|
+
// Load the account and its provider
|
|
524
|
+
const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
525
|
+
|
|
526
|
+
// Check permissions
|
|
527
|
+
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
528
|
+
fileEntity.CheckPermissions(EntityPermissionType.Read, true);
|
|
529
|
+
|
|
530
|
+
// Create download URL with extended user context (includes account for credential lookup)
|
|
531
|
+
const userContext = this.buildExtendedUserContext(context, account);
|
|
532
|
+
const downloadUrl = await createDownloadUrl(providerEntity, input.ObjectName, userContext);
|
|
533
|
+
return downloadUrl;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@Mutation(() => CreatePreAuthUploadUrlPayload)
|
|
537
|
+
async CreatePreAuthUploadUrl(@Arg('input', () => CreatePreAuthUploadUrlInput) input: CreatePreAuthUploadUrlInput, @Ctx() context: AppContext) {
|
|
538
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
539
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
540
|
+
|
|
541
|
+
// Load the account and its provider
|
|
542
|
+
const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
543
|
+
|
|
544
|
+
// Check permissions
|
|
545
|
+
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
546
|
+
fileEntity.CheckPermissions(EntityPermissionType.Create, true);
|
|
547
|
+
|
|
548
|
+
// Create upload URL with extended user context (includes account for credential lookup)
|
|
549
|
+
const userContext = this.buildExtendedUserContext(context, account);
|
|
550
|
+
const { UploadUrl, updatedInput } = await createUploadUrl(
|
|
551
|
+
providerEntity,
|
|
552
|
+
{
|
|
553
|
+
ID: '', // Not needed for direct upload
|
|
554
|
+
Name: input.ObjectName,
|
|
555
|
+
ProviderID: providerEntity.ID,
|
|
556
|
+
ContentType: input.ContentType,
|
|
557
|
+
},
|
|
558
|
+
userContext,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// Extract ProviderKey if it exists (spread into updatedInput by createUploadUrl)
|
|
562
|
+
const providerKey = (updatedInput as { ProviderKey?: string }).ProviderKey;
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
UploadUrl,
|
|
566
|
+
ProviderKey: providerKey,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
@Mutation(() => Boolean)
|
|
571
|
+
async DeleteStorageObject(@Arg('input', () => DeleteStorageObjectInput) input: DeleteStorageObjectInput, @Ctx() context: AppContext) {
|
|
572
|
+
console.log('[FileResolver] DeleteStorageObject called:', input);
|
|
573
|
+
|
|
574
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
575
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
576
|
+
|
|
577
|
+
// Load the account and its provider
|
|
578
|
+
const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
579
|
+
|
|
580
|
+
console.log('[FileResolver] Provider loaded:', {
|
|
581
|
+
providerID: providerEntity.ID,
|
|
582
|
+
providerName: providerEntity.Name,
|
|
583
|
+
serverDriverKey: providerEntity.ServerDriverKey,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Check permissions
|
|
587
|
+
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
588
|
+
fileEntity.CheckPermissions(EntityPermissionType.Delete, true);
|
|
589
|
+
|
|
590
|
+
console.log('[FileResolver] Permissions checked, calling deleteObject...');
|
|
591
|
+
|
|
592
|
+
// Delete the object with extended user context (includes account for credential lookup)
|
|
593
|
+
const userContext = this.buildExtendedUserContext(context, account);
|
|
594
|
+
const success = await deleteObject(providerEntity, input.ObjectName, userContext);
|
|
595
|
+
|
|
596
|
+
console.log('[FileResolver] deleteObject returned:', success);
|
|
597
|
+
|
|
598
|
+
return success;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
@Mutation(() => Boolean)
|
|
602
|
+
async MoveStorageObject(@Arg('input', () => MoveStorageObjectInput) input: MoveStorageObjectInput, @Ctx() context: AppContext) {
|
|
603
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
604
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
605
|
+
|
|
606
|
+
// Load the account and its provider
|
|
607
|
+
const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
608
|
+
|
|
609
|
+
// Check permissions
|
|
610
|
+
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
611
|
+
fileEntity.CheckPermissions(EntityPermissionType.Update, true);
|
|
612
|
+
|
|
613
|
+
// Move the object with extended user context (includes account for credential lookup)
|
|
614
|
+
const userContext = this.buildExtendedUserContext(context, account);
|
|
615
|
+
const success = await moveObject(providerEntity, input.OldName, input.NewName, userContext);
|
|
616
|
+
return success;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
@Mutation(() => Boolean)
|
|
620
|
+
async CopyStorageObject(@Arg('input', () => CopyStorageObjectInput) input: CopyStorageObjectInput, @Ctx() context: AppContext) {
|
|
621
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
622
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
623
|
+
|
|
624
|
+
// Load the account and its provider
|
|
625
|
+
const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
626
|
+
|
|
627
|
+
// Check permissions - copying requires both read (source) and create (destination)
|
|
628
|
+
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
629
|
+
fileEntity.CheckPermissions(EntityPermissionType.Read, true);
|
|
630
|
+
fileEntity.CheckPermissions(EntityPermissionType.Create, true);
|
|
631
|
+
|
|
632
|
+
// Copy the object with extended user context (includes account for credential lookup)
|
|
633
|
+
const userContext = this.buildExtendedUserContext(context, account);
|
|
634
|
+
const success = await copyObject(providerEntity, input.SourceName, input.DestinationName, userContext);
|
|
635
|
+
return success;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
@Mutation(() => Boolean)
|
|
639
|
+
async CreateDirectory(@Arg('input', () => CreateDirectoryInput) input: CreateDirectoryInput, @Ctx() context: AppContext) {
|
|
640
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
641
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
642
|
+
|
|
643
|
+
// Load the account and its provider
|
|
644
|
+
const { account: accountEntity, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
645
|
+
|
|
646
|
+
// Check permissions
|
|
647
|
+
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
648
|
+
fileEntity.CheckPermissions(EntityPermissionType.Create, true);
|
|
649
|
+
|
|
650
|
+
// Initialize driver with account-based credentials from Credential Engine
|
|
651
|
+
const driver = await initializeDriverWithAccountCredentials({
|
|
652
|
+
accountEntity,
|
|
653
|
+
providerEntity,
|
|
654
|
+
contextUser: user,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const success = await driver.CreateDirectory(input.Path);
|
|
658
|
+
return success;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
@Mutation(() => CopyObjectBetweenAccountsPayload)
|
|
662
|
+
async CopyObjectBetweenAccounts(
|
|
663
|
+
@Arg('input', () => CopyObjectBetweenAccountsInput) input: CopyObjectBetweenAccountsInput,
|
|
664
|
+
@Ctx() context: AppContext,
|
|
665
|
+
): Promise<CopyObjectBetweenAccountsPayload> {
|
|
666
|
+
console.log('[FileResolver] CopyObjectBetweenAccounts called:', {
|
|
667
|
+
sourceAccountID: input.SourceAccountID,
|
|
668
|
+
destinationAccountID: input.DestinationAccountID,
|
|
669
|
+
sourcePath: input.SourcePath,
|
|
670
|
+
destinationPath: input.DestinationPath,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
674
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
675
|
+
|
|
676
|
+
// Check permissions - copying requires both read (source) and create (destination)
|
|
677
|
+
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
678
|
+
fileEntity.CheckPermissions(EntityPermissionType.Read, true);
|
|
679
|
+
fileEntity.CheckPermissions(EntityPermissionType.Create, true);
|
|
680
|
+
|
|
681
|
+
// Load the source account and its provider
|
|
682
|
+
const { account: sourceAccount, provider: sourceProviderEntity } = await this.loadAccountAndProvider(input.SourceAccountID, context);
|
|
683
|
+
|
|
684
|
+
// Load the destination account and its provider
|
|
685
|
+
const { account: destAccount, provider: destProviderEntity } = await this.loadAccountAndProvider(input.DestinationAccountID, context);
|
|
686
|
+
|
|
687
|
+
// Perform the cross-provider copy with extended user context (includes account for credential lookup)
|
|
688
|
+
const sourceUserContext = this.buildExtendedUserContext(context, sourceAccount);
|
|
689
|
+
const destUserContext = this.buildExtendedUserContext(context, destAccount);
|
|
690
|
+
const result = await copyObjectBetweenProviders(sourceProviderEntity, destProviderEntity, input.SourcePath, input.DestinationPath, {
|
|
691
|
+
sourceUserContext,
|
|
692
|
+
destinationUserContext: destUserContext,
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
console.log('[FileResolver] CopyObjectBetweenAccounts result:', result);
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
success: result.success,
|
|
699
|
+
message: result.message,
|
|
700
|
+
bytesTransferred: result.bytesTransferred,
|
|
701
|
+
sourceAccount: sourceAccount.Name,
|
|
702
|
+
destinationAccount: destAccount.Name,
|
|
703
|
+
sourcePath: result.sourcePath,
|
|
704
|
+
destinationPath: result.destinationPath,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
@Query(() => SearchAcrossAccountsPayload)
|
|
709
|
+
async SearchAcrossAccounts(
|
|
710
|
+
@Arg('input', () => SearchAcrossAccountsInput) input: SearchAcrossAccountsInput,
|
|
711
|
+
@Ctx() context: AppContext,
|
|
712
|
+
): Promise<SearchAcrossAccountsPayload> {
|
|
713
|
+
console.log('[FileResolver] SearchAcrossAccounts called:', {
|
|
714
|
+
accountIDs: input.AccountIDs,
|
|
715
|
+
query: input.Query,
|
|
716
|
+
maxResultsPerAccount: input.MaxResultsPerAccount,
|
|
717
|
+
fileTypes: input.FileTypes,
|
|
718
|
+
searchContent: input.SearchContent,
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
722
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
723
|
+
|
|
724
|
+
// Check permissions - searching requires read access
|
|
725
|
+
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
726
|
+
fileEntity.CheckPermissions(EntityPermissionType.Read, true);
|
|
727
|
+
|
|
728
|
+
// Load all requested account entities in a single query
|
|
729
|
+
const rv = new RunView();
|
|
730
|
+
const quotedIDs = input.AccountIDs.map((id) => `'${id}'`).join(', ');
|
|
731
|
+
const accountResult = await rv.RunView<FileStorageAccountEntity>(
|
|
732
|
+
{
|
|
733
|
+
EntityName: 'MJ: File Storage Accounts',
|
|
734
|
+
ExtraFilter: `ID IN (${quotedIDs})`,
|
|
735
|
+
ResultType: 'entity_object',
|
|
736
|
+
},
|
|
737
|
+
user,
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
if (!accountResult.Success) {
|
|
741
|
+
throw new Error(`Failed to load storage accounts: ${accountResult.ErrorMessage}`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const accountEntities = accountResult.Results;
|
|
745
|
+
if (accountEntities.length === 0) {
|
|
746
|
+
throw new Error('No valid storage accounts found for the provided IDs');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Log any accounts that weren't found
|
|
750
|
+
if (accountEntities.length < input.AccountIDs.length) {
|
|
751
|
+
const foundIDs = new Set(accountEntities.map((a) => a.ID));
|
|
752
|
+
const missingIDs = input.AccountIDs.filter((id) => !foundIDs.has(id));
|
|
753
|
+
console.warn(`[FileResolver] Accounts not found: ${missingIDs.join(', ')}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Load providers for all accounts
|
|
757
|
+
const providerIDs = [...new Set(accountEntities.map((a) => a.ProviderID))];
|
|
758
|
+
const quotedProviderIDs = providerIDs.map((id) => `'${id}'`).join(', ');
|
|
759
|
+
const providerResult = await rv.RunView<FileStorageProviderEntity>(
|
|
760
|
+
{
|
|
761
|
+
EntityName: 'File Storage Providers',
|
|
762
|
+
ExtraFilter: `ID IN (${quotedProviderIDs})`,
|
|
763
|
+
ResultType: 'entity_object',
|
|
764
|
+
},
|
|
765
|
+
user,
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
if (!providerResult.Success) {
|
|
769
|
+
throw new Error(`Failed to load storage providers: ${providerResult.ErrorMessage}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const providerMap = new Map<string, FileStorageProviderEntity>();
|
|
773
|
+
for (const provider of providerResult.Results) {
|
|
774
|
+
providerMap.set(provider.ID, provider);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Build account/provider pairs for the search
|
|
778
|
+
const accountInputs: AccountSearchInput[] = [];
|
|
779
|
+
for (const account of accountEntities) {
|
|
780
|
+
const provider = providerMap.get(account.ProviderID);
|
|
781
|
+
if (provider) {
|
|
782
|
+
accountInputs.push({ accountEntity: account, providerEntity: provider });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Execute the search across all accounts with account-based credentials
|
|
787
|
+
const result = await searchAcrossAccounts(accountInputs, input.Query, {
|
|
788
|
+
maxResultsPerAccount: input.MaxResultsPerAccount,
|
|
789
|
+
fileTypes: input.FileTypes,
|
|
790
|
+
searchContent: input.SearchContent,
|
|
791
|
+
contextUser: user,
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
console.log('[FileResolver] SearchAcrossAccounts result:', {
|
|
795
|
+
totalResultsReturned: result.totalResultsReturned,
|
|
796
|
+
successfulAccounts: result.successfulAccounts,
|
|
797
|
+
failedAccounts: result.failedAccounts,
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Convert results to GraphQL payload format
|
|
801
|
+
const accountResults: AccountSearchResultPayload[] = result.accountResults.map((ar: AccountSearchResult) => ({
|
|
802
|
+
accountID: ar.accountID,
|
|
803
|
+
accountName: ar.accountName,
|
|
804
|
+
success: ar.success,
|
|
805
|
+
errorMessage: ar.errorMessage,
|
|
806
|
+
results: ar.results.map((r: FileSearchResult) => ({
|
|
807
|
+
path: r.path,
|
|
808
|
+
name: r.name,
|
|
809
|
+
size: r.size,
|
|
810
|
+
contentType: r.contentType,
|
|
811
|
+
lastModified: r.lastModified.toISOString(),
|
|
812
|
+
relevance: r.relevance,
|
|
813
|
+
excerpt: r.excerpt,
|
|
814
|
+
matchInFilename: r.matchInFilename,
|
|
815
|
+
objectId: r.objectId,
|
|
816
|
+
})),
|
|
817
|
+
totalMatches: ar.totalMatches,
|
|
818
|
+
hasMore: ar.hasMore,
|
|
819
|
+
nextPageToken: ar.nextPageToken,
|
|
820
|
+
}));
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
accountResults,
|
|
824
|
+
totalResultsReturned: result.totalResultsReturned,
|
|
825
|
+
successfulAccounts: result.successfulAccounts,
|
|
826
|
+
failedAccounts: result.failedAccounts,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
157
829
|
}
|