@eluvio/elv-client-js 4.2.30 → 4.2.32
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/ElvClient-min.js +1 -1
- package/dist/ElvClient-node-min.js +1 -1
- package/dist/ElvFrameClient-min.js +1 -1
- package/dist/ElvPermissionsClient-min.js +1 -1
- package/dist/ElvWalletClient-min.js +1 -1
- package/dist/ElvWalletClient-node-min.js +1 -1
- package/dist/src/ContentObjectVerification.js +281 -0
- package/dist/src/ElvClient.js +275 -355
- package/dist/src/EthClient.js +2 -3
- package/dist/src/FrameClient.js +1 -1
- package/dist/src/client/ABRPublishing.js +60 -71
- package/dist/src/client/ContentAccess.js +204 -298
- package/dist/src/client/Files.js +844 -1400
- package/dist/src/client/LiveConf.js +37 -54
- package/dist/src/client/LiveStream.js +81 -123
- package/package.json +1 -1
- package/src/AuthorizationClient.js +5 -0
- package/src/ElvClient.js +28 -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 +148 -0
- package/testScripts/Test.js +14 -2
- package/utilities/ObjectInfo.js +99 -0
- package/utilities/TenantContent.js +136 -0
|
@@ -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
|
@@ -1286,6 +1286,117 @@ exports.StreamStartRecording = async function({name, start=false}) {
|
|
|
1286
1286
|
return status;
|
|
1287
1287
|
};
|
|
1288
1288
|
|
|
1289
|
+
/**
|
|
1290
|
+
* Stop the current live recording and start a new one with a new edge write token.
|
|
1291
|
+
* The main object is finalized only once.
|
|
1292
|
+
*
|
|
1293
|
+
* @methodGroup Live Stream
|
|
1294
|
+
* @namedParams
|
|
1295
|
+
* @param {string} name - Object ID or name of the live stream object
|
|
1296
|
+
*
|
|
1297
|
+
* @return {Promise<Object>} - The status response for the new recording session
|
|
1298
|
+
*/
|
|
1299
|
+
exports.StreamRestartRecording = async function({name}) {
|
|
1300
|
+
let status = await this.StreamStatus({name});
|
|
1301
|
+
const oldEdgeWriteToken = status?.edgeWriteToken;
|
|
1302
|
+
|
|
1303
|
+
if(!oldEdgeWriteToken) {
|
|
1304
|
+
throw new Error("Unable to restart stream - no active recording session");
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if(["running", "starting", "stalled"].includes(status.state)) {
|
|
1308
|
+
status = await this.StreamStartOrStopOrReset({name, op: "stop"});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const IsStopped = currentStatus =>
|
|
1312
|
+
currentStatus?.state === "stopped" &&
|
|
1313
|
+
!currentStatus.error &&
|
|
1314
|
+
currentStatus.edgeWriteToken === oldEdgeWriteToken;
|
|
1315
|
+
|
|
1316
|
+
if(!IsStopped(status)) {
|
|
1317
|
+
throw new Error(`Unable to restart stream - current LRO did not stop definitively (state: ${status?.state || "unknown"})`);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const {libraryId, objectId, fabricApi} = status;
|
|
1321
|
+
if(!fabricApi) {
|
|
1322
|
+
throw new Error("Unable to restart stream - ingress node API is unavailable");
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
this.SetNodes({fabricURIs: [fabricApi]});
|
|
1326
|
+
|
|
1327
|
+
let mainWriteToken;
|
|
1328
|
+
let newEdgeWriteToken;
|
|
1329
|
+
let mainObjectFinalizeStarted = false;
|
|
1330
|
+
|
|
1331
|
+
try {
|
|
1332
|
+
({writeToken: mainWriteToken} = await this.EditContentObject({
|
|
1333
|
+
libraryId,
|
|
1334
|
+
objectId
|
|
1335
|
+
}));
|
|
1336
|
+
|
|
1337
|
+
({writeToken: newEdgeWriteToken} = await this.EditContentObject({
|
|
1338
|
+
libraryId,
|
|
1339
|
+
objectId
|
|
1340
|
+
}));
|
|
1341
|
+
|
|
1342
|
+
await this.MergeMetadata({
|
|
1343
|
+
libraryId,
|
|
1344
|
+
objectId,
|
|
1345
|
+
writeToken: mainWriteToken,
|
|
1346
|
+
metadata: {
|
|
1347
|
+
live_recording: {
|
|
1348
|
+
status: {
|
|
1349
|
+
edge_write_token: newEdgeWriteToken,
|
|
1350
|
+
state: "active"
|
|
1351
|
+
},
|
|
1352
|
+
fabric_config: {
|
|
1353
|
+
edge_write_token: newEdgeWriteToken
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
// Verify the old LRO is definitively stopped before publishing and deleting the old token.
|
|
1360
|
+
status = await this.StreamStatus({name});
|
|
1361
|
+
if(!IsStopped(status)) {
|
|
1362
|
+
throw new Error(`Unable to restart stream - current LRO stop could not be confirmed (state: ${status?.state || "unknown"})`);
|
|
1363
|
+
}
|
|
1364
|
+
mainObjectFinalizeStarted = true;
|
|
1365
|
+
const finalizeResponse = await this.FinalizeContentObject({
|
|
1366
|
+
libraryId,
|
|
1367
|
+
objectId,
|
|
1368
|
+
writeToken: mainWriteToken,
|
|
1369
|
+
commitMessage: `Restart stream with edge write token ${newEdgeWriteToken}`
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
try {
|
|
1373
|
+
await this.DeleteWriteToken({writeToken: oldEdgeWriteToken});
|
|
1374
|
+
} catch(error) {
|
|
1375
|
+
this.Log(`Unable to delete old edge write token ${oldEdgeWriteToken}: ${error.message}`, true);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const restartedStatus = await this.StreamStartOrStopOrReset({name, op: "start"});
|
|
1379
|
+
return {
|
|
1380
|
+
...restartedStatus,
|
|
1381
|
+
hash: finalizeResponse.hash
|
|
1382
|
+
};
|
|
1383
|
+
} catch(error) {
|
|
1384
|
+
// Before main-object finalization begins, both new drafts are safe to discard and the old
|
|
1385
|
+
// stopped recording remains referenced by the published object.
|
|
1386
|
+
if(!mainObjectFinalizeStarted) {
|
|
1387
|
+
for(const writeToken of [newEdgeWriteToken, mainWriteToken].filter(Boolean)) {
|
|
1388
|
+
try {
|
|
1389
|
+
await this.DeleteWriteToken({writeToken});
|
|
1390
|
+
} catch(cleanupError) {
|
|
1391
|
+
this.Log(`Unable to clean up restart write token ${writeToken}: ${cleanupError.message}`, true);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
throw error;
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1289
1400
|
/**
|
|
1290
1401
|
* Start, stop or reset a stream within the current session (current edge write token)
|
|
1291
1402
|
*
|
|
@@ -4215,6 +4326,43 @@ exports.OutputsReset = async function({libraryId, objectId, outputId}) {
|
|
|
4215
4326
|
}
|
|
4216
4327
|
};
|
|
4217
4328
|
|
|
4329
|
+
/**
|
|
4330
|
+
* Manually switch a live output's active input hop (e.g. primary <-> failover).
|
|
4331
|
+
*
|
|
4332
|
+
* @methodGroup Live Stream
|
|
4333
|
+
* @namedParams
|
|
4334
|
+
* @param {string=} libraryId - Library ID of the output settings object. If not provided, it will be retrieved automatically.
|
|
4335
|
+
* @param {string} objectId - Object ID of the output settings object
|
|
4336
|
+
* @param {string} outputId - ID of the output to switch
|
|
4337
|
+
* @param {number=} hop=0 - Target hop index (0 = primary input, 1 = first failover input)
|
|
4338
|
+
*
|
|
4339
|
+
* @returns {Promise<Object>} - Response from the hop call
|
|
4340
|
+
*/
|
|
4341
|
+
exports.OutputsHop = async function({libraryId, objectId, outputId, hop=0}) {
|
|
4342
|
+
ValidateObject(objectId);
|
|
4343
|
+
ValidatePresence("outputId", outputId);
|
|
4344
|
+
|
|
4345
|
+
if(!libraryId) {
|
|
4346
|
+
libraryId = await this.ContentObjectLibraryId({objectId});
|
|
4347
|
+
}
|
|
4348
|
+
|
|
4349
|
+
// Route to a live egress node, then to the specific output's node
|
|
4350
|
+
const {restore} = await RouteToLiveEgress({client: this});
|
|
4351
|
+
await RouteToOutputNode({client: this, libraryId, objectId, outputId});
|
|
4352
|
+
|
|
4353
|
+
try {
|
|
4354
|
+
return await this.CallBitcodeMethod({
|
|
4355
|
+
libraryId,
|
|
4356
|
+
objectId,
|
|
4357
|
+
method: UrlJoin("live", "outputs", outputId, "ctrl", "hop"),
|
|
4358
|
+
queryParams: {hop},
|
|
4359
|
+
constant: false
|
|
4360
|
+
});
|
|
4361
|
+
} finally {
|
|
4362
|
+
restore();
|
|
4363
|
+
}
|
|
4364
|
+
};
|
|
4365
|
+
|
|
4218
4366
|
/**
|
|
4219
4367
|
* Delete a live output.
|
|
4220
4368
|
*
|
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
|
+
}
|