Skip to content

Commit

Permalink
enable execution in attended mode (#173)
Browse files Browse the repository at this point in the history
* enable execution in attended mode

* 2.6.60
  • Loading branch information
kbarbounakis authored Nov 10, 2024
1 parent a2fe6ea commit 33cb963
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 6 deletions.
11 changes: 11 additions & 0 deletions UnattendedMode.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DataContext } from "./types";
import {ConfigurationBase} from '@themost/common';

export declare interface ConfigurableApplication {
getConfiguration(): ConfigurationBase
}

export declare function executeInUnattendedMode(context: DataContext, func: (callback: (err?: Error) => void) => void, callback: (err?: Error) => void): void;
export declare function executeInUnattendedModeAsync(context: DataContext, func: () => Promise<void>): Promise<void>;
export declare function enableUnattendedExecution(app: ConfigurableApplication, account?: string): void;
export declare function disableUnattendedExecution(app: ConfigurableApplication): void;
110 changes: 110 additions & 0 deletions UnattendedMode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
var unattendedMode = Symbol('unattendedMode');
var {RandomUtils} = require('@themost/common');

/**
* Execute a callable function with elevated privileges in unattended mode.
* @param {import('./types').DataContext} context
* @param {function(function(Error?): void)} callable
* @param {function(Error?): void} callback
* @returns {void}
*/
function executeInUnattendedMode(context, callable, callback) {
if (typeof callable !== 'function') {
return callback(new Error('Unattended mode requires a callable function'));
}
// if the context is already in unattended mode
if (context[unattendedMode]) {
try {
// execute callable function
void callable(function (err) {
return callback(err);
});
} catch (err) {
return callback(err);
}
} else {
var interactiveUser;
try {
const account = context.getConfiguration().getSourceAt('settings/auth/unattendedExecutionAccount');
if (account == null) {
return callback(new Error('The unattended execution account is not defined. The operation cannot be completed.'));
}
// enter unattended mode
context[unattendedMode] = true;
// get interactive user
if (context.user) {
interactiveUser = Object.assign({}, context.user);
// set interactive user
context.interactiveUser = interactiveUser;
}
if (account) {
context.user = {name: account, authenticationType: 'Basic'};
}
void callable(function (err) {
// restore user
if (interactiveUser) {
context.user = Object.assign({}, interactiveUser);
}
delete context.interactiveUser;
// exit unattended mode
delete context[unattendedMode];
return callback(err);
});
} catch (err) {
// restore user
if (interactiveUser) {
context.user = Object.assign({}, interactiveUser);
}
delete context.interactiveUser;
// exit unattended mode
delete context[unattendedMode];
return callback(err);
}
}
}

/**
* Execute a callable function with elevated privileges in unattended mode.
* @param {import('./types').DataContext} context
* @param {function(): Promise<void>} callable
* @returns {Promise<void>}
*/
function executeInUnattendedModeAsync(context, callable) {
return new Promise((resolve, reject) => {
void executeInUnattendedMode(context, function (cb) {
return callable().then(function () {
return cb();
}).catch(function (err) {
return cb(err);
});
}, function (err) {
if (err) {
return reject(err);
}
return resolve();
});
});
}

/**
* Enables unattended mode
* @param {{getConfiguration(): import('@themost/common').ConfigurationBase}} app
* @param {string=} executionAccount
*/
function enableUnattendedExecution(app, executionAccount) {
app.getConfiguration().setSourceAt('settings/auth/unattendedExecutionAccount', executionAccount || RandomUtils.randomChars(14));
}
/**
* Disables unattended mode
* @param {{getConfiguration(): import('@themost/common').ConfigurationBase}} app
*/
function disableUnattendedExecution(app) {
app.getConfiguration().setSourceAt('settings/auth/unattendedExecutionAccount', null);
}

module.exports = {
executeInUnattendedMode,
executeInUnattendedModeAsync,
enableUnattendedExecution,
disableUnattendedExecution
}
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from './data-listeners';
export * from './data-associations';
export * from './data-application';
export * from './data-errors';
export * from './UnattendedMode';
10 changes: 8 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ var { DataObjectAssociationListener,
DataObjectMultiAssociationError } = require('./data-associations');
var { DataApplication } = require('./data-application');

var { UknonwnAttributeError } = require('./data-errors');
var { UnknownAttributeError } = require('./data-errors');

