@eluvio/elv-client-js 4.2.29 → 4.2.31
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/package.json +1 -1
- package/src/AuthorizationClient.js +5 -0
- package/src/ElvClient.js +79 -5
- package/src/FrameClient.js +16 -0
- package/src/client/ContentAccess.js +192 -0
- package/src/client/ContentManagement.js +366 -9
- package/src/client/LiveStream.js +37 -0
- package/testScripts/Test.js +14 -2
- package/utilities/ObjectInfo.js +99 -0
- package/utilities/TenantContent.js +136 -0
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ const Ethers = require("ethers");
|
|
|
3
3
|
const Utils = require("./Utils");
|
|
4
4
|
const UrlJoin = require("url-join");
|
|
5
5
|
const {LogMessage} = require("./LogMessage");
|
|
6
|
+
const {ValidateObject, ValidateParameters} = require("./Validation");
|
|
6
7
|
|
|
7
8
|
/*
|
|
8
9
|
// -- Contract javascript files built using build/BuildContracts.js
|
|
@@ -119,6 +120,10 @@ class AuthorizationClient {
|
|
|
119
120
|
}) {
|
|
120
121
|
if(versionHash) { objectId = this.client.utils.DecodeVersionHash(versionHash).objectId; }
|
|
121
122
|
|
|
123
|
+
if(update && !objectId) {
|
|
124
|
+
throw Error("An objectId or versionHash is required when 'update' is true");
|
|
125
|
+
}
|
|
126
|
+
|
|
122
127
|
const isWalletRequest =
|
|
123
128
|
objectId &&
|
|
124
129
|
this.client.signer &&
|
package/src/ElvClient.js
CHANGED
|
@@ -131,6 +131,7 @@ class ElvClient {
|
|
|
131
131
|
* @param {Array<string>} authServiceURIs - A list of full URIs to auth service endpoints
|
|
132
132
|
* @param {Array<string>} fileServiceURIs - A list of full URIs to file service endpoints
|
|
133
133
|
* @param {Array<string>=} searchURIs - A list of full URIs to search service endpoints
|
|
134
|
+
* @param {Array<string>=} tenantQueryURIs - A list of full URIs to tenant query service endpoints
|
|
134
135
|
* @param {number=} ethereumContractTimeout=10 - Number of seconds to wait for contract calls
|
|
135
136
|
* @param {string=} trustAuthorityId - (OAuth) The ID of the trust authority to use for OAuth authentication
|
|
136
137
|
* @param {string=} staticToken - Static token that will be used for all authorization in place of normal auth. Also known as an anonymous token containing the space
|
|
@@ -152,6 +153,7 @@ class ElvClient {
|
|
|
152
153
|
authServiceURIs,
|
|
153
154
|
fileServiceURIs,
|
|
154
155
|
searchURIs,
|
|
156
|
+
tenantQueryURIs,
|
|
155
157
|
ethereumContractTimeout = 10,
|
|
156
158
|
trustAuthorityId,
|
|
157
159
|
staticToken,
|
|
@@ -179,6 +181,7 @@ class ElvClient {
|
|
|
179
181
|
this.fileServiceURIs = fileServiceURIs || fabricURIs;
|
|
180
182
|
this.ethereumURIs = ethereumURIs;
|
|
181
183
|
this.searchURIs = searchURIs;
|
|
184
|
+
this.tenantQueryURIs = tenantQueryURIs || [];
|
|
182
185
|
this.ethereumContractTimeout = ethereumContractTimeout;
|
|
183
186
|
|
|
184
187
|
this.trustAuthorityId = trustAuthorityId;
|
|
@@ -262,6 +265,11 @@ class ElvClient {
|
|
|
262
265
|
|
|
263
266
|
const searchURIs = fabricInfo.network.services.search || [];
|
|
264
267
|
|
|
268
|
+
let tenantQueryURIs = fabricInfo.network.services.tenant_query || [];
|
|
269
|
+
if(tenantQueryURIs.find(filterHTTPS)) {
|
|
270
|
+
tenantQueryURIs = tenantQueryURIs.filter(filterHTTPS);
|
|
271
|
+
}
|
|
272
|
+
|
|
265
273
|
const fabricVersion = Math.max(...(fabricInfo.network.api_versions || [2]));
|
|
266
274
|
|
|
267
275
|
return {
|
|
@@ -275,6 +283,7 @@ class ElvClient {
|
|
|
275
283
|
fileServiceURIs,
|
|
276
284
|
kmsURIs: kmsUrls,
|
|
277
285
|
searchURIs,
|
|
286
|
+
tenantQueryURIs,
|
|
278
287
|
fabricVersion
|
|
279
288
|
};
|
|
280
289
|
} catch(error) {
|
|
@@ -378,6 +387,7 @@ class ElvClient {
|
|
|
378
387
|
authServiceURIs,
|
|
379
388
|
fileServiceURIs,
|
|
380
389
|
searchURIs,
|
|
390
|
+
tenantQueryURIs,
|
|
381
391
|
fabricVersion
|
|
382
392
|
} = await ElvClient.Configuration({
|
|
383
393
|
configUrl,
|
|
@@ -395,6 +405,7 @@ class ElvClient {
|
|
|
395
405
|
authServiceURIs,
|
|
396
406
|
fileServiceURIs,
|
|
397
407
|
searchURIs,
|
|
408
|
+
tenantQueryURIs,
|
|
398
409
|
ethereumContractTimeout,
|
|
399
410
|
trustAuthorityId,
|
|
400
411
|
staticToken,
|
|
@@ -427,6 +438,7 @@ class ElvClient {
|
|
|
427
438
|
this.AuthHttpClient = new HttpClient({uris: this.authServiceURIs, networkName: this.networkName, debug: this.debug});
|
|
428
439
|
this.FileServiceHttpClient = new HttpClient({uris: this.fileServiceURIs, networkName: this.networkName, debug: this.debug});
|
|
429
440
|
this.SearchHttpClient = new HttpClient({uris: this.searchURIs || [], networkName: this.networkName, debug: this.debug});
|
|
441
|
+
this.TenantQueryHttpClient = new HttpClient({uris: this.tenantQueryURIs || [], networkName: this.networkName, debug: this.debug});
|
|
430
442
|
this.ethClient = new EthClient({client: this, uris: this.ethereumURIs, networkId: this.networkId, debug: this.debug, timeout: this.ethereumContractTimeout});
|
|
431
443
|
|
|
432
444
|
if(!this.signer) {
|
|
@@ -483,7 +495,7 @@ class ElvClient {
|
|
|
483
495
|
throw Error("Unable to change region: Configuration URL not set");
|
|
484
496
|
}
|
|
485
497
|
|
|
486
|
-
const {fabricURIs, ethereumURIs, authServiceURIs, fileServiceURIs, searchURIs} = await ElvClient.Configuration({
|
|
498
|
+
const {fabricURIs, ethereumURIs, authServiceURIs, fileServiceURIs, searchURIs, tenantQueryURIs} = await ElvClient.Configuration({
|
|
487
499
|
configUrl: this.configUrl,
|
|
488
500
|
region
|
|
489
501
|
});
|
|
@@ -495,7 +507,8 @@ class ElvClient {
|
|
|
495
507
|
ethereumURIs,
|
|
496
508
|
authServiceURIs,
|
|
497
509
|
fileServiceURIs,
|
|
498
|
-
searchURIs
|
|
510
|
+
searchURIs,
|
|
511
|
+
tenantQueryURIs
|
|
499
512
|
});
|
|
500
513
|
|
|
501
514
|
return {
|
|
@@ -503,7 +516,8 @@ class ElvClient {
|
|
|
503
516
|
ethereumURIs,
|
|
504
517
|
fileServiceURIs,
|
|
505
518
|
authServiceURIs,
|
|
506
|
-
searchURIs
|
|
519
|
+
searchURIs,
|
|
520
|
+
tenantQueryURIs
|
|
507
521
|
};
|
|
508
522
|
}
|
|
509
523
|
|
|
@@ -558,7 +572,8 @@ class ElvClient {
|
|
|
558
572
|
ethereumURIs: this.ethereumURIs,
|
|
559
573
|
authServiceURIs: this.authServiceURIs,
|
|
560
574
|
fileServiceURIs: this.fileServiceURIs,
|
|
561
|
-
searchURIs: this.searchURIs
|
|
575
|
+
searchURIs: this.searchURIs,
|
|
576
|
+
tenantQueryURIs: this.tenantQueryURIs
|
|
562
577
|
};
|
|
563
578
|
}
|
|
564
579
|
|
|
@@ -571,10 +586,11 @@ class ElvClient {
|
|
|
571
586
|
* @param {Array<string>=} authServiceURIs - A list of URLs for the auth service, in preference order
|
|
572
587
|
* @param {Array<string>=} fileServiceURIs - A list of URLs for file service jobs, in preference order
|
|
573
588
|
* @param {Array<string>=} searchURIs - A list of URLs for the search nodes, in preference order
|
|
589
|
+
* @param {Array<string>=} tenantQueryURIs - A list of URLs for the tenant query nodes, in preference order
|
|
574
590
|
*
|
|
575
591
|
* @methodGroup Nodes
|
|
576
592
|
*/
|
|
577
|
-
SetNodes({fabricURIs, ethereumURIs, authServiceURIs, fileServiceURIs, searchURIs}) {
|
|
593
|
+
SetNodes({fabricURIs, ethereumURIs, authServiceURIs, fileServiceURIs, searchURIs, tenantQueryURIs}) {
|
|
578
594
|
if(fabricURIs) {
|
|
579
595
|
this.fabricURIs = fabricURIs;
|
|
580
596
|
|
|
@@ -609,6 +625,13 @@ class ElvClient {
|
|
|
609
625
|
this.SearchHttpClient.uris = searchURIs;
|
|
610
626
|
this.SearchHttpClient.uriIndex = 0;
|
|
611
627
|
}
|
|
628
|
+
|
|
629
|
+
if(tenantQueryURIs) {
|
|
630
|
+
this.tenantQueryURIs = tenantQueryURIs;
|
|
631
|
+
|
|
632
|
+
this.TenantQueryHttpClient.uris = tenantQueryURIs;
|
|
633
|
+
this.TenantQueryHttpClient.uriIndex = 0;
|
|
634
|
+
}
|
|
612
635
|
}
|
|
613
636
|
|
|
614
637
|
/**
|
|
@@ -936,6 +959,57 @@ class ElvClient {
|
|
|
936
959
|
json 79b {"adr":"VVf4DQU357tDnZGYQeDrntRJ5rs=","spc":"ispc3ANoVSzNA3P6t7abLR69ho5YPPZU"}
|
|
937
960
|
*/
|
|
938
961
|
|
|
962
|
+
/*
|
|
963
|
+
PLAIN TOKEN PREFIX + BODY | aplsjcJf1HYcDDUuCdXcSZtU86nYK162YmYJeuqwMczEBJVkD5D5EvsBvVwYDRsf4hzDvBWMoe9piBpqx...
|
|
964
|
+
PREFIX 6b aplsjc | apl=plain s=ES256K jc=json-compressed
|
|
965
|
+
BODY base58(SIGNATURE + PAYLOAD)
|
|
966
|
+
SIGNATURE 65b ES256K signature
|
|
967
|
+
PAYLOAD raw-deflate JSON
|
|
968
|
+
json {"adr":"VVf4DQU357tDnZGYQeDrntRJ5rs=","spc":"ispc3ANoVSzNA3P6t7abLR69ho5YPPZU"}
|
|
969
|
+
*/
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Create a signed "plain" authorization token (`aplsjc...`).
|
|
973
|
+
*
|
|
974
|
+
* Plain tokens are largely the same as client-signed access tokens - they do not contain a b/c transaction
|
|
975
|
+
* and authorization is determined from the rights of the current signer's address.
|
|
976
|
+
* Currently needed for tenant content query /tenant/:tid/q/query
|
|
977
|
+
* This token will be deprecated in the future in favor of client-signed access tokens.
|
|
978
|
+
*
|
|
979
|
+
* @methodGroup Authorization
|
|
980
|
+
* @namedParams
|
|
981
|
+
* @param {string=} libraryId - Optional library ID to scope the token. If omitted, the token is space-wide.
|
|
982
|
+
* @returns {Promise<string>} - The signed Plain auth token
|
|
983
|
+
*/
|
|
984
|
+
async CreatePlainToken({libraryId}={}) {
|
|
985
|
+
const address = this.CurrentAccountAddress();
|
|
986
|
+
if(!address) {
|
|
987
|
+
throw Error("Unable to create plain token: No signer is configured");
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const token = {
|
|
991
|
+
adr: Buffer.from(address.replace(/^0x/, ""), "hex").toString("base64"),
|
|
992
|
+
spc: await this.ContentSpaceId()
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
if(libraryId) {
|
|
996
|
+
token.lib = libraryId;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const compressedToken = Pako.deflateRaw(Buffer.from(JSON.stringify(token), "utf-8"));
|
|
1000
|
+
const signature = await this.authClient.Sign(Ethers.utils.keccak256(compressedToken));
|
|
1001
|
+
const signatureBytes = Buffer.from(signature.replace(/^0x/, ""), "hex");
|
|
1002
|
+
|
|
1003
|
+
if(signatureBytes.length !== 65) {
|
|
1004
|
+
throw Error("Unable to create plain token: Expected a 65-byte ES256K signature");
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return `aplsjc${this.utils.B58(Buffer.concat([
|
|
1008
|
+
signatureBytes,
|
|
1009
|
+
Buffer.from(compressedToken)
|
|
1010
|
+
]))}`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
939
1013
|
async PersonalSign({
|
|
940
1014
|
message,
|
|
941
1015
|
addEthereumPrefix,
|
package/src/FrameClient.js
CHANGED
|
@@ -316,6 +316,9 @@ class FrameClient {
|
|
|
316
316
|
"AddAccessGroupMember",
|
|
317
317
|
"AddContentLibraryGroup",
|
|
318
318
|
"AddContentObjectGroupPermission",
|
|
319
|
+
"AddContentObjectGroups",
|
|
320
|
+
"AddContentObjectQueryFields",
|
|
321
|
+
"AddContentObjectTags",
|
|
319
322
|
"AddLibraryContentType",
|
|
320
323
|
"AssetMetadata",
|
|
321
324
|
"AuditContentObject",
|
|
@@ -341,10 +344,13 @@ class FrameClient {
|
|
|
341
344
|
"ContentObjectAccessComplete",
|
|
342
345
|
"ContentObjectGraph",
|
|
343
346
|
"ContentObjectGroupPermissions",
|
|
347
|
+
"ContentObjectGroups",
|
|
344
348
|
"ContentObjectImageUrl",
|
|
345
349
|
"ContentObjectLibraryId",
|
|
346
350
|
"ContentObjectMetadata",
|
|
347
351
|
"ContentObjectOwner",
|
|
352
|
+
"ContentObjectQueryFields",
|
|
353
|
+
"ContentObjectTags",
|
|
348
354
|
"ContentObjectTenantId",
|
|
349
355
|
"ContentObjectVersions",
|
|
350
356
|
"ContentObjects",
|
|
@@ -364,6 +370,7 @@ class FrameClient {
|
|
|
364
370
|
"CreateAccessGroup",
|
|
365
371
|
"CreateAndFinalizeContentObject",
|
|
366
372
|
"CreateAuthorizationToken",
|
|
373
|
+
"CreateContentGroup",
|
|
367
374
|
"CreateContentLibrary",
|
|
368
375
|
"CreateContentObject",
|
|
369
376
|
"CreateContentType",
|
|
@@ -375,6 +382,7 @@ class FrameClient {
|
|
|
375
382
|
"CreateNTPInstance",
|
|
376
383
|
"CreateNonOwnerCap",
|
|
377
384
|
"CreatePart",
|
|
385
|
+
"CreatePlainToken",
|
|
378
386
|
"CreateProductionMaster",
|
|
379
387
|
"CreateShare",
|
|
380
388
|
"CreateSignedMessageJSON",
|
|
@@ -450,6 +458,7 @@ class FrameClient {
|
|
|
450
458
|
"OutputsCreate",
|
|
451
459
|
"OutputsDelete",
|
|
452
460
|
"OutputsDeleteBatch",
|
|
461
|
+
"OutputsHop",
|
|
453
462
|
"OutputsList",
|
|
454
463
|
"OutputsListItem",
|
|
455
464
|
"OutputsModify",
|
|
@@ -473,6 +482,9 @@ class FrameClient {
|
|
|
473
482
|
"RemoveAccessGroupMember",
|
|
474
483
|
"RemoveContentObjectGroupPermission",
|
|
475
484
|
"RemoveContentLibraryGroup",
|
|
485
|
+
"RemoveContentObjectGroups",
|
|
486
|
+
"RemoveContentObjectQueryFields",
|
|
487
|
+
"RemoveContentObjectTags",
|
|
476
488
|
"RemoveLibraryContentType",
|
|
477
489
|
"Rep",
|
|
478
490
|
"ReplaceContractMetadata",
|
|
@@ -488,7 +500,10 @@ class FrameClient {
|
|
|
488
500
|
"SetAuthContext",
|
|
489
501
|
"SetAuthPolicy",
|
|
490
502
|
"SetContentLibraryImage",
|
|
503
|
+
"SetContentObjectGroups",
|
|
491
504
|
"SetContentObjectImage",
|
|
505
|
+
"SetContentObjectQueryFields",
|
|
506
|
+
"SetContentObjectTags",
|
|
492
507
|
"SetCustomContentContract",
|
|
493
508
|
"SetGroupPermission",
|
|
494
509
|
"SetNodes",
|
|
@@ -531,6 +546,7 @@ class FrameClient {
|
|
|
531
546
|
"StreamUnassignProfile",
|
|
532
547
|
"StreamUpdateConfig",
|
|
533
548
|
"SuspendNTPInstance",
|
|
549
|
+
"TenantContent",
|
|
534
550
|
"TenantContractId",
|
|
535
551
|
"TenantId",
|
|
536
552
|
"UnlinkAccessGroupFromOauth",
|
|
@@ -3305,3 +3305,195 @@ exports.QParts = async function({libraryId, objectId, partHash, format="blob"})
|
|
|
3305
3305
|
})
|
|
3306
3306
|
);
|
|
3307
3307
|
};
|
|
3308
|
+
|
|
3309
|
+
/* Content groups */
|
|
3310
|
+
|
|
3311
|
+
/**
|
|
3312
|
+
* Retrieve a content object property ("tags", "groups" or "query_fields").
|
|
3313
|
+
*
|
|
3314
|
+
* @private
|
|
3315
|
+
*/
|
|
3316
|
+
const ContentObjectProperty = async function({objectId, versionHash, writeToken, property}) {
|
|
3317
|
+
if(writeToken && versionHash) {
|
|
3318
|
+
throw Error(`Cannot specify writeToken and versionHash at same time (token:${writeToken}, hash:${versionHash})`);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
if(writeToken) {
|
|
3322
|
+
ValidateWriteToken(writeToken);
|
|
3323
|
+
} else if(versionHash) {
|
|
3324
|
+
ValidateVersion(versionHash);
|
|
3325
|
+
} else {
|
|
3326
|
+
ValidateObject(objectId);
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
if(writeToken) {
|
|
3330
|
+
objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
3331
|
+
} else if(versionHash) {
|
|
3332
|
+
objectId = this.utils.DecodeVersionHash(versionHash).objectId;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
const token = await this.GenerateStateChannelToken({
|
|
3336
|
+
objectId,
|
|
3337
|
+
versionHash
|
|
3338
|
+
});
|
|
3339
|
+
|
|
3340
|
+
return this.HttpClient.RequestJsonBody({
|
|
3341
|
+
headers: {Authorization: `Bearer ${token}`},
|
|
3342
|
+
method: "GET",
|
|
3343
|
+
path: UrlJoin("q", writeToken || versionHash || objectId, property)
|
|
3344
|
+
});
|
|
3345
|
+
};
|
|
3346
|
+
|
|
3347
|
+
/**
|
|
3348
|
+
* Get content tags
|
|
3349
|
+
*
|
|
3350
|
+
* @methodGroup Content Groups
|
|
3351
|
+
* @namedParams
|
|
3352
|
+
* @param {string=} objectId - ID of the object
|
|
3353
|
+
* @param {string=} versionHash - Hash of the object version
|
|
3354
|
+
* @param {string=} writeToken - Write token of the draft
|
|
3355
|
+
*
|
|
3356
|
+
* @returns {Promise<Array<string>>} - The content object tags
|
|
3357
|
+
*/
|
|
3358
|
+
exports.ContentObjectTags = async function({objectId, versionHash, writeToken}) {
|
|
3359
|
+
this.Log(`Retrieving content tags: ${writeToken || versionHash || objectId}`);
|
|
3360
|
+
|
|
3361
|
+
return ContentObjectProperty.call(this, {objectId, versionHash, writeToken, property: "tags"});
|
|
3362
|
+
};
|
|
3363
|
+
|
|
3364
|
+
/**
|
|
3365
|
+
* Get content query fields
|
|
3366
|
+
*
|
|
3367
|
+
* @methodGroup Content Groups
|
|
3368
|
+
* @namedParams
|
|
3369
|
+
* @param {string=} objectId - ID of the object
|
|
3370
|
+
* @param {string=} versionHash - Hash of the object version
|
|
3371
|
+
* @param {string=} writeToken - Write token of the draft
|
|
3372
|
+
*
|
|
3373
|
+
* @returns {Promise<Object<string, string>>} - The content object query fields
|
|
3374
|
+
*/
|
|
3375
|
+
exports.ContentObjectQueryFields = async function({objectId, versionHash, writeToken}) {
|
|
3376
|
+
this.Log(`Retrieving content query fields: ${writeToken || versionHash || objectId}`);
|
|
3377
|
+
|
|
3378
|
+
return ContentObjectProperty.call(this, {objectId, versionHash, writeToken, property: "query_fields"});
|
|
3379
|
+
};
|
|
3380
|
+
|
|
3381
|
+
/**
|
|
3382
|
+
* Get the groups a content object belongs to
|
|
3383
|
+
*
|
|
3384
|
+
* @methodGroup Content Groups
|
|
3385
|
+
* @namedParams
|
|
3386
|
+
* @param {string=} objectId - ID of the object
|
|
3387
|
+
* @param {string=} versionHash - Hash of the object version
|
|
3388
|
+
* @param {string=} writeToken - Write token of the draft
|
|
3389
|
+
*
|
|
3390
|
+
* @returns {Promise<Array<string>>} - The content groups the object belongs to
|
|
3391
|
+
*/
|
|
3392
|
+
exports.ContentObjectGroups = async function({objectId, versionHash, writeToken}) {
|
|
3393
|
+
this.Log(`Retrieving content object groups: ${writeToken || versionHash || objectId}`);
|
|
3394
|
+
|
|
3395
|
+
return ContentObjectProperty.call(this, {objectId, versionHash, writeToken, property: "groups"});
|
|
3396
|
+
};
|
|
3397
|
+
|
|
3398
|
+
/**
|
|
3399
|
+
* Query tenant content.
|
|
3400
|
+
*
|
|
3401
|
+
* The response is based on the fabric index and can use groups, query_fields and tags for filtering and sorting.
|
|
3402
|
+
* The response may include metadata fields as specified in select/remove.
|
|
3403
|
+
*
|
|
3404
|
+
* @methodGroup Tenants
|
|
3405
|
+
* @namedParams
|
|
3406
|
+
* @param {string=} tenantId - ID of the tenant to query. Defaults to the tenant of the current user
|
|
3407
|
+
* @param {Array<string>=} filter - Filter expressions of the form `<field>:<comparator>:<value>`, e.g.
|
|
3408
|
+
* `tag:eq:elv:media:live_stream` or `qf.date:ge:2026-09-01`. Multiple expressions are combined with AND.
|
|
3409
|
+
*
|
|
3410
|
+
* Comparators: `::` or `:co:` contains, `:nc:` not contains, `:eq:` equals, `:ne:` not equals,
|
|
3411
|
+
* `:lt:` less than, `:le:` less than or equal, `:gt:` greater than, `:ge:` greater than or equal.
|
|
3412
|
+
* Comparisons are lexicographic string comparisons. `:sw:` and `:ew:` are not supported here.
|
|
3413
|
+
*
|
|
3414
|
+
* Reserved fields:
|
|
3415
|
+
* - `tag` - matches when *any one* of the object's tags satisfies the expression
|
|
3416
|
+
* - `group` - ID of a content group the object belongs to
|
|
3417
|
+
* - `unique_group.id`, `unique_group.type`
|
|
3418
|
+
* - `lib`, `library` - library ID. Supports `:eq:` only
|
|
3419
|
+
* - `created_block_time` - when the latest version was created. Comparison operators only
|
|
3420
|
+
*
|
|
3421
|
+
* Any other field name is treated as a query field name. Prefix with `qf.` to filter on a query field
|
|
3422
|
+
* whose name collides with a reserved one, e.g. `qf.tag:eq:x`. `created_block_number` is no longer
|
|
3423
|
+
* supported and is rejected.
|
|
3424
|
+
*
|
|
3425
|
+
* @param {string=} sortBy - Name of a query field to sort by. This is a query field name, not a metadata path,
|
|
3426
|
+
* and only one may be given. When omitted, results are sorted by the time the latest version was created.
|
|
3427
|
+
*
|
|
3428
|
+
* Note that sorting by a query field also restricts the results to objects that have that field
|
|
3429
|
+
* @param {boolean=} sortDescending=false - Sort in descending order
|
|
3430
|
+
* @param {(Array<string> | string)=} include - Index data to return alongside each result. Tokens:
|
|
3431
|
+
* `tags`, `groups`, `uniquegroups`, `fields` (query fields), `type`, `name`, `all`, `none`. May be given
|
|
3432
|
+
* as an array or as a comma-separated string. The Fabric defaults to query fields only; `none` suppresses
|
|
3433
|
+
* all index data, including those
|
|
3434
|
+
* @param {Array<string>=} select - Metadata subtree paths to include in each result. If omitted, no metadata
|
|
3435
|
+
* is retrieved at all and each result contains only its IDs and index data
|
|
3436
|
+
* @param {Array<string>=} remove - Metadata subtree paths to omit from each result. Only has an effect when
|
|
3437
|
+
* `select` is also specified
|
|
3438
|
+
* @param {number=} start - Index of the first object to retrieve. Defaults to the first
|
|
3439
|
+
* @param {number=} limit - Maximum number of objects to return. Defaults to 100, capped at 998
|
|
3440
|
+
* @param {string=} authorizationToken - Additional authorization token for the request
|
|
3441
|
+
*
|
|
3442
|
+
* @returns {Promise<Object>} - The query result, including `versions` and `paging` information. One entry per
|
|
3443
|
+
* content object - the index holds only the latest version - carrying `id`, `hash`, `qlib_id`,
|
|
3444
|
+
* `last_modified_at` and whatever `include` selects - by default the object's query fields
|
|
3445
|
+
*/
|
|
3446
|
+
exports.TenantContent = async function({
|
|
3447
|
+
tenantId,
|
|
3448
|
+
filter=[],
|
|
3449
|
+
sortBy,
|
|
3450
|
+
sortDescending,
|
|
3451
|
+
include,
|
|
3452
|
+
select=[],
|
|
3453
|
+
remove=[],
|
|
3454
|
+
start,
|
|
3455
|
+
limit,
|
|
3456
|
+
authorizationToken
|
|
3457
|
+
}) {
|
|
3458
|
+
tenantId = tenantId || await this.userProfileClient.TenantContractId();
|
|
3459
|
+
ValidatePresence("tenantId", tenantId);
|
|
3460
|
+
|
|
3461
|
+
this.Log(`Querying content for tenant ${tenantId}`);
|
|
3462
|
+
|
|
3463
|
+
const path = UrlJoin("tenants", tenantId, "q", "query");
|
|
3464
|
+
|
|
3465
|
+
// Tenant query works best with plain token - it allows it to return objecte metadata fields.
|
|
3466
|
+
const token = authorizationToken || await this.CreatePlainToken();
|
|
3467
|
+
|
|
3468
|
+
const httpClient =
|
|
3469
|
+
this.TenantQueryHttpClient && this.TenantQueryHttpClient.uris && this.TenantQueryHttpClient.uris.length > 0
|
|
3470
|
+
? this.TenantQueryHttpClient
|
|
3471
|
+
: this.HttpClient;
|
|
3472
|
+
|
|
3473
|
+
const queryParams = {
|
|
3474
|
+
filter,
|
|
3475
|
+
sort_by: sortBy,
|
|
3476
|
+
sort_descending: sortDescending,
|
|
3477
|
+
include,
|
|
3478
|
+
select,
|
|
3479
|
+
remove,
|
|
3480
|
+
start,
|
|
3481
|
+
limit
|
|
3482
|
+
};
|
|
3483
|
+
|
|
3484
|
+
Object.keys(queryParams).forEach(key => {
|
|
3485
|
+
const value = queryParams[key];
|
|
3486
|
+
if(value === undefined || (Array.isArray(value) && value.length === 0)) {
|
|
3487
|
+
delete queryParams[key];
|
|
3488
|
+
}
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
return this.utils.ResponseToJson(
|
|
3492
|
+
httpClient.Request({
|
|
3493
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
3494
|
+
method: "GET",
|
|
3495
|
+
path: path,
|
|
3496
|
+
queryParams
|
|
3497
|
+
})
|
|
3498
|
+
);
|
|
3499
|
+
};
|
|
@@ -25,6 +25,34 @@ const {
|
|
|
25
25
|
ValidatePresence,
|
|
26
26
|
} = require("../Validation");
|
|
27
27
|
|
|
28
|
+
/* Validation for Fabric tags, groups and query fields */
|
|
29
|
+
|
|
30
|
+
const ValidateTags = (tags) => {
|
|
31
|
+
if(!Array.isArray(tags)) {
|
|
32
|
+
throw Error("Tags must be an array");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Group references are content object IDs. A library ID is also accepted, since a library
|
|
37
|
+
// may act as a group (see the Fabric's "lib" unique group type).
|
|
38
|
+
const ValidateGroups = (groups) => {
|
|
39
|
+
if(!Array.isArray(groups)) {
|
|
40
|
+
throw Error("Groups must be an array");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
groups.forEach(groupId => {
|
|
44
|
+
if(typeof groupId !== "string" || !(groupId.startsWith("iq__") || groupId.startsWith("ilib"))) {
|
|
45
|
+
throw Error(`Invalid group ID: ${groupId}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ValidateQueryFields = (queryFields) => {
|
|
51
|
+
if(typeof queryFields !== "object" || queryFields === null || Array.isArray(queryFields)) {
|
|
52
|
+
throw Error("Query fields must be an object");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
28
56
|
exports.SetVisibility = async function({id, visibility}) {
|
|
29
57
|
this.Log(`Setting visibility ${visibility} on ${id}`);
|
|
30
58
|
|
|
@@ -536,14 +564,18 @@ exports.RemoveLibraryContentType = async function({libraryId, typeId, typeName,
|
|
|
536
564
|
* @namedParams
|
|
537
565
|
* @param {string} libraryId - ID of the library
|
|
538
566
|
* @param {string=} objectId - ID of the object (if contract already exists)
|
|
539
|
-
* @param {Object=} options -
|
|
540
|
-
* type
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
567
|
+
* @param {Object=} options - Additional object creation options
|
|
568
|
+
* @param {string=} options.type - Version hash of the content type to associate with the object
|
|
569
|
+
* @param {Object=} options.meta - Metadata to use for the new object
|
|
570
|
+
* @param {string=} options.copy_from - Version hash of an existing object version to copy
|
|
571
|
+
* @param {string=} options.objectType - System-level object type, e.g. "cgroup" for a content group. May only be
|
|
572
|
+
* specified when creating a new object - the Fabric rejects an object type change on a new version of an existing object
|
|
573
|
+
* @param {Array<string>=} options.tags - Fabric tags to associate with the object
|
|
574
|
+
* @param {Array<string>=} options.groups - IDs of the content groups to add the object to. These categorize content and
|
|
575
|
+
* are distinct from the access groups that grant permissions
|
|
576
|
+
* @param {Object<string, string>=} options.queryFields - Unencrypted, indexed query fields to associate with the object
|
|
577
|
+
* @param {boolean=} options.noEncryptionConk - Set to true to prevent creation of an encryption conk for the object
|
|
578
|
+
* @param {boolean=} options.createKMSConk - Set to true to create a KMS conk for object (usually for sharing a playable object) (incompatible with noEncryptionConk: true)
|
|
547
579
|
*
|
|
548
580
|
* @returns {Promise<Object>} - Response containing the object ID and write token of the draft, as well as the url of the node that created the write token.
|
|
549
581
|
*/
|
|
@@ -555,6 +587,10 @@ exports.CreateContentObject = async function({libraryId, objectId, options={}})
|
|
|
555
587
|
|
|
556
588
|
if(options.noEncryptionConk && options.createKMSConk) throw new Error("Incompatible options: noEncryptionConk and createKMSConk both set to true");
|
|
557
589
|
|
|
590
|
+
if(options.tags !== undefined) { ValidateTags(options.tags); }
|
|
591
|
+
if(options.groups !== undefined) { ValidateGroups(options.groups); }
|
|
592
|
+
if(options.queryFields !== undefined) { ValidateQueryFields(options.queryFields); }
|
|
593
|
+
|
|
558
594
|
// Look up content type, if specified
|
|
559
595
|
let typeId;
|
|
560
596
|
if(options.type) {
|
|
@@ -613,7 +649,11 @@ exports.CreateContentObject = async function({libraryId, objectId, options={}})
|
|
|
613
649
|
body: { // filter out options not recognized by server (noEncryptionConk, createKMSConk)
|
|
614
650
|
type: options.type,
|
|
615
651
|
meta: options.meta,
|
|
616
|
-
copy_from: options.copy_from
|
|
652
|
+
copy_from: options.copy_from,
|
|
653
|
+
object_type: options.objectType,
|
|
654
|
+
tags: options.tags,
|
|
655
|
+
groups: options.groups,
|
|
656
|
+
query_fields: options.queryFields
|
|
617
657
|
}
|
|
618
658
|
});
|
|
619
659
|
|
|
@@ -1621,3 +1661,320 @@ exports.DeleteWriteToken = async function({writeToken}) {
|
|
|
1621
1661
|
|
|
1622
1662
|
await this.HttpClient.ClearWriteToken({writeToken});
|
|
1623
1663
|
};
|
|
1664
|
+
|
|
1665
|
+
/* Content group creation, modification, deletion */
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Create content tags
|
|
1669
|
+
*
|
|
1670
|
+
* @methodGroup Content Groups
|
|
1671
|
+
* @namedParams
|
|
1672
|
+
* @param {string} libraryId - ID of the library
|
|
1673
|
+
* @param {string} writeToken - Write token of the draft
|
|
1674
|
+
* @param {Array<string>} tags - List of tags
|
|
1675
|
+
*
|
|
1676
|
+
* @returns {Promise<Array<string>>} - The tags on the object after the update
|
|
1677
|
+
*/
|
|
1678
|
+
exports.AddContentObjectTags = async function({libraryId, writeToken, tags}) {
|
|
1679
|
+
ValidateLibrary(libraryId);
|
|
1680
|
+
ValidateWriteToken(writeToken);
|
|
1681
|
+
ValidatePresence("tags", tags);
|
|
1682
|
+
|
|
1683
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "tags");
|
|
1684
|
+
const objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
1685
|
+
|
|
1686
|
+
this.Log(`Adding tags: ${tags.join(", ")}`);
|
|
1687
|
+
|
|
1688
|
+
return this.HttpClient.RequestJsonBody({
|
|
1689
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
1690
|
+
method: "POST",
|
|
1691
|
+
path: path,
|
|
1692
|
+
body: tags,
|
|
1693
|
+
allowFailover: false
|
|
1694
|
+
});
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Update content tags, replacing any existing tags
|
|
1699
|
+
*
|
|
1700
|
+
* @methodGroup Content Groups
|
|
1701
|
+
* @namedParams
|
|
1702
|
+
* @param {string} libraryId - ID of the library
|
|
1703
|
+
* @param {string} writeToken - Write token of the draft
|
|
1704
|
+
* @param {Array<string>} tags - Tags the object should have. Must be given explicitly - pass an empty
|
|
1705
|
+
* array to remove all tags
|
|
1706
|
+
*
|
|
1707
|
+
* @returns {Promise<Array<string>>} - The tags on the object after the update
|
|
1708
|
+
*/
|
|
1709
|
+
exports.SetContentObjectTags = async function({libraryId, writeToken, tags}) {
|
|
1710
|
+
ValidateLibrary(libraryId);
|
|
1711
|
+
ValidateWriteToken(writeToken);
|
|
1712
|
+
ValidateTags(tags);
|
|
1713
|
+
|
|
1714
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "tags");
|
|
1715
|
+
const objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
1716
|
+
|
|
1717
|
+
this.Log(`Updating tags: ${Array.isArray(tags) ? tags.join(", ") : tags}`);
|
|
1718
|
+
|
|
1719
|
+
return this.HttpClient.RequestJsonBody({
|
|
1720
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
1721
|
+
method: "PUT",
|
|
1722
|
+
path: path,
|
|
1723
|
+
body: tags,
|
|
1724
|
+
allowFailover: false
|
|
1725
|
+
});
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* Remove the given tags from a content object
|
|
1730
|
+
*
|
|
1731
|
+
* @methodGroup Content Groups
|
|
1732
|
+
* @namedParams
|
|
1733
|
+
* @param {string} libraryId - ID of the library
|
|
1734
|
+
* @param {string} writeToken - Write token of the draft
|
|
1735
|
+
* @param {Array<string>} tags - List of tags
|
|
1736
|
+
*
|
|
1737
|
+
* @returns {Promise<Array<string>>} - The tags on the object after the update
|
|
1738
|
+
*/
|
|
1739
|
+
exports.RemoveContentObjectTags = async function({libraryId, writeToken, tags=[]}) {
|
|
1740
|
+
ValidateLibrary(libraryId);
|
|
1741
|
+
ValidateWriteToken(writeToken);
|
|
1742
|
+
ValidatePresence("tags", tags);
|
|
1743
|
+
|
|
1744
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "tags");
|
|
1745
|
+
const objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
1746
|
+
|
|
1747
|
+
this.Log(`Removing tags: ${tags.join(", ")}`);
|
|
1748
|
+
|
|
1749
|
+
return this.HttpClient.RequestJsonBody({
|
|
1750
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
1751
|
+
method: "DELETE",
|
|
1752
|
+
path: path,
|
|
1753
|
+
body: tags,
|
|
1754
|
+
allowFailover: false
|
|
1755
|
+
});
|
|
1756
|
+
};
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Create content query fields
|
|
1760
|
+
*
|
|
1761
|
+
* @methodGroup Content Groups
|
|
1762
|
+
* @namedParams
|
|
1763
|
+
* @param {string} libraryId - ID of the library
|
|
1764
|
+
* @param {string} writeToken - Write token of the draft
|
|
1765
|
+
* @param {Object} queryFields - Query fields defined by key/value pairs for each field
|
|
1766
|
+
*
|
|
1767
|
+
* @returns {Promise<Object<string, string>>} - The query fields on the object after the update
|
|
1768
|
+
*/
|
|
1769
|
+
exports.AddContentObjectQueryFields = async function({libraryId, writeToken, queryFields}) {
|
|
1770
|
+
ValidateLibrary(libraryId);
|
|
1771
|
+
ValidateWriteToken(writeToken);
|
|
1772
|
+
ValidatePresence("queryFields", queryFields);
|
|
1773
|
+
|
|
1774
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "query_fields");
|
|
1775
|
+
const objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
1776
|
+
|
|
1777
|
+
return this.HttpClient.RequestJsonBody({
|
|
1778
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
1779
|
+
method: "POST",
|
|
1780
|
+
path: path,
|
|
1781
|
+
body: queryFields,
|
|
1782
|
+
allowFailover: false
|
|
1783
|
+
});
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Replace existing content query fields
|
|
1788
|
+
*
|
|
1789
|
+
* @methodGroup Content Groups
|
|
1790
|
+
* @namedParams
|
|
1791
|
+
* @param {string} libraryId - ID of the library
|
|
1792
|
+
* @param {string} writeToken - Write token of the draft
|
|
1793
|
+
* @param {Object<string, string>} queryFields - Query fields as key/value pairs. Must be given explicitly -
|
|
1794
|
+
* pass an empty object to remove all fields
|
|
1795
|
+
*
|
|
1796
|
+
* @returns {Promise<Object<string, string>>} - The query fields on the object after the update
|
|
1797
|
+
*/
|
|
1798
|
+
exports.SetContentObjectQueryFields = async function({libraryId, writeToken, queryFields}) {
|
|
1799
|
+
ValidateLibrary(libraryId);
|
|
1800
|
+
ValidateWriteToken(writeToken);
|
|
1801
|
+
ValidateQueryFields(queryFields);
|
|
1802
|
+
|
|
1803
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "query_fields");
|
|
1804
|
+
const objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
1805
|
+
|
|
1806
|
+
return this.HttpClient.RequestJsonBody({
|
|
1807
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
1808
|
+
method: "PUT",
|
|
1809
|
+
path: path,
|
|
1810
|
+
body: queryFields,
|
|
1811
|
+
allowFailover: false
|
|
1812
|
+
});
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
/**
|
|
1816
|
+
* Remove the given query fields from a content object
|
|
1817
|
+
*
|
|
1818
|
+
* @methodGroup Content Groups
|
|
1819
|
+
* @namedParams
|
|
1820
|
+
* @param {string} libraryId - ID of the library
|
|
1821
|
+
* @param {string} writeToken - Write token of the draft
|
|
1822
|
+
* @param {Array<string>} fieldNames - Names of the query fields to remove
|
|
1823
|
+
*
|
|
1824
|
+
* @returns {Promise<Object<string, string>>} - The query fields on the object after the update
|
|
1825
|
+
*/
|
|
1826
|
+
exports.RemoveContentObjectQueryFields = async function({libraryId, writeToken, fieldNames=[]}) {
|
|
1827
|
+
ValidateLibrary(libraryId);
|
|
1828
|
+
ValidateWriteToken(writeToken);
|
|
1829
|
+
ValidatePresence("fieldNames", fieldNames);
|
|
1830
|
+
|
|
1831
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "query_fields");
|
|
1832
|
+
const objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
1833
|
+
|
|
1834
|
+
this.Log(`Removing query fields: ${fieldNames.join(", ")}`);
|
|
1835
|
+
|
|
1836
|
+
return this.HttpClient.RequestJsonBody({
|
|
1837
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
1838
|
+
method: "DELETE",
|
|
1839
|
+
path: path,
|
|
1840
|
+
body: fieldNames,
|
|
1841
|
+
allowFailover: false
|
|
1842
|
+
});
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Create a content group - a content object that other content objects can be associated with in order to
|
|
1847
|
+
* categorize them. Content groups are distinct from the access groups that grant permissions.
|
|
1848
|
+
*
|
|
1849
|
+
* The group is left as a draft; finalize it before adding content objects to it.
|
|
1850
|
+
*
|
|
1851
|
+
* @methodGroup Content Groups
|
|
1852
|
+
* @namedParams
|
|
1853
|
+
* @param {string} libraryId - ID of the library
|
|
1854
|
+
* @param {string=} name - Internal content management name of the group
|
|
1855
|
+
* @param {string=} displayTitle - Public-facing title of the group
|
|
1856
|
+
* @param {Array<string>=} tags - Fabric tags to associate with the group
|
|
1857
|
+
* @param {Object<string, string>=} queryFields - Unencrypted, indexed query fields to associate with the group,
|
|
1858
|
+
* as key/value pairs
|
|
1859
|
+
*
|
|
1860
|
+
* @returns {Promise<Object>} - Response containing the object ID and write token of the draft
|
|
1861
|
+
*/
|
|
1862
|
+
exports.CreateContentGroup = async function({libraryId, name, displayTitle, tags, queryFields}) {
|
|
1863
|
+
ValidateLibrary(libraryId);
|
|
1864
|
+
|
|
1865
|
+
const {objectId, writeToken} = await this.CreateContentObject({
|
|
1866
|
+
libraryId,
|
|
1867
|
+
options: {
|
|
1868
|
+
objectType: "cgroup",
|
|
1869
|
+
meta: {
|
|
1870
|
+
public: {
|
|
1871
|
+
name,
|
|
1872
|
+
asset_metadata: {
|
|
1873
|
+
display_title: displayTitle
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
},
|
|
1877
|
+
tags,
|
|
1878
|
+
queryFields
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
this.Log(`Created content group: ${objectId}`);
|
|
1883
|
+
|
|
1884
|
+
return {
|
|
1885
|
+
objectId,
|
|
1886
|
+
writeToken
|
|
1887
|
+
};
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1890
|
+
/**
|
|
1891
|
+
* Add a content object to the given content groups. Groups the object already belongs to are retained.
|
|
1892
|
+
*
|
|
1893
|
+
* @methodGroup Content Groups
|
|
1894
|
+
* @namedParams
|
|
1895
|
+
* @param {string} libraryId - ID of the library
|
|
1896
|
+
* @param {string} writeToken - Write token of the draft
|
|
1897
|
+
* @param {Array<string>} groups - IDs of the content groups to add the object to. An empty array is a no-op
|
|
1898
|
+
*
|
|
1899
|
+
* @returns {Promise<Array<string>>} - The groups the object belongs to after the update. Order is not preserved
|
|
1900
|
+
*/
|
|
1901
|
+
exports.AddContentObjectGroups = async function({libraryId, writeToken, groups=[]}) {
|
|
1902
|
+
ValidateLibrary(libraryId);
|
|
1903
|
+
ValidateWriteToken(writeToken);
|
|
1904
|
+
ValidateGroups(groups);
|
|
1905
|
+
|
|
1906
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "groups");
|
|
1907
|
+
const objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
1908
|
+
|
|
1909
|
+
this.Log(`Adding groups: ${groups.join(", ")}`);
|
|
1910
|
+
|
|
1911
|
+
return this.HttpClient.RequestJsonBody({
|
|
1912
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
1913
|
+
method: "POST",
|
|
1914
|
+
path: path,
|
|
1915
|
+
body: groups,
|
|
1916
|
+
allowFailover: false
|
|
1917
|
+
});
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
/**
|
|
1921
|
+
* Set the content groups a content object belongs to, replacing any existing groups.
|
|
1922
|
+
*
|
|
1923
|
+
* @methodGroup Content Groups
|
|
1924
|
+
* @namedParams
|
|
1925
|
+
* @param {string} libraryId - ID of the library
|
|
1926
|
+
* @param {string} writeToken - Write token of the draft
|
|
1927
|
+
* @param {Array<string>} groups - IDs of the content groups the object should belong to. Must be given
|
|
1928
|
+
* explicitly - pass an empty array to remove the object from all groups
|
|
1929
|
+
*
|
|
1930
|
+
* @returns {Promise<Array<string>>} - The groups the object belongs to after the update. Order is not preserved
|
|
1931
|
+
*/
|
|
1932
|
+
exports.SetContentObjectGroups = async function({libraryId, writeToken, groups}) {
|
|
1933
|
+
ValidateLibrary(libraryId);
|
|
1934
|
+
ValidateWriteToken(writeToken);
|
|
1935
|
+
ValidateGroups(groups);
|
|
1936
|
+
|
|
1937
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "groups");
|
|
1938
|
+
const objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
1939
|
+
|
|
1940
|
+
this.Log(`Setting groups: ${groups.join(", ")}`);
|
|
1941
|
+
|
|
1942
|
+
return this.HttpClient.RequestJsonBody({
|
|
1943
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
1944
|
+
method: "PUT",
|
|
1945
|
+
path: path,
|
|
1946
|
+
body: groups,
|
|
1947
|
+
allowFailover: false
|
|
1948
|
+
});
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1951
|
+
/**
|
|
1952
|
+
* Remove a content object from the given content groups. Groups not listed are retained, and
|
|
1953
|
+
* removing a group the object does not belong to is a no-op.
|
|
1954
|
+
*
|
|
1955
|
+
* @methodGroup Content Groups
|
|
1956
|
+
* @namedParams
|
|
1957
|
+
* @param {string} libraryId - ID of the library
|
|
1958
|
+
* @param {string} writeToken - Write token of the draft
|
|
1959
|
+
* @param {Array<string>} groups - IDs of the content groups to remove the object from. An empty array is a no-op
|
|
1960
|
+
*
|
|
1961
|
+
* @returns {Promise<Array<string>>} - The groups the object belongs to after the update. Order is not preserved
|
|
1962
|
+
*/
|
|
1963
|
+
exports.RemoveContentObjectGroups = async function({libraryId, writeToken, groups=[]}) {
|
|
1964
|
+
ValidateLibrary(libraryId);
|
|
1965
|
+
ValidateWriteToken(writeToken);
|
|
1966
|
+
ValidateGroups(groups);
|
|
1967
|
+
|
|
1968
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "groups");
|
|
1969
|
+
const objectId = this.utils.DecodeWriteToken(writeToken).objectId;
|
|
1970
|
+
|
|
1971
|
+
this.Log(`Removing groups: ${groups.join(", ")}`);
|
|
1972
|
+
|
|
1973
|
+
return this.HttpClient.RequestJsonBody({
|
|
1974
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
1975
|
+
method: "DELETE",
|
|
1976
|
+
path: path,
|
|
1977
|
+
body: groups,
|
|
1978
|
+
allowFailover: false
|
|
1979
|
+
});
|
|
1980
|
+
};
|
package/src/client/LiveStream.js
CHANGED
|
@@ -4215,6 +4215,43 @@ exports.OutputsReset = async function({libraryId, objectId, outputId}) {
|
|
|
4215
4215
|
}
|
|
4216
4216
|
};
|
|
4217
4217
|
|
|
4218
|
+
/**
|
|
4219
|
+
* Manually switch a live output's active input hop (e.g. primary <-> failover).
|
|
4220
|
+
*
|
|
4221
|
+
* @methodGroup Live Stream
|
|
4222
|
+
* @namedParams
|
|
4223
|
+
* @param {string=} libraryId - Library ID of the output settings object. If not provided, it will be retrieved automatically.
|
|
4224
|
+
* @param {string} objectId - Object ID of the output settings object
|
|
4225
|
+
* @param {string} outputId - ID of the output to switch
|
|
4226
|
+
* @param {number=} hop=0 - Target hop index (0 = primary input, 1 = first failover input)
|
|
4227
|
+
*
|
|
4228
|
+
* @returns {Promise<Object>} - Response from the hop call
|
|
4229
|
+
*/
|
|
4230
|
+
exports.OutputsHop = async function({libraryId, objectId, outputId, hop=0}) {
|
|
4231
|
+
ValidateObject(objectId);
|
|
4232
|
+
ValidatePresence("outputId", outputId);
|
|
4233
|
+
|
|
4234
|
+
if(!libraryId) {
|
|
4235
|
+
libraryId = await this.ContentObjectLibraryId({objectId});
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
// Route to a live egress node, then to the specific output's node
|
|
4239
|
+
const {restore} = await RouteToLiveEgress({client: this});
|
|
4240
|
+
await RouteToOutputNode({client: this, libraryId, objectId, outputId});
|
|
4241
|
+
|
|
4242
|
+
try {
|
|
4243
|
+
return await this.CallBitcodeMethod({
|
|
4244
|
+
libraryId,
|
|
4245
|
+
objectId,
|
|
4246
|
+
method: UrlJoin("live", "outputs", outputId, "ctrl", "hop"),
|
|
4247
|
+
queryParams: {hop},
|
|
4248
|
+
constant: false
|
|
4249
|
+
});
|
|
4250
|
+
} finally {
|
|
4251
|
+
restore();
|
|
4252
|
+
}
|
|
4253
|
+
};
|
|
4254
|
+
|
|
4218
4255
|
/**
|
|
4219
4256
|
* Delete a live output.
|
|
4220
4257
|
*
|
package/testScripts/Test.js
CHANGED
|
@@ -11,7 +11,7 @@ const Test = async () => {
|
|
|
11
11
|
|
|
12
12
|
try {
|
|
13
13
|
const client = await ElvClient.FromNetworkName({
|
|
14
|
-
networkName: "
|
|
14
|
+
networkName: "main"
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
const wallet = client.GenerateWallet();
|
|
@@ -21,7 +21,19 @@ const Test = async () => {
|
|
|
21
21
|
|
|
22
22
|
client.SetSigner({signer});
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const configUrl = new URL(await client.ConfigUrl());
|
|
25
|
+
configUrl.pathname = "/config";
|
|
26
|
+
// if(geo) { configUrl.searchParams.set("elvgeo", geo); } // optional region scoping
|
|
27
|
+
|
|
28
|
+
const fabricInfo = await (await fetch(configUrl.toString())).json();
|
|
29
|
+
// fabricInfo.network.services.* e.g. live_egress, ethereum, authority, etc.
|
|
30
|
+
const liveEgressUrls = fabricInfo?.network?.services?.live_egress;
|
|
31
|
+
console.log('fabricINfo', fabricInfo.network?.services?.tenant_query)
|
|
32
|
+
|
|
33
|
+
// tenant_query nodes are now wired into the client and used automatically by TenantContent:
|
|
34
|
+
console.log('client tenantQueryURIs', client.Nodes().tenantQueryURIs)
|
|
35
|
+
// const result = await client.TenantContent({limit: 5});
|
|
36
|
+
// console.log(JSON.stringify(result, null, 2));
|
|
25
37
|
} catch(error) {
|
|
26
38
|
console.error(error);
|
|
27
39
|
console.error(JSON.stringify(error, null, 2));
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// List all info for an object: details, profile, tags, content groups and query fields
|
|
2
|
+
|
|
3
|
+
const R = require("ramda");
|
|
4
|
+
|
|
5
|
+
const {NewOpt} = require("./lib/options");
|
|
6
|
+
|
|
7
|
+
const Utility = require("./lib/Utility");
|
|
8
|
+
|
|
9
|
+
const Client = require("./lib/concerns/Client");
|
|
10
|
+
const ExistObjOrVer = require("./lib/concerns/ExistObjOrVer");
|
|
11
|
+
const ArgOutfile = require("./lib/concerns/ArgOutfile");
|
|
12
|
+
|
|
13
|
+
// Sections that come from the Fabric's tags/groups/query_fields endpoints. A node that does not
|
|
14
|
+
// serve them should not prevent the rest of the info from being reported.
|
|
15
|
+
const FABRIC_PROPERTIES = [
|
|
16
|
+
{key: "tags", method: "ContentObjectTags", empty: []},
|
|
17
|
+
{key: "groups", method: "ContentObjectGroups", empty: []},
|
|
18
|
+
{key: "query_fields", method: "ContentObjectQueryFields", empty: {}}
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
class ObjectInfo extends Utility {
|
|
22
|
+
blueprint() {
|
|
23
|
+
return {
|
|
24
|
+
concerns: [Client, ExistObjOrVer, ArgOutfile],
|
|
25
|
+
options: [
|
|
26
|
+
NewOpt("details", {
|
|
27
|
+
descTemplate: "Include object details (id, hash, type, size stats). Default: true",
|
|
28
|
+
default: true,
|
|
29
|
+
type: "boolean"
|
|
30
|
+
}),
|
|
31
|
+
NewOpt("profile", {
|
|
32
|
+
descTemplate: "Include the content profile (owner, visibility, status code, KMS, tenant). Default: true",
|
|
33
|
+
default: true,
|
|
34
|
+
type: "boolean"
|
|
35
|
+
}),
|
|
36
|
+
NewOpt("properties", {
|
|
37
|
+
descTemplate: "Include Fabric tags, content groups and query fields. Default: true",
|
|
38
|
+
default: true,
|
|
39
|
+
type: "boolean"
|
|
40
|
+
})
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async body() {
|
|
46
|
+
const logger = this.logger;
|
|
47
|
+
const client = await this.concerns.Client.get();
|
|
48
|
+
|
|
49
|
+
const {libraryId, objectId, versionHash} = await this.concerns.ExistObjOrVer.argsProc();
|
|
50
|
+
|
|
51
|
+
const info = {
|
|
52
|
+
object_id: objectId,
|
|
53
|
+
version_hash: versionHash,
|
|
54
|
+
library_id: libraryId
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if(this.args.details || this.args.profile) {
|
|
58
|
+
// ContentObject requests details and profile together
|
|
59
|
+
const contentObject = await client.ContentObject({objectId, versionHash});
|
|
60
|
+
|
|
61
|
+
if(this.args.profile) {
|
|
62
|
+
info.profile = contentObject.content_profile;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if(this.args.details) {
|
|
66
|
+
info.details = R.omit(["content_profile"], contentObject);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if(this.args.properties) {
|
|
71
|
+
for(const {key, method, empty} of FABRIC_PROPERTIES) {
|
|
72
|
+
try {
|
|
73
|
+
info[key] = await client[method]({objectId, versionHash}) || empty;
|
|
74
|
+
} catch(error) {
|
|
75
|
+
logger.warn(`Unable to retrieve ${key}: ${error.message || error}`);
|
|
76
|
+
info[key] = {error: error.message || String(error)};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
logger.data("object_info", info);
|
|
82
|
+
|
|
83
|
+
if(this.args.outfile) {
|
|
84
|
+
this.concerns.ArgOutfile.writeJson({obj: info});
|
|
85
|
+
} else {
|
|
86
|
+
logger.logObject(info);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
header() {
|
|
91
|
+
return `Get info for object ${this.args.versionHash || this.args.objectId}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if(require.main === module) {
|
|
96
|
+
Utility.cmdLineInvoke(ObjectInfo);
|
|
97
|
+
} else {
|
|
98
|
+
module.exports = ObjectInfo;
|
|
99
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Query the published content of a tenant using the Fabric content index
|
|
2
|
+
|
|
3
|
+
const {NewOpt} = require("./lib/options");
|
|
4
|
+
|
|
5
|
+
const Utility = require("./lib/Utility");
|
|
6
|
+
|
|
7
|
+
const Client = require("./lib/concerns/Client");
|
|
8
|
+
const ArgOutfile = require("./lib/concerns/ArgOutfile");
|
|
9
|
+
|
|
10
|
+
// Joined into a single cell so the table keeps a stable, readable width
|
|
11
|
+
const Join = value => Array.isArray(value) ? value.join(", ") : value;
|
|
12
|
+
|
|
13
|
+
const FlattenMap = obj =>
|
|
14
|
+
Object.entries(obj || {}).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
15
|
+
|
|
16
|
+
class TenantContent extends Utility {
|
|
17
|
+
blueprint() {
|
|
18
|
+
return {
|
|
19
|
+
concerns: [Client, ArgOutfile],
|
|
20
|
+
options: [
|
|
21
|
+
NewOpt("tenantId", {
|
|
22
|
+
descTemplate: "Tenant to query (should start with 'iten'). Defaults to the tenant of the current user",
|
|
23
|
+
type: "string"
|
|
24
|
+
}),
|
|
25
|
+
NewOpt("filter", {
|
|
26
|
+
descTemplate: "Filter expression(s) '<field>:<comparator>:<value>', combined with AND. " +
|
|
27
|
+
"Reserved fields: tag, group, unique_group.id, unique_group.type, lib/library (eq only), " +
|
|
28
|
+
"created_block_time. Any other name is a query field - prefix with 'qf.' to disambiguate. " +
|
|
29
|
+
"Comparators: co (or ::), nc, eq, ne, lt, le, gt, ge. " +
|
|
30
|
+
"e.g. --filter 'tag:eq:elv:media:live_stream' 'qf.date:ge:2026-09-01'",
|
|
31
|
+
string: true,
|
|
32
|
+
type: "array"
|
|
33
|
+
}),
|
|
34
|
+
NewOpt("sortBy", {
|
|
35
|
+
descTemplate: "Query field name to sort by (not a metadata path). Only one may be given. " +
|
|
36
|
+
"Also restricts results to objects that have the field",
|
|
37
|
+
type: "string"
|
|
38
|
+
}),
|
|
39
|
+
NewOpt("sortDescending", {
|
|
40
|
+
descTemplate: "Sort in descending order",
|
|
41
|
+
type: "boolean"
|
|
42
|
+
}),
|
|
43
|
+
NewOpt("include", {
|
|
44
|
+
descTemplate: "Index data to return with each result: tags, groups, uniquegroups, fields, " +
|
|
45
|
+
"type, name, all, none. Defaults to query fields only",
|
|
46
|
+
string: true,
|
|
47
|
+
type: "array"
|
|
48
|
+
}),
|
|
49
|
+
NewOpt("select", {
|
|
50
|
+
descTemplate: "Metadata subtree path(s) to include in each result. If omitted, no metadata is retrieved",
|
|
51
|
+
string: true,
|
|
52
|
+
type: "array"
|
|
53
|
+
}),
|
|
54
|
+
NewOpt("remove", {
|
|
55
|
+
descTemplate: "Metadata subtree path(s) to omit from each result. Only has an effect with --select",
|
|
56
|
+
string: true,
|
|
57
|
+
type: "array"
|
|
58
|
+
}),
|
|
59
|
+
NewOpt("start", {
|
|
60
|
+
descTemplate: "Index of the first object to retrieve",
|
|
61
|
+
type: "number"
|
|
62
|
+
}),
|
|
63
|
+
NewOpt("limit", {
|
|
64
|
+
descTemplate: "Maximum number of objects to return. Fabric default 100, capped at 998",
|
|
65
|
+
type: "number"
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async body() {
|
|
72
|
+
const logger = this.logger;
|
|
73
|
+
const client = await this.concerns.Client.get();
|
|
74
|
+
|
|
75
|
+
const {
|
|
76
|
+
tenantId, filter, sortBy, sortDescending, include, select, remove, start, limit
|
|
77
|
+
} = this.args;
|
|
78
|
+
|
|
79
|
+
const response = await client.TenantContent({
|
|
80
|
+
tenantId,
|
|
81
|
+
filter,
|
|
82
|
+
sortBy,
|
|
83
|
+
sortDescending,
|
|
84
|
+
include,
|
|
85
|
+
select,
|
|
86
|
+
remove,
|
|
87
|
+
start,
|
|
88
|
+
limit
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const versions = response.versions || [];
|
|
92
|
+
|
|
93
|
+
logger.data("versions", versions);
|
|
94
|
+
logger.data("paging", response.paging);
|
|
95
|
+
logger.log(`Found ${versions.length} object(s)`);
|
|
96
|
+
|
|
97
|
+
if(this.args.outfile) {
|
|
98
|
+
this.concerns.ArgOutfile.writeJson({obj: response});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Only show columns that at least one result actually has
|
|
103
|
+
const rows = versions.map(v => {
|
|
104
|
+
const row = {object_id: v.id, hash: v.hash};
|
|
105
|
+
if(v.name !== undefined) row.name = v.name;
|
|
106
|
+
if(v.last_modified_at !== undefined) row.last_modified_at = v.last_modified_at;
|
|
107
|
+
if(v.query_fields !== undefined) row.query_fields = FlattenMap(v.query_fields);
|
|
108
|
+
if(v.tags !== undefined) row.tags = Join(v.tags);
|
|
109
|
+
if(v.groups !== undefined) row.groups = Join(v.groups);
|
|
110
|
+
if(v.unique_groups !== undefined) row.unique_groups = FlattenMap(v.unique_groups);
|
|
111
|
+
if(v.error) row.error = v.error;
|
|
112
|
+
return row;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if(rows.length > 0) {
|
|
116
|
+
logger.logTable({list: rows});
|
|
117
|
+
} else {
|
|
118
|
+
logger.warn("No content matched the query.");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if(response.paging) {
|
|
122
|
+
const {items, limit: pageLimit, current} = response.paging;
|
|
123
|
+
logger.log(`Showing ${current || 0}-${(current || 0) + versions.length} of ${items} (page size ${pageLimit})`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
header() {
|
|
128
|
+
return `Query content for tenant ${this.args.tenantId || "of current user"}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if(require.main === module) {
|
|
133
|
+
Utility.cmdLineInvoke(TenantContent);
|
|
134
|
+
} else {
|
|
135
|
+
module.exports = TenantContent;
|
|
136
|
+
}
|