Skip to content

Commit

Permalink
implement unattended helper functions (#172)
Browse files Browse the repository at this point in the history
* implement unattended execution functions

* validate unattended mode

* fix lint errors

* fix lint

* update codacy exclude paths

* update type definition

* 2.15.0
  • Loading branch information
kbarbounakis authored Nov 10, 2024
1 parent 602a8c1 commit eabcef6
Show file tree
Hide file tree
Showing 13 changed files with 248 additions and 14 deletions.
3 changes: 3 additions & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ exclude_paths:
- "spec/**/*"
- ".github/**/*"
- "babel.config.js"
- "jest.config.js"
- "jest.setup.js"
- "jest.logger.js"
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 @@ -19,3 +19,4 @@ export * from './has-parent-junction';
export * from './data-listeners';
export * from './data-associations';
export * from './data-application';
export * from './UnattendedMode';
8 changes: 7 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ var { DataObjectAssociationListener,
DataObjectMultiAssociationError } = require('./data-associations');
var { DataApplication } = require('./data-application');

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

module.exports = {
TypeParser,
PrivilegeType,
Expand Down Expand Up @@ -165,6 +167,10 @@ module.exports = {
EntitySetKind,
ODataModelBuilder,
ODataConventionModelBuilder,
EntitySetSchemaLoaderStrategy
EntitySetSchemaLoaderStrategy,
executeInUnattendedMode,
executeInUnattendedModeAsync,
enableUnattendedExecution,
disableUnattendedExecution
};

13 changes: 5 additions & 8 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ module.exports = {

// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
maxWorkers: 1,

// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
Expand All @@ -80,14 +79,12 @@ module.exports = {
// "json",
// "node"
// ],

// 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: {
// '^@themost/query$': [
// '<rootDir>/modules/query/src/index'
// ]
// },

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
1 change: 1 addition & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {TraceUtils} = require('@themost/common');
const JestLogger = require('./jest.logger');
// noinspection JSCheckFunctionSignatures
TraceUtils.useLogger(new JestLogger());
/* global jest */
jest.setTimeout(30000);
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.14.3",
"version": "2.15.0",
"description": "MOST Web Framework Codename Blueshift - Data module",
"main": "index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion spec/DataModel.upsert.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('DataModel.upsert', () => {
return done();
});
afterAll(async () => {
await context.finalize();
await context.finalizeAsync();
await app.finalize();
});
it('should use upsert() to insert or update a single item', async () => {
Expand Down
3 changes: 3 additions & 0 deletions spec/TestApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export class TestApplication extends IApplication {
await service.finalize();
}
}
async finalizeAsync(): Promise<void> {
await this.finalize();
}

}

Expand Down
99 changes: 99 additions & 0 deletions spec/UnattendedMode.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {TestApplication, TestApplication2} from './TestApplication';
import {
DataContext,
disableUnattendedExecution,
enableUnattendedExecution,
executeInUnattendedModeAsync
} from '@themost/data';

describe('UnattendedMode', () => {
let app: TestApplication;
let context: DataContext;
beforeAll((done) => {
app = new TestApplication2();
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<any>(
(x) => x.orderedItem.name === '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<any>(
(x) => x.orderedItem.name === '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<any>(
(x) => x.orderedItem.name === '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<any>(
(x) => x.orderedItem.name === '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<any>(
(x) => x.orderedItem.name === '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<any>(
(x) => x.orderedItem.name === 'Razer Blade (2013)'
).getItems();
expect(items.length).toBeTruthy();
});
});
});
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
"dom"
],
"paths": {
"@themost/data": [
"./index"
]
}
},
"exclude": [
"node_modules"
]
}
}

0 comments on commit eabcef6

Please sign in to comment.