var { executeInUnattendedModeAsync, executeInUnattendedMode, enableUnattendedExecution, disableUnattendedExecution } = require('./UnattendedMode');

module.exports = {
TypeParser,
Expand Down Expand Up @@ -168,6 +170,10 @@ module.exports = {
ODataModelBuilder,
ODataConventionModelBuilder,
EntitySetSchemaLoaderStrategy,
UknonwnAttributeError
UnknownAttributeError,
enableUnattendedExecution,
disableUnattendedExecution,
executeInUnattendedMode,
executeInUnattendedModeAsync
};

6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ module.exports = {
// ],

// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
moduleNameMapper: {
'^@themost/data$': [
'<rootDir>/index'
]
},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@themost/data",
"version": "2.6.58",
"version": "2.6.60",
"description": "MOST Web Framework Codename Blueshift - Data module",
"main": "index.js",
"types": "index.d.ts",
Expand Down
100 changes: 100 additions & 0 deletions spec/UnattendedMode.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {TestApplication} from './TestApplication';
import {
DataContext,
disableUnattendedExecution,
enableUnattendedExecution,
executeInUnattendedModeAsync
} from '@themost/data';
import {resolve} from 'path';

describe('UnattendedMode', () => {
let app: TestApplication;
let context: DataContext;
beforeAll((done) => {
app = new TestApplication(resolve(__dirname, 'test2'));
context = app.createContext();
return done();
});
afterAll(async () => {
await context.finalizeAsync();
await app.finalize();
});
it('should execute in unattended mode', async () => {
// set context user
context.user = {
name: '[email protected]'
};
const items = await context.model('Order').where('orderedItem/name').equal(
'Razer Blade (2013)'
).getItems();
expect(items.length).toBeFalsy();
await executeInUnattendedModeAsync(context, async () => {
expect(context.interactiveUser).toBeTruthy();
expect(context.interactiveUser.name).toEqual('[email protected]');
const items = await context.model('Order').where('orderedItem/name').equal(
'Razer Blade (2013)'
).getItems();
expect(items.length).toBeTruthy();
});
expect(context.interactiveUser).toBeFalsy();
});

it('should execute in unattended mode and get error', async () => {
// set context user
context.user = {
name: '[email protected]'
};
await expect(executeInUnattendedModeAsync(context, async () => {
throw new Error('Custom error');
})).rejects.toThrow();
expect(context.interactiveUser).toBeFalsy();
expect(context.user.name).toEqual('[email protected]');
});

it('should execute in unattended mode in series', async () => {
// set context user
context.user = {
name: '[email protected]'
};
await executeInUnattendedModeAsync(context, async () => {
expect(context.interactiveUser).toBeTruthy();
const items = await context.model('Order').where('orderedItem/name').equal(
'Razer Blade (2013)'
).getItems();
expect(items.length).toBeTruthy();
await executeInUnattendedModeAsync(context, async () => {
expect(context.interactiveUser).toBeTruthy();
expect(context.interactiveUser.name).toEqual('[email protected]');
const items = await context.model('Order').where('orderedItem/name').equal(
'Sony VAIO Flip 15'
).getItems();
expect(items.length).toBeTruthy();
});
});
expect(context.interactiveUser).toBeFalsy();
expect(context.user.name).toEqual('[email protected]');
});

it('should disable unattended execution', async () => {
// set context user
context.user = {
name: '[email protected]'
};
disableUnattendedExecution(app);
expect(app.getConfiguration().getSourceAt('settings/auth/unattendedExecutionAccount')).toBeFalsy();
await expect(executeInUnattendedModeAsync(context, async () => {
await context.model('Order').where('orderedItem/name').equal(
'Sony VAIO Flip 15'
).getItems();
})).rejects.toThrow('The unattended execution account is not defined. The operation cannot be completed.');
expect(context.interactiveUser).toBeFalsy();
enableUnattendedExecution(app);
expect(app.getConfiguration().getSourceAt('settings/auth/unattendedExecutionAccount')).toBeTruthy();
await executeInUnattendedModeAsync(context, async () => {
const items = await context.model('Order').where('orderedItem/name').equal(
'Razer Blade (2013)'
).getItems();
expect(items.length).toBeTruthy();
});
});
});
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"dom"
],
"paths": {
"@themost/data": [
"./index"
]
}
},
"exclude": [
Expand Down

0 comments on commit 33cb963

Please sign in to comment.