@memberjunction/server 6.1.0-edge.2 → 6.1.0-edge.3
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/LICENSE +180 -4
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +56 -13
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +501 -1597
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2711 -17021
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +7 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -14
- package/dist/index.js.map +1 -1
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +50 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +76 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +299 -81
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +129 -43
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +21 -1
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +23 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +91 -91
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/index.ts +59 -15
- package/src/auth/initializeProviders.ts +128 -7
- package/src/config.ts +55 -36
- package/src/generated/generated.ts +1911 -10551
- package/src/generic/ResolverBase.ts +11 -4
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +108 -15
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +77 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IntegrationDiscoveryResolver.ts +316 -68
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +149 -41
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/StartTaskGraphDispatcher.ts +21 -1
- package/src/services/TaskGraphAgentRunner.ts +23 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/types.ts +6 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure reporting helpers for the connector schema-refresh pipeline launch.
|
|
3
|
+
*
|
|
4
|
+
* The refresh is a live vendor introspect and can run for minutes on a large catalog, so
|
|
5
|
+
* IntegrationCreateConnection / IntegrationUpdateConnection can launch it *detached* and hand the
|
|
6
|
+
* caller a tailable run ID instead of blocking. That split creates one easy-to-get-wrong reporting
|
|
7
|
+
* case: a detached summary's counts are placeholder zeros, and describing them as results ("0
|
|
8
|
+
* created, 0 updated") reads to an operator as "the refresh found nothing" — the opposite of "the
|
|
9
|
+
* refresh has not finished yet". This module owns that decision so it can be pinned by a test
|
|
10
|
+
* without standing up a resolver.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The subset of the pipeline summary that message construction reads. Structural so both the
|
|
15
|
+
* GraphQL `@ObjectType` and a test fixture satisfy it without either depending on the other.
|
|
16
|
+
*/
|
|
17
|
+
export interface SchemaRefreshSummaryLike {
|
|
18
|
+
/** Tailable run ID — the only meaningful field while `InProgress` is true. */
|
|
19
|
+
RunID: string;
|
|
20
|
+
/** True ⇒ launched detached and still running; every count below is a placeholder zero. */
|
|
21
|
+
InProgress: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the pipeline itself succeeded. A pipeline that fails at ConnectionTest RETURNS rather
|
|
24
|
+
* than throwing, with every count at zero — so counts alone read identically to a clean
|
|
25
|
+
* no-op refresh. Reporting that as "0 created, 0 updated" tells an operator the vendor had
|
|
26
|
+
* nothing new when in fact the credentials were never accepted.
|
|
27
|
+
*/
|
|
28
|
+
Succeeded: boolean;
|
|
29
|
+
/** The pipeline's own failure reason, when `Succeeded` is false. */
|
|
30
|
+
FailureMessage?: string;
|
|
31
|
+
ObjectsCreated: number;
|
|
32
|
+
ObjectsUpdated: number;
|
|
33
|
+
UnresolvedObjects: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The counts clause, or an explicit failure clause when the pipeline did not succeed. */
|
|
37
|
+
function describeFinishedRefresh(summary: SchemaRefreshSummaryLike): string {
|
|
38
|
+
if (!summary.Succeeded) {
|
|
39
|
+
return `schema refresh FAILED (${summary.FailureMessage ?? 'no reason reported'}) — see run ${summary.RunID}`;
|
|
40
|
+
}
|
|
41
|
+
return `schema refresh: ${summary.ObjectsCreated} created, ` +
|
|
42
|
+
`${summary.ObjectsUpdated} updated, ${summary.UnresolvedObjects.length} PK-unresolved`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds the human-facing `Message` for IntegrationCreateConnection.
|
|
47
|
+
*
|
|
48
|
+
* @param testConnection whether the caller asked for (and got) a live connection test
|
|
49
|
+
* @param summary the refresh summary, or undefined when no refresh was requested
|
|
50
|
+
*/
|
|
51
|
+
export function BuildCreateConnectionMessage(
|
|
52
|
+
testConnection: boolean,
|
|
53
|
+
summary: SchemaRefreshSummaryLike | undefined
|
|
54
|
+
): string {
|
|
55
|
+
if (!summary) return 'Connection created and test passed';
|
|
56
|
+
const created = `Connection created${testConnection ? ', test passed' : ''}`;
|
|
57
|
+
// Never report a detached run's placeholder counts as findings — point at the run stream instead.
|
|
58
|
+
if (summary.InProgress) return `${created}, schema refresh running — tail run ${summary.RunID}`;
|
|
59
|
+
return `${created}, ${describeFinishedRefresh(summary)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds the human-facing `Message` for IntegrationUpdateConnection's blocking-refresh path.
|
|
64
|
+
* Shares {@link describeFinishedRefresh} with create so a failed refresh can never be reported as
|
|
65
|
+
* a clean zero-count run on one path and honestly on the other.
|
|
66
|
+
*/
|
|
67
|
+
export function BuildUpdateConnectionMessage(summary: SchemaRefreshSummaryLike): string {
|
|
68
|
+
return `Updated, ${describeFinishedRefresh(summary)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Builds the human-facing `Message` for IntegrationUpdateConnection's detached-refresh path.
|
|
73
|
+
* Separate from {@link BuildCreateConnectionMessage} because update has no connection-test clause.
|
|
74
|
+
*/
|
|
75
|
+
export function BuildDetachedRefreshMessage(runID: string): string {
|
|
76
|
+
return `Updated, schema refresh running — tail run ${runID}`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit-conversion seam between MJServer's configuration and SQLServerDataProvider.
|
|
3
|
+
*
|
|
4
|
+
* MJServer configures `databaseSettings.metadataCacheRefreshInterval` in **milliseconds**
|
|
5
|
+
* (default 180000 = 3 minutes, overridable via the METADATA_CACHE_REFRESH_INTERVAL env var).
|
|
6
|
+
* `SQLServerProviderConfigData`'s third constructor parameter is `checkRefreshIntervalSeconds`
|
|
7
|
+
* — declared in **seconds**.
|
|
8
|
+
*
|
|
9
|
+
* Commit 645c5a5e8 fixed a bug where the raw millisecond value was handed straight to the
|
|
10
|
+
* provider, so the "3 minute" default became a 180000-second (~50 hour) metadata cache
|
|
11
|
+
* refresh cadence. This function is the single place that conversion happens; both provider
|
|
12
|
+
* bootstrap call sites in `index.ts` (the main pool and the CodeGen-credentialed pool) go
|
|
13
|
+
* through it, and `config-units.test.ts` pins the contract so the units can never silently
|
|
14
|
+
* regress again.
|
|
15
|
+
*
|
|
16
|
+
* @param metadataCacheRefreshIntervalMs - the configured refresh interval, in milliseconds
|
|
17
|
+
* @returns the same interval expressed in seconds, the unit `checkRefreshIntervalSeconds` declares
|
|
18
|
+
*/
|
|
19
|
+
export function MetadataCacheRefreshIntervalSeconds(metadataCacheRefreshIntervalMs: number): number {
|
|
20
|
+
return metadataCacheRefreshIntervalMs / 1000;
|
|
21
|
+
}
|
|
@@ -41,6 +41,7 @@ import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
|
41
41
|
import { GetReadOnlyProvider } from '../util.js';
|
|
42
42
|
import { configInfo } from '../config.js';
|
|
43
43
|
import { MediaAccessKeyManager } from '../rest/MediaAccessKeys.js';
|
|
44
|
+
import { UploadTokenManager } from '../rest/UploadTokenManager.js';
|
|
44
45
|
import { deriveSidecarPath, parsePeaksSidecar } from './peaksSidecar.js';
|
|
45
46
|
|
|
46
47
|
@InputType()
|
|
@@ -49,6 +50,63 @@ export class CreateUploadURLInput {
|
|
|
49
50
|
FileID: string;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
@InputType()
|
|
54
|
+
export class UploadStorageFileInput {
|
|
55
|
+
@Field(() => String, { nullable: true })
|
|
56
|
+
FileName?: string;
|
|
57
|
+
|
|
58
|
+
@Field(() => String, { nullable: true })
|
|
59
|
+
Base64Data?: string;
|
|
60
|
+
|
|
61
|
+
@Field(() => String, { nullable: true })
|
|
62
|
+
UploadToken?: string;
|
|
63
|
+
|
|
64
|
+
@Field(() => String, { nullable: true })
|
|
65
|
+
MimeType?: string;
|
|
66
|
+
|
|
67
|
+
@Field(() => String, { nullable: true })
|
|
68
|
+
AccountID?: string;
|
|
69
|
+
|
|
70
|
+
@Field(() => String, { nullable: true })
|
|
71
|
+
CategoryID?: string;
|
|
72
|
+
|
|
73
|
+
@Field(() => String, { nullable: true })
|
|
74
|
+
Description?: string;
|
|
75
|
+
|
|
76
|
+
@Field(() => String, { nullable: true })
|
|
77
|
+
PathPrefix?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@ObjectType()
|
|
81
|
+
export class UploadStorageFilePayload {
|
|
82
|
+
@Field(() => Boolean)
|
|
83
|
+
Success: boolean;
|
|
84
|
+
|
|
85
|
+
@Field(() => String, { nullable: true })
|
|
86
|
+
FileID?: string;
|
|
87
|
+
|
|
88
|
+
@Field(() => MJFile_, { nullable: true })
|
|
89
|
+
File?: MJFile_;
|
|
90
|
+
|
|
91
|
+
@Field(() => String, { nullable: true })
|
|
92
|
+
ErrorMessage?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@ObjectType()
|
|
96
|
+
export class CreateUploadStageTokenPayload {
|
|
97
|
+
@Field(() => Boolean)
|
|
98
|
+
Success: boolean;
|
|
99
|
+
|
|
100
|
+
@Field(() => String, { nullable: true })
|
|
101
|
+
Token?: string;
|
|
102
|
+
|
|
103
|
+
@Field(() => String, { nullable: true })
|
|
104
|
+
Url?: string;
|
|
105
|
+
|
|
106
|
+
@Field(() => String, { nullable: true })
|
|
107
|
+
ErrorMessage?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
52
110
|
@ObjectType()
|
|
53
111
|
export class CreateFilePayload {
|
|
54
112
|
@Field(() => MJFile_)
|
|
@@ -414,7 +472,8 @@ export class FileResolver extends FileResolverBase {
|
|
|
414
472
|
|
|
415
473
|
// Access authorized — mint the capability token.
|
|
416
474
|
const { Token, ExpiresAt } = MediaAccessKeyManager.Instance.Sign(fileId, contextUser.ID);
|
|
417
|
-
const
|
|
475
|
+
const fileNameSuffix = fileEntity.Name ? `/${encodeURIComponent(fileEntity.Name)}` : '';
|
|
476
|
+
const url = `${this.resolvePublicBaseUrl()}/media/${encodeURIComponent(fileId)}${fileNameSuffix}?token=${encodeURIComponent(Token)}`;
|
|
418
477
|
|
|
419
478
|
// Best-effort: surface precomputed waveform peaks from a peaks.json sidecar beside the file, so
|
|
420
479
|
// the player renders the real waveform instantly without fetching/decoding the audio. Never
|
|
@@ -573,6 +632,85 @@ export class FileResolver extends FileResolverBase {
|
|
|
573
632
|
return { File, UploadUrl, NameExists };
|
|
574
633
|
}
|
|
575
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Direct server-side file upload using MJ Storage subsystem.
|
|
637
|
+
* Uploads file contents directly via the configured storage driver (Box, Azure, S3, Google Drive, Dropbox, etc.)
|
|
638
|
+
* and creates/persists the MJ: Files database record.
|
|
639
|
+
*/
|
|
640
|
+
@Mutation(() => UploadStorageFilePayload)
|
|
641
|
+
async UploadStorageFile(
|
|
642
|
+
@Arg('input', () => UploadStorageFileInput) input: UploadStorageFileInput,
|
|
643
|
+
@Ctx() context: AppContext
|
|
644
|
+
): Promise<UploadStorageFilePayload> {
|
|
645
|
+
try {
|
|
646
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
647
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
648
|
+
|
|
649
|
+
// Permission check on MJ: Files
|
|
650
|
+
const fileEntity = await provider.GetEntityObject<MJFileEntity>('MJ: Files', user);
|
|
651
|
+
fileEntity.CheckPermissions(EntityPermissionType.Create, true);
|
|
652
|
+
|
|
653
|
+
let buffer: Buffer;
|
|
654
|
+
let fileName: string = input.FileName || 'file';
|
|
655
|
+
let mimeType: string = input.MimeType || 'application/octet-stream';
|
|
656
|
+
|
|
657
|
+
if (input.UploadToken) {
|
|
658
|
+
const staged = UploadTokenManager.Instance.Consume(input.UploadToken, user.ID);
|
|
659
|
+
if (!staged) {
|
|
660
|
+
return {
|
|
661
|
+
Success: false,
|
|
662
|
+
ErrorMessage: 'Upload token is invalid, expired, or has already been used.',
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
buffer = staged.buffer;
|
|
666
|
+
fileName = input.FileName || staged.fileName || 'file';
|
|
667
|
+
mimeType = input.MimeType || staged.mimeType || 'application/octet-stream';
|
|
668
|
+
} else if (input.Base64Data) {
|
|
669
|
+
buffer = Buffer.from(input.Base64Data, 'base64');
|
|
670
|
+
} else {
|
|
671
|
+
return {
|
|
672
|
+
Success: false,
|
|
673
|
+
ErrorMessage: 'Either UploadToken or Base64Data must be provided.',
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Ensure FileStorageEngine is configured under the user context
|
|
678
|
+
await FileStorageEngine.Instance.Config(false, user, provider);
|
|
679
|
+
if (input.AccountID && !FileStorageEngine.Instance.GetAccountById(input.AccountID)) {
|
|
680
|
+
await FileStorageEngine.Instance.Config(true, user, provider);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const uploadResult = await FileStorageEngine.Instance.UploadFile({
|
|
684
|
+
content: buffer,
|
|
685
|
+
fileName,
|
|
686
|
+
mimeType,
|
|
687
|
+
contextUser: user,
|
|
688
|
+
storageAccountId: input.AccountID,
|
|
689
|
+
categoryID: input.CategoryID,
|
|
690
|
+
description: input.Description,
|
|
691
|
+
pathPrefix: input.PathPrefix,
|
|
692
|
+
provider,
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
const loaded = await fileEntity.Load(uploadResult.FileID);
|
|
696
|
+
const mapper = new FieldMapper();
|
|
697
|
+
const File = loaded ? (mapper.MapFields({ ...fileEntity.GetAll() }) as unknown as MJFile_) : undefined;
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
Success: true,
|
|
701
|
+
FileID: uploadResult.FileID,
|
|
702
|
+
File,
|
|
703
|
+
};
|
|
704
|
+
} catch (error) {
|
|
705
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
706
|
+
LogError(`UploadStorageFile failed for '${input.FileName}': ${message}`);
|
|
707
|
+
return {
|
|
708
|
+
Success: false,
|
|
709
|
+
ErrorMessage: message,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
576
714
|
@FieldResolver(() => String)
|
|
577
715
|
async DownloadUrl(@Root() file: MJFile_, @Ctx() context: AppContext) {
|
|
578
716
|
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
@@ -580,6 +718,19 @@ export class FileResolver extends FileResolverBase {
|
|
|
580
718
|
const fileEntity = await md.GetEntityObject<MJFileEntity>('MJ: Files', user);
|
|
581
719
|
fileEntity.CheckPermissions(EntityPermissionType.Read, true);
|
|
582
720
|
|
|
721
|
+
await FileStorageEngine.Instance.Config(false, user, md);
|
|
722
|
+
let accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
|
|
723
|
+
if (accounts.length === 0) {
|
|
724
|
+
await FileStorageEngine.Instance.Config(true, user, md);
|
|
725
|
+
accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (accounts.length > 0) {
|
|
729
|
+
const account = accounts[0];
|
|
730
|
+
const driver = await FileStorageEngine.Instance.GetDriver(account.ID, user);
|
|
731
|
+
return await driver.CreatePreAuthDownloadUrl(file.ProviderKey ?? file.Name);
|
|
732
|
+
}
|
|
733
|
+
|
|
583
734
|
const providerEntity = await md.GetEntityObject<MJFileStorageProviderEntity>('MJ: File Storage Providers', user);
|
|
584
735
|
await providerEntity.Load(file.ProviderID);
|
|
585
736
|
|
|
@@ -734,16 +885,13 @@ export class FileResolver extends FileResolverBase {
|
|
|
734
885
|
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
735
886
|
const user = this.GetUserFromPayload(context.userPayload);
|
|
736
887
|
|
|
737
|
-
// Load the account and its provider
|
|
738
|
-
const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
739
|
-
|
|
740
888
|
// Check permissions
|
|
741
889
|
const fileEntity = await md.GetEntityObject<MJFileEntity>('MJ: Files', user);
|
|
742
890
|
fileEntity.CheckPermissions(EntityPermissionType.Read, true);
|
|
743
891
|
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
const downloadUrl = await
|
|
892
|
+
await FileStorageEngine.Instance.Config(false, user, md);
|
|
893
|
+
const driver = await FileStorageEngine.Instance.GetDriver(input.AccountID, user);
|
|
894
|
+
const downloadUrl = await driver.CreatePreAuthDownloadUrl(input.ObjectName);
|
|
747
895
|
return downloadUrl;
|
|
748
896
|
}
|
|
749
897
|
|
|
@@ -762,67 +910,79 @@ export class FileResolver extends FileResolverBase {
|
|
|
762
910
|
* @see CreateFile for the legacy path that also creates a File entity record.
|
|
763
911
|
*/
|
|
764
912
|
@Mutation(() => CreatePreAuthUploadUrlPayload)
|
|
765
|
-
async CreatePreAuthUploadUrl(
|
|
913
|
+
async CreatePreAuthUploadUrl(
|
|
914
|
+
@Arg('input', () => CreatePreAuthUploadUrlInput) input: CreatePreAuthUploadUrlInput,
|
|
915
|
+
@Ctx() context: AppContext
|
|
916
|
+
): Promise<CreatePreAuthUploadUrlPayload> {
|
|
766
917
|
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
767
918
|
const user = this.GetUserFromPayload(context.userPayload);
|
|
768
919
|
|
|
769
|
-
// Load the account and its provider
|
|
770
|
-
const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
771
|
-
|
|
772
920
|
// Check permissions
|
|
773
921
|
const fileEntity = await md.GetEntityObject<MJFileEntity>('MJ: Files', user);
|
|
774
922
|
fileEntity.CheckPermissions(EntityPermissionType.Create, true);
|
|
775
923
|
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
ProviderID: providerEntity.ID,
|
|
784
|
-
ContentType: input.ContentType,
|
|
785
|
-
},
|
|
786
|
-
userContext,
|
|
787
|
-
);
|
|
788
|
-
|
|
789
|
-
// Extract ProviderKey if it exists (spread into updatedInput by createUploadUrl)
|
|
790
|
-
const providerKey = (updatedInput as { ProviderKey?: string }).ProviderKey;
|
|
924
|
+
await FileStorageEngine.Instance.Config(false, user, md);
|
|
925
|
+
const driver = await FileStorageEngine.Instance.GetDriver(input.AccountID, user);
|
|
926
|
+
if (!driver.SupportsPreAuthUpload) {
|
|
927
|
+
throw new Error(`Storage provider '${driver.constructor.name}' does not support pre-authenticated direct uploads.`);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const result = await driver.CreatePreAuthUploadUrl(input.ObjectName);
|
|
791
931
|
|
|
792
932
|
return {
|
|
793
|
-
UploadUrl,
|
|
794
|
-
ProviderKey:
|
|
933
|
+
UploadUrl: result.UploadUrl,
|
|
934
|
+
ProviderKey: result.ProviderKey,
|
|
795
935
|
};
|
|
796
936
|
}
|
|
797
937
|
|
|
938
|
+
/**
|
|
939
|
+
* Mints a cryptographically signed, short-lived media-upload token for staging raw binary files.
|
|
940
|
+
* Checks user permissions on `MJ: Files` before issuing.
|
|
941
|
+
*/
|
|
942
|
+
@Mutation(() => CreateUploadStageTokenPayload)
|
|
943
|
+
async CreateUploadStageToken(
|
|
944
|
+
@Ctx() context: AppContext
|
|
945
|
+
): Promise<CreateUploadStageTokenPayload> {
|
|
946
|
+
try {
|
|
947
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
948
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
949
|
+
|
|
950
|
+
// Permission check on MJ: Files
|
|
951
|
+
const fileEntity = await provider.GetEntityObject<MJFileEntity>('MJ: Files', user);
|
|
952
|
+
fileEntity.CheckPermissions(EntityPermissionType.Create, true);
|
|
953
|
+
|
|
954
|
+
const mint = MediaAccessKeyManager.Instance.SignUpload(user.ID);
|
|
955
|
+
|
|
956
|
+
return {
|
|
957
|
+
Success: true,
|
|
958
|
+
Token: mint.Token,
|
|
959
|
+
Url: '/media/upload-stage',
|
|
960
|
+
};
|
|
961
|
+
} catch (err) {
|
|
962
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
963
|
+
return {
|
|
964
|
+
Success: false,
|
|
965
|
+
ErrorMessage: message,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
798
970
|
@Mutation(() => Boolean)
|
|
799
971
|
async DeleteStorageObject(@Arg('input', () => DeleteStorageObjectInput) input: DeleteStorageObjectInput, @Ctx() context: AppContext) {
|
|
800
|
-
console.log('[FileResolver] DeleteStorageObject called:', input);
|
|
801
|
-
|
|
802
972
|
const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
803
973
|
const user = this.GetUserFromPayload(context.userPayload);
|
|
804
974
|
|
|
805
975
|
// Load the account and its provider
|
|
806
976
|
const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
|
|
807
977
|
|
|
808
|
-
console.log('[FileResolver] Provider loaded:', {
|
|
809
|
-
providerID: providerEntity.ID,
|
|
810
|
-
providerName: providerEntity.Name,
|
|
811
|
-
serverDriverKey: providerEntity.ServerDriverKey,
|
|
812
|
-
});
|
|
813
|
-
|
|
814
978
|
// Check permissions
|
|
815
979
|
const fileEntity = await md.GetEntityObject<MJFileEntity>('MJ: Files', user);
|
|
816
980
|
fileEntity.CheckPermissions(EntityPermissionType.Delete, true);
|
|
817
981
|
|
|
818
|
-
console.log('[FileResolver] Permissions checked, calling deleteObject...');
|
|
819
|
-
|
|
820
982
|
// Delete the object with extended user context (includes account for credential lookup)
|
|
821
983
|
const userContext = this.buildExtendedUserContext(context, account);
|
|
822
984
|
const success = await deleteObject(providerEntity, input.ObjectName, userContext);
|
|
823
985
|
|
|
824
|
-
console.log('[FileResolver] deleteObject returned:', success);
|
|
825
|
-
|
|
826
986
|
return success;
|
|
827
987
|
}
|
|
828
988
|
|