dataverse-utils 5.0.2 → 5.0.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/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # dataverse-utils
2
+ | NPM |
3
+ | --- |
4
+ | [![npm](https://img.shields.io/npm/v/dataverse-utils.svg?style=flat-square)](https://www.npmjs.com/package/dataverse-utils) |
5
+
6
+ Utilities for interacting with Dataverse environments
7
+
8
+ # Deploy
9
+
10
+ Deployment configuration is stored in dataverse.config.json. Access token will be acquired via device-code flow.
11
+
12
+ ```sh
13
+ dataverse-utils deploy webresource
14
+
15
+ dataverse-utils deploy assembly
16
+ ```
17
+
18
+ # Generate Early-Bound TS Files
19
+
20
+ Generate early bound TypeScript files for tables. Access token will be acquired via device-code flow.
21
+
22
+ ```sh
23
+ dataverse-utils generate account
24
+ ```
@@ -0,0 +1,50 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { deployAssembly } from './models/pluginAssembly.js';
4
+ import { deployPluginPackage } from './models/pluginPackage.js';
5
+ import { deployApi } from './models/customApi.js';
6
+ import { logger } from './logger.js';
7
+ export async function assemblyDeploy(creds, apiConfig) {
8
+ const currentPath = '.';
9
+ const configFile = await fs.promises.readFile(path.resolve(currentPath, 'dataverse.config.json'), 'utf8');
10
+ if (configFile == null) {
11
+ logger.warn('unable to find dataverse.config.json file');
12
+ return;
13
+ }
14
+ const config = JSON.parse(configFile);
15
+ if (config.assembly) {
16
+ logger.info('deploy plugin package');
17
+ try {
18
+ await deployPluginPackage(config, apiConfig, creds.solution);
19
+ }
20
+ catch (error) {
21
+ logger.error(error.message);
22
+ return;
23
+ }
24
+ logger.done(`deployed plugin package ${config.prefix}_${config.name}\r\n`);
25
+ }
26
+ else {
27
+ logger.info(`deploy assembly to ${creds.server}`);
28
+ try {
29
+ await deployAssembly(config, apiConfig, creds.solution);
30
+ }
31
+ catch (error) {
32
+ logger.error(error.message);
33
+ return;
34
+ }
35
+ logger.done(`deployed assembly ${config.name}`);
36
+ }
37
+ if (config.customapis?.length > 0 && (config.pluginassemblyid || config.assembly?.pluginassemblyid)) {
38
+ logger.info('deploy custom api');
39
+ const assemblyId = config.pluginassemblyid ?? config.assembly.pluginassemblyid;
40
+ try {
41
+ const promises = config.customapis.map((a) => deployApi(a, assemblyId, apiConfig, creds.solution));
42
+ await Promise.all(promises);
43
+ }
44
+ catch (error) {
45
+ logger.error(error.message);
46
+ }
47
+ logger.done('deployed custom api');
48
+ }
49
+ await fs.promises.writeFile(path.resolve(currentPath, 'dataverse.config.json'), JSON.stringify(config, null, 4), 'utf8');
50
+ }
package/lib/auth.js ADDED
@@ -0,0 +1,67 @@
1
+ import prompts from 'prompts';
2
+ import { cacheExists, cachePlugin, deleteCache } from './cachePlugin.js';
3
+ import { PublicClientApplication } from '@azure/msal-node';
4
+ import { logger } from './logger.js';
5
+ const clientId = '51f81489-12ee-4a9e-aaae-a2591f45987d';
6
+ export const onTokenFailure = async (url, error) => {
7
+ if (error) {
8
+ logger.error(`failed to acquire access token: ${error}`);
9
+ }
10
+ else {
11
+ logger.error('failed to acquire access token');
12
+ }
13
+ if (cacheExists(url)) {
14
+ const { deleteToken } = await prompts({
15
+ type: 'confirm',
16
+ name: 'deleteToken',
17
+ message: `delete current token cache for ${url}?`
18
+ });
19
+ if (deleteToken) {
20
+ deleteCache(url);
21
+ }
22
+ }
23
+ };
24
+ export const getAccessToken = async (authEndpoint, url) => {
25
+ const config = {
26
+ auth: {
27
+ clientId: clientId,
28
+ authority: authEndpoint
29
+ },
30
+ cache: {
31
+ cachePlugin: cachePlugin(url)
32
+ }
33
+ };
34
+ const pca = new PublicClientApplication(config);
35
+ const cache = pca.getTokenCache();
36
+ const accounts = await cache?.getAllAccounts().catch(ex => {
37
+ throw new Error(ex.message);
38
+ });
39
+ // Try to get token silently
40
+ if (accounts.length > 0) {
41
+ try {
42
+ const silentToken = await pca.acquireTokenSilent({
43
+ account: accounts[0],
44
+ scopes: [`${url}/.default`]
45
+ });
46
+ if (silentToken) {
47
+ return silentToken;
48
+ }
49
+ }
50
+ catch (ex) {
51
+ if (ex.message.indexOf('The refresh token has expired due to inactivity') === -1) {
52
+ throw new Error(ex.message);
53
+ }
54
+ }
55
+ }
56
+ // Acquire token by device code
57
+ try {
58
+ const token = await pca.acquireTokenByDeviceCode({
59
+ scopes: [`${url}/.default`],
60
+ deviceCodeCallback: (response) => logger.info(response.message)
61
+ });
62
+ return token;
63
+ }
64
+ catch (ex) {
65
+ throw new Error(ex.message);
66
+ }
67
+ };
@@ -0,0 +1,97 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import Cryptr from 'cryptr';
5
+ const encrypt = (text) => {
6
+ const user = os.userInfo().username;
7
+ const cryptr = new Cryptr(user);
8
+ const encrypted = cryptr.encrypt(text);
9
+ return encrypted;
10
+ };
11
+ const decrypt = (text) => {
12
+ const user = os.userInfo().username;
13
+ const cryptr = new Cryptr(user);
14
+ const decrypted = cryptr.decrypt(text);
15
+ return decrypted;
16
+ };
17
+ export const getCachePath = (url) => {
18
+ const org = new URL(url).hostname.split('.')[0];
19
+ if (!fs.existsSync(path.join(os.homedir(), './.dataverse-utils/'))) {
20
+ fs.mkdirSync(path.join(os.homedir(), './.dataverse-utils/'));
21
+ }
22
+ return path.join(os.homedir(), `./.dataverse-utils/${org}.json`);
23
+ };
24
+ export const cacheExists = (url) => {
25
+ const cacheLocation = getCachePath(url);
26
+ return fs.existsSync(cacheLocation);
27
+ };
28
+ export const deleteCache = (url) => {
29
+ const cacheLocation = getCachePath(url);
30
+ if (fs.existsSync(cacheLocation)) {
31
+ fs.unlinkSync(cacheLocation);
32
+ return true;
33
+ }
34
+ else {
35
+ return false;
36
+ }
37
+ };
38
+ export const cachePlugin = (url) => {
39
+ const cacheLocation = getCachePath(url);
40
+ const beforeCacheAccess = (tokenCacheContext) => {
41
+ return new Promise((resolve, reject) => {
42
+ if (fs.existsSync(cacheLocation)) {
43
+ fs.readFile(cacheLocation, 'utf-8', (err, data) => {
44
+ if (err) {
45
+ reject();
46
+ }
47
+ else {
48
+ try {
49
+ const decrypted = decrypt(data);
50
+ tokenCacheContext.tokenCache.deserialize(decrypted);
51
+ resolve();
52
+ }
53
+ catch (ex) {
54
+ reject(ex);
55
+ }
56
+ }
57
+ });
58
+ }
59
+ else {
60
+ try {
61
+ const encrypted = encrypt(tokenCacheContext.tokenCache.serialize());
62
+ fs.writeFile(cacheLocation, encrypted, (err) => {
63
+ if (err) {
64
+ reject();
65
+ }
66
+ else {
67
+ resolve();
68
+ }
69
+ });
70
+ }
71
+ catch (ex) {
72
+ reject(ex);
73
+ }
74
+ }
75
+ });
76
+ };
77
+ const afterCacheAccess = (tokenCacheContext) => {
78
+ return new Promise((resolve, reject) => {
79
+ if (tokenCacheContext.cacheHasChanged) {
80
+ const encrypted = encrypt(tokenCacheContext.tokenCache.serialize());
81
+ fs.writeFile(cacheLocation, encrypted, (err) => {
82
+ if (err) {
83
+ reject(err);
84
+ }
85
+ resolve();
86
+ });
87
+ }
88
+ else {
89
+ resolve();
90
+ }
91
+ });
92
+ };
93
+ return {
94
+ beforeCacheAccess,
95
+ afterCacheAccess
96
+ };
97
+ };
@@ -0,0 +1,65 @@
1
+ import { retrieveMultiple, unboundAction } from 'dataverse-webapi/lib/node';
2
+ import { logger } from './logger.js';
3
+ export var ComponentType;
4
+ (function (ComponentType) {
5
+ ComponentType[ComponentType["WebResource"] = 61] = "WebResource";
6
+ ComponentType[ComponentType["PluginType"] = 90] = "PluginType";
7
+ ComponentType[ComponentType["PluginAssembly"] = 91] = "PluginAssembly";
8
+ ComponentType[ComponentType["SDKMessageProcessingStep"] = 92] = "SDKMessageProcessingStep";
9
+ ComponentType[ComponentType["SDKMessageProcessingStepImage"] = 93] = "SDKMessageProcessingStepImage";
10
+ ComponentType[ComponentType["PluginPackage"] = 10865] = "PluginPackage";
11
+ })(ComponentType || (ComponentType = {}));
12
+ export async function addToSolution(id, solution, type, apiConfig) {
13
+ const data = {
14
+ ComponentId: id,
15
+ ComponentType: type,
16
+ SolutionUniqueName: solution,
17
+ AddRequiredComponents: false,
18
+ IncludedComponentSettingsValues: null
19
+ };
20
+ logger.info(`add component ${id} to solution ${solution}`);
21
+ await unboundAction(apiConfig, 'AddSolutionComponent', data);
22
+ }
23
+ export async function publish(publishXml, apiConfig) {
24
+ const data = {
25
+ ParameterXml: `<importexportxml><webresources>${publishXml}</webresources></importexportxml>`
26
+ };
27
+ await unboundAction(apiConfig, 'PublishXml', data);
28
+ }
29
+ export async function getTableMetadata(table, apiConfig) {
30
+ const options = [
31
+ '?$select=DisplayName,LogicalName,EntitySetName,SchemaName',
32
+ '&$expand=Attributes($select=LogicalName,SchemaName)'
33
+ ].join('');
34
+ const metadata = await retrieveMultiple(apiConfig, `EntityDefinitions(LogicalName='${table}')`, options);
35
+ if (metadata == null) {
36
+ throw Error(`Table ${table} not found in metadata cache`);
37
+ }
38
+ const choiceOptions = [
39
+ '?$select=attributevalue,value,attributename',
40
+ `&$filter=objecttypecode eq '${table}'`
41
+ ].join('');
42
+ const choiceMetadata = await retrieveMultiple(apiConfig, 'stringmaps', choiceOptions);
43
+ const tableMetadata = {
44
+ logicalName: metadata.LogicalName,
45
+ schemaName: metadata.SchemaName,
46
+ entitySetName: metadata.EntitySetName,
47
+ choices: [],
48
+ fields: metadata.Attributes.map((a) => {
49
+ return {
50
+ logicalName: a.LogicalName,
51
+ schemaName: a.SchemaName
52
+ };
53
+ })
54
+ };
55
+ choiceMetadata.value.forEach((c) => {
56
+ const index = tableMetadata.choices.findIndex(x => x.column === c.attributename);
57
+ if (index === -1) {
58
+ tableMetadata.choices.push({ column: c.attributename, options: [{ text: c.value, value: c.attributevalue }] });
59
+ }
60
+ else {
61
+ tableMetadata.choices[index].options.push({ text: c.value, value: c.attributevalue });
62
+ }
63
+ });
64
+ return tableMetadata;
65
+ }
package/lib/deploy.js ADDED
@@ -0,0 +1,68 @@
1
+ import prompts from 'prompts';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { logger } from './logger.js';
5
+ import { assemblyDeploy } from './assemblyDeploy.js';
6
+ import { webResourceDeploy } from './webResourceDeploy.js';
7
+ import { WebApiConfig } from 'dataverse-webapi/lib/node';
8
+ import { getAccessToken, onTokenFailure } from './auth.js';
9
+ export default async function deploy(type, files) {
10
+ if (!type || (type !== 'webresource' && type !== 'assembly' && type !== 'pcf')) {
11
+ const invalid = type !== undefined && type !== 'webresource' && type !== 'assembly' && type !== 'pcf';
12
+ const invalidMessage = invalid ? `${type} is not a valid project type. ` : '';
13
+ const { typePrompt } = await prompts({
14
+ type: 'select',
15
+ name: 'typePrompt',
16
+ message: `${invalidMessage}select project type to deploy`,
17
+ choices: [
18
+ { title: 'web resource', value: 'webresource' },
19
+ { title: 'pcf', value: 'webresource' },
20
+ { title: 'plugin or workflow activity', value: 'assembly' }
21
+ ]
22
+ });
23
+ type = typePrompt;
24
+ }
25
+ const currentPath = '.';
26
+ const credsFile = await fs.promises.readFile(path.resolve(currentPath, 'dataverse.config.json'), 'utf8');
27
+ if (credsFile == null) {
28
+ logger.warn('unable to find dataverse.config.json file');
29
+ return;
30
+ }
31
+ const creds = JSON.parse(credsFile).connection;
32
+ if (!creds.authEndpoint) {
33
+ logger.error(('authEndpoint not found in dataverse.config.json. if you recently updated the package, please update the config file and replace tenant with authEndpoint with full authorization URL.'));
34
+ return;
35
+ }
36
+ let token = null;
37
+ // Check if token is available in environment
38
+ if (process.env.ACCESSTOKEN) {
39
+ token = { accessToken: process.env.ACCESSTOKEN };
40
+ }
41
+ else {
42
+ try {
43
+ token = await getAccessToken(creds.authEndpoint, creds.server);
44
+ }
45
+ catch (error) {
46
+ onTokenFailure(creds.server, error.message);
47
+ return;
48
+ }
49
+ if (token == null || token.accessToken == null) {
50
+ onTokenFailure(creds.server);
51
+ return;
52
+ }
53
+ }
54
+ const apiConfig = new WebApiConfig('8.2', token.accessToken, creds.server);
55
+ switch (type) {
56
+ case 'webresource':
57
+ await webResourceDeploy(creds, apiConfig, files);
58
+ break;
59
+ case 'assembly':
60
+ await assemblyDeploy(creds, apiConfig);
61
+ break;
62
+ case 'pcf':
63
+ logger.error('PCF deploy coming soon');
64
+ break;
65
+ default:
66
+ break;
67
+ }
68
+ }
@@ -0,0 +1,97 @@
1
+ import prompts from 'prompts';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { logger } from './logger.js';
5
+ import { getTableMetadata } from './dataverse.service.js';
6
+ import { WebApiConfig } from 'dataverse-webapi/lib/node';
7
+ import { getAccessToken, onTokenFailure } from './auth.js';
8
+ export default async function generate(table) {
9
+ while (!table) {
10
+ const { tablePrompt } = await prompts({
11
+ type: 'text',
12
+ name: 'tablePrompt',
13
+ message: `enter table to generate`
14
+ });
15
+ table = tablePrompt;
16
+ }
17
+ const currentPath = '.';
18
+ const credsFile = await fs.promises.readFile(path.resolve(currentPath, 'dataverse.config.json'), 'utf8');
19
+ if (credsFile == null) {
20
+ logger.warn('unable to find dataverse.config.json file');
21
+ return;
22
+ }
23
+ const creds = JSON.parse(credsFile).connection;
24
+ if (!creds.authEndpoint) {
25
+ logger.error(('authEndpoint not found in dataverse.config.json. if you recently updated the package, please update the config file and replace tenant with authEndpoint with full authorization URL.'));
26
+ return;
27
+ }
28
+ let token = null;
29
+ try {
30
+ token = await getAccessToken(creds.authEndpoint, creds.server);
31
+ }
32
+ catch (error) {
33
+ onTokenFailure(creds.server, error.message);
34
+ return;
35
+ }
36
+ if (token == null || token.accessToken == null) {
37
+ onTokenFailure(creds.server);
38
+ return;
39
+ }
40
+ const apiConfig = new WebApiConfig('8.2', token.accessToken, creds.server);
41
+ let metadata = {};
42
+ logger.info('Retrieve table metadata');
43
+ try {
44
+ metadata = await getTableMetadata(table, apiConfig);
45
+ }
46
+ catch (error) {
47
+ logger.error(error.message);
48
+ return;
49
+ }
50
+ // Build code file from metadata
51
+ const codeFile = [
52
+ `class ${metadata.schemaName} {`,
53
+ '\r\n',
54
+ ` LogicalName = '${metadata.logicalName}';`,
55
+ '\r\n',
56
+ ` SchemaName = '${metadata.schemaName}';`,
57
+ '\r\n',
58
+ ` EntitySetName = '${metadata.entitySetName}';`,
59
+ '\r\n',
60
+ '\r\n',
61
+ ' Fields = {',
62
+ '\r\n',
63
+ metadata.fields.map(f => {
64
+ return ` '${f.schemaName}': '${f.logicalName}'`;
65
+ }).join(',\r\n'),
66
+ '\r\n',
67
+ ' }',
68
+ '\r\n',
69
+ '\r\n',
70
+ metadata.choices.map(c => {
71
+ const field = metadata.fields.find(f => f.logicalName === c.column);
72
+ return ` ${field?.schemaName ?? c.column} = ${metadata.schemaName}_${field?.schemaName ?? c.column};`;
73
+ }).join('\r\n'),
74
+ '\r\n',
75
+ '}',
76
+ '\r\n',
77
+ '\r\n',
78
+ metadata.choices.map(c => {
79
+ const field = metadata.fields.find(f => f.logicalName === c.column);
80
+ return [
81
+ `export enum ${metadata.schemaName}_${field?.schemaName ?? c.column} {`,
82
+ '\r\n',
83
+ c.options.map(x => ` '${x.text.replace(`'`, `\\'`)}' = ${x.value}`).join(',\r\n'),
84
+ '\r\n',
85
+ '}'
86
+ ].join('');
87
+ }).join('\r\n\r\n'),
88
+ '\r\n',
89
+ '\r\n',
90
+ `export default new ${metadata.schemaName}();`
91
+ ].join('');
92
+ if (!fs.existsSync(path.resolve(currentPath, 'src', 'scripts', 'models'))) {
93
+ fs.mkdirSync(path.resolve(currentPath, 'src', 'scripts', 'models'));
94
+ }
95
+ fs.writeFileSync(path.resolve(currentPath, 'src', 'scripts', 'models', `${metadata.schemaName}.ts`), codeFile);
96
+ logger.done(`Table metadata output to models/${metadata.schemaName}.ts`);
97
+ }
package/lib/index.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import deploy from './deploy.js';
4
+ import generate from './generate.js';
5
+ program
6
+ .usage('<command> [options]');
7
+ // Deploy command
8
+ program
9
+ .command('deploy')
10
+ .description('Deploy file(s) to dataverse (webresource, assembly, pcf)')
11
+ .argument('[type]', 'Type of project to deploy')
12
+ .argument('[files]', 'Comma separate list of files to deploy')
13
+ .action((type, files) => {
14
+ deploy(type, files);
15
+ });
16
+ // Generate command
17
+ program
18
+ .command('generate')
19
+ .description('Generate early-bound TypeScript file for specified table')
20
+ .argument('[table]', 'Table to generate')
21
+ .action((table) => {
22
+ generate(table);
23
+ });
24
+ // Show help on unknown command
25
+ program
26
+ .arguments('<command>')
27
+ .action((cmd) => {
28
+ program.outputHelp();
29
+ console.log();
30
+ console.log(`Unknown command ${cmd}.`);
31
+ console.log();
32
+ });
33
+ program.parse(process.argv);
34
+ if (!process.argv.slice(2).length) {
35
+ program.outputHelp();
36
+ }
package/lib/logger.js ADDED
@@ -0,0 +1,31 @@
1
+ import kleur from 'kleur';
2
+ import figures from 'figures';
3
+ const isTest = process.env.JEST_WORKER_ID !== undefined;
4
+ export const icons = {
5
+ done: kleur.green(figures.tick),
6
+ info: kleur.cyan(figures.pointer),
7
+ error: kleur.red(figures.cross),
8
+ warn: kleur.yellow(figures.warning)
9
+ };
10
+ export const logger = {
11
+ info(...args) {
12
+ if (!isTest) {
13
+ console.info(icons.info, ...args);
14
+ }
15
+ },
16
+ warn(...args) {
17
+ if (!isTest) {
18
+ console.warn(icons.warn, ...args);
19
+ }
20
+ },
21
+ error(...args) {
22
+ if (!isTest) {
23
+ console.error(icons.error, ...args);
24
+ }
25
+ },
26
+ done(...args) {
27
+ if (!isTest) {
28
+ console.info(icons.done, ...args);
29
+ }
30
+ },
31
+ };
@@ -0,0 +1,72 @@
1
+ import { retrieveMultiple, createWithReturnData, update } from 'dataverse-webapi/lib/node';
2
+ import { retrieveType } from './pluginType.js';
3
+ import { logger } from '../logger.js';
4
+ export async function deployApi(config, assemblyId, apiConfig, solution) {
5
+ const api = structuredClone(config);
6
+ if (!config.customapiid) {
7
+ config.customapiid = await retrieveApi(api.name, apiConfig);
8
+ }
9
+ if (config.plugintype && !config.plugintypeid) {
10
+ config.plugintypeid = await retrieveType(config.plugintype, assemblyId, apiConfig);
11
+ if (config.plugintypeid === '') {
12
+ logger.error(`unable to find plugin type ${api.plugintype}`);
13
+ return;
14
+ }
15
+ }
16
+ delete api.plugintype;
17
+ delete api.customapiid;
18
+ delete api.plugintypeid;
19
+ api['PluginTypeId@odata.bind'] = `plugintypes(${config.plugintypeid})`;
20
+ if (config.customapiid !== '') {
21
+ try {
22
+ await updateApi(config.customapiid, api, apiConfig);
23
+ }
24
+ catch (error) {
25
+ throw new Error(`failed to update custom api: ${error.message}`);
26
+ }
27
+ }
28
+ else {
29
+ try {
30
+ config.customapiid = await createApi(api, apiConfig, solution);
31
+ }
32
+ catch (error) {
33
+ throw new Error(`failed to create custom api: ${error.message}`);
34
+ }
35
+ }
36
+ }
37
+ export async function retrieveApi(name, apiConfig) {
38
+ const options = `$select=customapiid&$filter=name eq '${name}'`;
39
+ const result = await retrieveMultiple(apiConfig, 'customapis', options);
40
+ return result.value.length > 0 ? result.value[0].customapiid : '';
41
+ }
42
+ async function createApi(api, apiConfig, solution) {
43
+ logger.info(`create custom api ${api.name}`);
44
+ const options = {};
45
+ if (solution) {
46
+ options.customHeaders = { 'MSCRM.SolutionUniqueName': solution };
47
+ }
48
+ const result = await createWithReturnData(apiConfig, 'customapis', api, '$select=customapiid', options);
49
+ if (result?.error) {
50
+ throw new Error(result.error.message);
51
+ }
52
+ return result.customapiid;
53
+ }
54
+ async function updateApi(id, api, apiConfig) {
55
+ logger.info(`update custom api ${api.name}`);
56
+ const record = {
57
+ displayname: api.displayname,
58
+ description: api.description,
59
+ name: api.name,
60
+ executeprivilegename: api.executeprivilegename
61
+ };
62
+ if (api['PluginTypeId@odata.bind']) {
63
+ record['PluginTypeId@odata.bind'] = api['PluginTypeId@odata.bind'];
64
+ }
65
+ else {
66
+ record['PluginTypeId@odata.bind'] = null;
67
+ }
68
+ const result = await update(apiConfig, 'customapis', id, record);
69
+ if (result?.error) {
70
+ throw new Error(result.error.message);
71
+ }
72
+ }
@@ -0,0 +1,80 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import fs from 'fs';
3
+ import { glob } from 'glob';
4
+ import { retrieveMultiple, createWithReturnData, update } from 'dataverse-webapi/lib/node';
5
+ import { deployType } from './pluginType.js';
6
+ import { logger } from '../logger.js';
7
+ export async function deployAssembly(config, apiConfig, solution) {
8
+ const files = await glob(`bin/Debug/**/${config.name}.dll`);
9
+ if (files.length === 0) {
10
+ logger.warn(`assembly ${config.name}.dll not found`);
11
+ return;
12
+ }
13
+ const content = (await fs.promises.readFile(files[0])).toString('base64');
14
+ if (!config.pluginassemblyid) {
15
+ config.pluginassemblyid = await retrieveAssembly(config.name, apiConfig);
16
+ }
17
+ if (config.pluginassemblyid !== '') {
18
+ try {
19
+ await updateAssembly(config, content, apiConfig);
20
+ }
21
+ catch (error) {
22
+ throw new Error(`failed to update assembly: ${error.message}`);
23
+ }
24
+ }
25
+ else {
26
+ try {
27
+ config.pluginassemblyid = await createAssembly(config, content, apiConfig, solution);
28
+ }
29
+ catch (error) {
30
+ throw new Error(`failed to create assembly: ${error.message}`);
31
+ }
32
+ }
33
+ if (config.types != null) {
34
+ try {
35
+ const promises = config.types.map((type) => deployType(type, config.pluginassemblyid, apiConfig, solution));
36
+ await Promise.all(promises);
37
+ }
38
+ catch (error) {
39
+ logger.error(error.message);
40
+ return;
41
+ }
42
+ }
43
+ }
44
+ export async function retrieveAssembly(name, apiConfig) {
45
+ const options = `$select=pluginassemblyid&$filter=name eq '${name}'`;
46
+ const result = await retrieveMultiple(apiConfig, 'pluginassemblies', options);
47
+ return result.value.length > 0 ? result.value[0].pluginassemblyid : '';
48
+ }
49
+ async function createAssembly(config, content, apiConfig, solution) {
50
+ logger.info(`create assembly ${config.name}`);
51
+ const assembly = {
52
+ name: config.name,
53
+ content: content,
54
+ isolationmode: config.isolationmode,
55
+ version: config.version,
56
+ publickeytoken: config.name,
57
+ sourcetype: 0,
58
+ culture: ''
59
+ };
60
+ const options = {};
61
+ if (solution) {
62
+ options.customHeaders = { 'MSCRM.SolutionUniqueName': solution };
63
+ }
64
+ const result = await createWithReturnData(apiConfig, 'pluginassemblies', assembly, '$select=pluginassemblyid', options);
65
+ if (result?.error) {
66
+ throw new Error(result.error.message);
67
+ }
68
+ return result.pluginassemblyid;
69
+ }
70
+ async function updateAssembly(config, content, apiConfig) {
71
+ logger.info(`update assembly ${config.name}`);
72
+ const assembly = {
73
+ content: content,
74
+ version: config.version
75
+ };
76
+ const result = await update(apiConfig, 'pluginassemblies', config.pluginassemblyid, assembly);
77
+ if (result?.error) {
78
+ throw new Error(result.error.message);
79
+ }
80
+ }
@@ -0,0 +1,64 @@
1
+ import { logger } from '../logger.js';
2
+ import { retrieveMultiple, createWithReturnData, update } from 'dataverse-webapi/lib/node';
3
+ export async function deployImage(stepId, stepName, config, message, apiConfig) {
4
+ const image = structuredClone(config);
5
+ image['sdkmessageprocessingstepid@odata.bind'] = `/sdkmessageprocessingsteps(${stepId})`;
6
+ switch (message) {
7
+ case 'Create':
8
+ image.messagepropertyname = 'Id';
9
+ break;
10
+ case 'SetState':
11
+ case 'SetStateDynamicEntity':
12
+ image.messagepropertyname = 'EntityMoniker';
13
+ break;
14
+ case 'Send':
15
+ case 'DeliverIncoming':
16
+ case 'DeliverPromote':
17
+ image.messagepropertyname = 'EmailId';
18
+ break;
19
+ default:
20
+ image.messagepropertyname = 'Target';
21
+ break;
22
+ }
23
+ if (!config.sdkmessageprocessingstepimageid) {
24
+ config.sdkmessageprocessingstepimageid = await retrieveImage(stepId, image, apiConfig);
25
+ }
26
+ if (config.sdkmessageprocessingstepimageid !== '') {
27
+ try {
28
+ await updateImage(config.sdkmessageprocessingstepimageid, image, stepName, apiConfig);
29
+ }
30
+ catch (error) {
31
+ throw new Error(`failed to update plugin image: ${error.message}`);
32
+ }
33
+ }
34
+ else {
35
+ try {
36
+ config.sdkmessageprocessingstepimageid = await createImage(image, stepName, apiConfig);
37
+ }
38
+ catch (error) {
39
+ throw new Error(`failed to create plugin image: ${error.message}`);
40
+ }
41
+ }
42
+ }
43
+ async function retrieveImage(stepId, image, apiConfig) {
44
+ const options = `$select=sdkmessageprocessingstepimageid&$filter=name eq '${image.name}' and _sdkmessageprocessingstepid_value eq ${stepId}`;
45
+ const result = await retrieveMultiple(apiConfig, 'sdkmessageprocessingstepimages', options);
46
+ return result.value.length > 0 ? result.value[0].sdkmessageprocessingstepimageid : '';
47
+ }
48
+ async function createImage(image, stepName, apiConfig) {
49
+ logger.info(`create plugin image ${image.name} for step ${stepName}`);
50
+ const result = await createWithReturnData(apiConfig, 'sdkmessageprocessingstepimages', image, '$select=sdkmessageprocessingstepimageid');
51
+ if (result?.error) {
52
+ throw new Error(result.error.message);
53
+ }
54
+ return result.sdkmessageprocessingstepimageid;
55
+ }
56
+ async function updateImage(id, image, stepName, apiConfig) {
57
+ logger.info(`update plugin image ${image.name} for step ${stepName}`);
58
+ const entity = { ...image };
59
+ delete entity.sdkmessageprocessingstepimageid;
60
+ const result = await update(apiConfig, 'sdkmessageprocessingstepimages', id, entity);
61
+ if (result?.error) {
62
+ throw new Error(result.error.message);
63
+ }
64
+ }
@@ -0,0 +1,86 @@
1
+ import fs from 'fs';
2
+ import { glob } from 'glob';
3
+ import { createWithReturnData, retrieveMultiple, update } from 'dataverse-webapi/lib/node';
4
+ import { retrieveAssembly } from './pluginAssembly.js';
5
+ import { logger } from '../logger.js';
6
+ import { retrieveType } from './pluginType.js';
7
+ import { deployStep } from './pluginStep.js';
8
+ export async function deployPluginPackage(config, apiConfig, solution) {
9
+ const files = await glob(`**/${config.name}.*.nupkg`);
10
+ if (files.length === 0) {
11
+ logger.warn(`package ${config.name}.nupkg not found`);
12
+ return;
13
+ }
14
+ const content = (await fs.promises.readFile(files[0])).toString('base64');
15
+ if (!config.pluginpackageid) {
16
+ config.pluginpackageid = await retrievePackage(config.prefix, config.name, apiConfig);
17
+ }
18
+ if (config.pluginpackageid) {
19
+ try {
20
+ await updatePackage(config, content, apiConfig);
21
+ }
22
+ catch (error) {
23
+ throw new Error(`failed to update package: ${error.message}`);
24
+ }
25
+ }
26
+ else {
27
+ try {
28
+ config.pluginpackageid = await createPackage(config, content, apiConfig, solution);
29
+ }
30
+ catch (error) {
31
+ throw new Error(`failed to create package: ${error.message}`);
32
+ }
33
+ }
34
+ if (config.assembly != null) {
35
+ try {
36
+ if (!config.assembly.pluginassemblyid) {
37
+ config.assembly.pluginassemblyid = await retrieveAssembly(config.assembly.name, apiConfig);
38
+ }
39
+ const promises = config.assembly.types?.map(async (t) => {
40
+ if (!t.plugintypeid && config.assembly?.pluginassemblyid) {
41
+ t.plugintypeid = await retrieveType(t.typename, config.assembly.pluginassemblyid, apiConfig);
42
+ }
43
+ const stepPromises = t.steps?.map((s) => deployStep(s, t.plugintypeid, apiConfig, solution)) ?? [];
44
+ await Promise.all(stepPromises);
45
+ }) ?? [];
46
+ await Promise.all(promises);
47
+ }
48
+ catch (error) {
49
+ logger.error(error.message);
50
+ return;
51
+ }
52
+ }
53
+ }
54
+ async function retrievePackage(prefix, name, apiConfig) {
55
+ const options = `$select=pluginpackageid&$filter=contains(name, '${prefix}_${name}')`;
56
+ const result = await retrieveMultiple(apiConfig, 'pluginpackages', options);
57
+ return result.value.length > 0 ? result.value[0].pluginpackageid : '';
58
+ }
59
+ async function createPackage(config, content, apiConfig, solution) {
60
+ logger.info(`create package ${config.name}`);
61
+ const pluginPackage = {
62
+ name: `${config.prefix}_${config.name}`,
63
+ version: config.version,
64
+ content: content
65
+ };
66
+ const options = {};
67
+ if (solution) {
68
+ options.customHeaders = { 'MSCRM.SolutionUniqueName': solution };
69
+ }
70
+ const result = await createWithReturnData(apiConfig, 'pluginpackages', pluginPackage, '$select=pluginpackageid', options);
71
+ if (result?.error) {
72
+ throw new Error(result.error.message);
73
+ }
74
+ return result.pluginpackageid;
75
+ }
76
+ async function updatePackage(config, content, apiConfig) {
77
+ logger.info(`update package ${config.name}`);
78
+ const updated = {
79
+ content: content,
80
+ version: config.version
81
+ };
82
+ const result = await update(apiConfig, 'pluginpackages', config.pluginpackageid, updated);
83
+ if (result?.error) {
84
+ throw new Error(result.error.message);
85
+ }
86
+ }
@@ -0,0 +1,93 @@
1
+ import { retrieveMultiple, createWithReturnData, update } from 'dataverse-webapi/lib/node';
2
+ import { logger } from '../logger.js';
3
+ import { deployImage } from './pluginImage.js';
4
+ export async function deployStep(config, pluginTypeId, apiConfig, solution) {
5
+ const step = structuredClone(config);
6
+ step['plugintypeid@odata.bind'] = `/plugintypes(${pluginTypeId})`;
7
+ const messageId = await getSdkMessageId(step.message ?? '', apiConfig);
8
+ if (messageId == '') {
9
+ logger.warn(`sdk message ${step.message} not found`);
10
+ return;
11
+ }
12
+ if (step.entity !== '' && step.entity !== 'none') {
13
+ const filterId = await getSdkMessageFilterId(messageId, step.entity ?? '', apiConfig);
14
+ if (filterId == '') {
15
+ logger.warn(`sdk message ${step.message} for entity ${step.entity} not found`);
16
+ return;
17
+ }
18
+ step['sdkmessagefilterid@odata.bind'] = `/sdkmessagefilters(${filterId})`;
19
+ }
20
+ step['sdkmessageid@odata.bind'] = `/sdkmessages(${messageId})`;
21
+ step.asyncautodelete = step.mode === 1;
22
+ delete step.images;
23
+ delete step.message;
24
+ delete step.entity;
25
+ delete step.sdkmessageprocessingstepid;
26
+ if (!config.sdkmessageprocessingstepid) {
27
+ config.sdkmessageprocessingstepid = await retrieveStep(step.name, pluginTypeId, apiConfig);
28
+ }
29
+ if (config.sdkmessageprocessingstepid !== '') {
30
+ try {
31
+ await updateStep(config.sdkmessageprocessingstepid, step, apiConfig);
32
+ }
33
+ catch (error) {
34
+ throw new Error(`failed to update plugin step: ${error.message}`);
35
+ }
36
+ }
37
+ else {
38
+ try {
39
+ config.sdkmessageprocessingstepid = await createStep(step, apiConfig, solution);
40
+ }
41
+ catch (error) {
42
+ throw new Error(`failed to create plugin step: ${error.message}`);
43
+ }
44
+ }
45
+ if (config.images && config.images.length > 0) {
46
+ try {
47
+ const promises = config.images.map((image) => deployImage(config.sdkmessageprocessingstepid, step.name, image, config.message, apiConfig));
48
+ await Promise.all(promises);
49
+ }
50
+ catch (error) {
51
+ throw new Error(error.message);
52
+ }
53
+ }
54
+ }
55
+ async function getSdkMessageFilterId(messageId, entityName, apiConfig) {
56
+ const options = [
57
+ `?$filter=primaryobjecttypecode eq '${entityName}' and _sdkmessageid_value eq ${messageId}`,
58
+ '&$select=sdkmessagefilterid'
59
+ ].join('');
60
+ const message = await retrieveMultiple(apiConfig, 'sdkmessagefilters', options);
61
+ return message.value.length > 0 ? message.value[0].sdkmessagefilterid : '';
62
+ }
63
+ async function getSdkMessageId(name, apiConfig) {
64
+ const options = [`?$filter=name eq '${name}'`, '&$select=sdkmessageid'].join('');
65
+ const message = await retrieveMultiple(apiConfig, 'sdkmessages', options);
66
+ return message.value.length > 0 ? message.value[0].sdkmessageid : '';
67
+ }
68
+ async function retrieveStep(name, typeId, apiConfig) {
69
+ const options = `$select=sdkmessageprocessingstepid&$filter=name eq '${name}' and _plugintypeid_value eq ${typeId}`;
70
+ const result = await retrieveMultiple(apiConfig, 'sdkmessageprocessingsteps', options);
71
+ return result.value.length > 0 ? result.value[0].sdkmessageprocessingstepid : '';
72
+ }
73
+ async function createStep(step, apiConfig, solution) {
74
+ logger.info(`create plugin step ${step.name}`);
75
+ const options = {};
76
+ if (solution) {
77
+ options.customHeaders = { 'MSCRM.SolutionUniqueName': solution };
78
+ }
79
+ const result = await createWithReturnData(apiConfig, 'sdkmessageprocessingsteps', step, '$select=sdkmessageprocessingstepid', options);
80
+ if (result?.error) {
81
+ throw new Error(result.error.message);
82
+ }
83
+ return result.sdkmessageprocessingstepid;
84
+ }
85
+ async function updateStep(id, step, apiConfig) {
86
+ logger.info(`update plugin step ${step.name}`);
87
+ const entity = { ...step };
88
+ delete entity.sdkmessageprocessingstepid;
89
+ const result = await update(apiConfig, 'sdkmessageprocessingsteps', id, entity);
90
+ if (result?.error) {
91
+ throw new Error(result.error.message);
92
+ }
93
+ }
@@ -0,0 +1,62 @@
1
+ import { retrieveMultiple, createWithReturnData, update } from 'dataverse-webapi/lib/node';
2
+ import { deployStep } from './pluginStep.js';
3
+ import { logger } from '../logger.js';
4
+ export async function deployType(config, assemblyId, apiConfig, solution) {
5
+ const type = {
6
+ name: config.name,
7
+ friendlyname: config.friendlyname,
8
+ typename: config.typename,
9
+ 'pluginassemblyid@odata.bind': `pluginassemblies(${assemblyId})`,
10
+ workflowactivitygroupname: config.workflowactivitygroupname
11
+ };
12
+ if (!config.plugintypeid) {
13
+ config.plugintypeid = await retrieveType(config.name, assemblyId, apiConfig);
14
+ }
15
+ if (config.plugintypeid !== '') {
16
+ try {
17
+ await updateType(config.plugintypeid, type, apiConfig);
18
+ }
19
+ catch (error) {
20
+ throw new Error(`failed to update plugin type: ${error.message}`);
21
+ }
22
+ }
23
+ else {
24
+ try {
25
+ config.plugintypeid = await createType(type, apiConfig);
26
+ }
27
+ catch (error) {
28
+ throw new Error(`failed to create plugin type: ${error.message}`);
29
+ }
30
+ }
31
+ try {
32
+ if (config.steps) {
33
+ const promises = config.steps.map((step) => deployStep(step, config.plugintypeid, apiConfig, solution));
34
+ await Promise.all(promises);
35
+ }
36
+ }
37
+ catch (error) {
38
+ throw new Error(error.message);
39
+ }
40
+ }
41
+ export async function retrieveType(name, assemblyId, apiConfig) {
42
+ const options = `$select=plugintypeid&$filter=typename eq '${name}' and _pluginassemblyid_value eq ${assemblyId}`;
43
+ const result = await retrieveMultiple(apiConfig, 'plugintypes', options);
44
+ return result.value.length > 0 ? result.value[0].plugintypeid : '';
45
+ }
46
+ async function createType(type, apiConfig) {
47
+ logger.info(`create assembly type ${type.name}`);
48
+ const result = await createWithReturnData(apiConfig, 'plugintypes', type, '$select=plugintypeid');
49
+ if (result?.error) {
50
+ throw new Error(result.error.message);
51
+ }
52
+ return result.plugintypeid;
53
+ }
54
+ async function updateType(id, type, apiConfig) {
55
+ logger.info(`update assembly type ${type.name}`);
56
+ const entity = { ...type };
57
+ delete entity.plugintypeid;
58
+ const result = await update(apiConfig, 'plugintypes', id, entity);
59
+ if (result?.error) {
60
+ throw new Error(result.error.message);
61
+ }
62
+ }
@@ -0,0 +1,126 @@
1
+ import { publish } from '../dataverse.service.js';
2
+ import { retrieveMultiple, createWithReturnData, update } from 'dataverse-webapi/lib/node';
3
+ import { logger } from '../logger.js';
4
+ import fs from 'fs';
5
+ function getWebResourceType(type) {
6
+ switch (type) {
7
+ case 'HTML':
8
+ return 1;
9
+ case 'CSS':
10
+ return 2;
11
+ case 'JavaScript':
12
+ return 3;
13
+ case 'XML':
14
+ return 4;
15
+ case 'PNG':
16
+ return 5;
17
+ case 'JPG':
18
+ return 6;
19
+ case 'GIF':
20
+ return 7;
21
+ case 'XAP':
22
+ return 8;
23
+ case 'XSL':
24
+ return 9;
25
+ case 'ICO':
26
+ return 10;
27
+ case 'SVG':
28
+ return 11;
29
+ case 'RESX':
30
+ return 12;
31
+ default:
32
+ return 0;
33
+ }
34
+ }
35
+ export async function deploy(webResources, apiConfig, solution, files) {
36
+ const publishXml = [];
37
+ let resources = webResources;
38
+ // Use list of files if provided
39
+ if (files) {
40
+ resources = [];
41
+ for (const file of files.split(',')) {
42
+ const resource = webResources.filter((r) => r.path?.endsWith(file));
43
+ if (resource.length === 0) {
44
+ logger.warn(`web resource ${file} not found in dataverse.config.json`);
45
+ continue;
46
+ }
47
+ resources.push(resource[0]);
48
+ }
49
+ }
50
+ const promises = resources.map(async (resource) => {
51
+ try {
52
+ if (!resource.webresourceid) {
53
+ resource.webresourceid = await retrieveResource(resource.name, apiConfig);
54
+ }
55
+ }
56
+ catch (error) {
57
+ logger.error(`failed to retrieve resource ${resource.name}: ${error.message}`);
58
+ return;
59
+ }
60
+ const fileContent = await fs.promises.readFile(resource.path, 'utf8');
61
+ const content = Buffer.from(fileContent).toString('base64');
62
+ if (resource.webresourceid !== '') {
63
+ try {
64
+ const updated = await updateResource(resource, content, apiConfig);
65
+ publishXml.push(updated);
66
+ }
67
+ catch (error) {
68
+ logger.error(`failed to update resource: ${error.message}`);
69
+ }
70
+ }
71
+ else {
72
+ try {
73
+ resource.webresourceid = await createResource(resource, content, apiConfig, solution);
74
+ }
75
+ catch (error) {
76
+ logger.error(`failed to create resource: ${error.message}`);
77
+ }
78
+ }
79
+ });
80
+ await Promise.all(promises);
81
+ // publish resources
82
+ if (publishXml.length > 0) {
83
+ try {
84
+ await publish(publishXml.join(''), apiConfig);
85
+ }
86
+ catch (error) {
87
+ logger.error(error.message);
88
+ }
89
+ }
90
+ }
91
+ async function retrieveResource(name, apiConfig) {
92
+ const options = `$select=webresourceid&$filter=name eq '${name}'`;
93
+ const result = await retrieveMultiple(apiConfig, 'webresourceset', options);
94
+ return result.value.length > 0 ? result.value[0].webresourceid : '';
95
+ }
96
+ async function createResource(resource, content, apiConfig, solution) {
97
+ logger.info(`create web resource ${resource.name}`);
98
+ const webResource = {
99
+ webresourcetype: getWebResourceType(resource.type),
100
+ name: resource.name,
101
+ displayname: resource.displayname || resource.name,
102
+ content: content
103
+ };
104
+ const options = {};
105
+ if (solution) {
106
+ options.customHeaders = { 'MSCRM.SolutionUniqueName': solution };
107
+ }
108
+ const result = await createWithReturnData(apiConfig, 'webresourceset', webResource, '$select=webresourceid', options);
109
+ if (result?.error) {
110
+ throw new Error(result.error.message);
111
+ }
112
+ return result.webresourceid;
113
+ }
114
+ async function updateResource(resource, content, apiConfig) {
115
+ logger.info(`update web resource ${resource.name}`);
116
+ const webResource = {
117
+ content: content,
118
+ name: resource.name,
119
+ displayname: resource.displayname || resource.name
120
+ };
121
+ const result = await update(apiConfig, 'webresourceset', resource.webresourceid, webResource);
122
+ if (result?.error) {
123
+ throw new Error(result.error.message);
124
+ }
125
+ return `<webresource>{${resource.webresourceid}}</webresource>`;
126
+ }
@@ -0,0 +1,24 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { logger } from './logger.js';
4
+ import { deploy } from './models/webResource.js';
5
+ export async function webResourceDeploy(creds, apiConfig, files) {
6
+ const currentPath = '.';
7
+ const configFile = await fs.promises.readFile(path.resolve(currentPath, 'dataverse.config.json'), 'utf8');
8
+ if (configFile == null) {
9
+ logger.warn('unable to find dataverse.config.json file');
10
+ return;
11
+ }
12
+ const config = JSON.parse(configFile);
13
+ const resources = config.webResources;
14
+ logger.info('deploy web resources');
15
+ try {
16
+ await deploy(resources, apiConfig, creds.solution, files);
17
+ }
18
+ catch (error) {
19
+ logger.error(error.message);
20
+ return;
21
+ }
22
+ logger.done('deployed web resources');
23
+ await fs.promises.writeFile(path.resolve(currentPath, 'dataverse.config.json'), JSON.stringify(config, null, 4), 'utf8');
24
+ }
package/license ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Derek Finlinson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "dataverse-utils",
3
- "version": "5.0.2",
3
+ "version": "5.0.3",
4
4
  "license": "MIT",
5
5
  "description": "Utilities for interacting with Dataverse environments",
6
6
  "exports": "./lib/dataverse-utils.js",
7
7
  "engines": {
8
8
  "node": ">=20.0.0"
9
9
  },
10
- "bin": "./lib/dataverse-utils.js",
10
+ "bin": {
11
+ "dataverse-utils": "lib/index.js"
12
+ },
11
13
  "files": [
12
14
  "/lib"
13
15
  ],
@@ -25,7 +27,7 @@
25
27
  "@azure/msal-node": "^3.5.0",
26
28
  "commander": "^13.1.0",
27
29
  "cryptr": "^6.3.0",
28
- "dataverse-webapi": "^3.0.2",
30
+ "dataverse-webapi": "^3.0.0",
29
31
  "figures": "^6.1.0",
30
32
  "glob": "^11.0.1",
31
33
  "kleur": "^4.1.5",