programmically give input to yeoman cmd for CI/CD tool - javascript

when we run the yeoman it is asking to add input one by one, is there anyway to give all input one go ? or programically?
for Example :
yo azuresfguest
it is asking to add 5 inputs, which i want to give one time, so i can run in CI/CD system
Thanks,

There isn't a general way, unfortunately. The specific generator would need to allow it.
I think it is deserving of a feature request on the Yeoman project, which I've logged here.
As a cumbersome workaround, you can create your own generator which re-uses an existing generator. The TypeScript code below gives an example; I'm using this approach to automate my CI process.
Add option to constructor:
constructor(args: string, opts: Generator.GeneratorOptions) {
super(args, opts);
...
this.option("prompts-json-file", {
type: String,
default: undefined,
description: "Skips prompting; uses file contents. Useful for automation",
});
}
Use the option:
async prompting() {
if (this.options["prompts-json-file"] !== undefined) {
this.answers = new Answers(JSON.parse(
fs.readFileSync(this.options["prompts-json-file"]).toString()
));
}
else {
this.answers = ...
}
}
Unfortunately this does bypass prompt validation so you'd need to separately ensure your file contains valid values.
Using it is relatively simple:
yo my-generator --prompts-json-file ./prompts.json

This should be accomplished using Yeomans storage API and a .yo-rc.json file:
https://yeoman.io/authoring/storage.html
I used to use a self defined option to make that optional similar to the approach from #BjornO
constructor(args, opts) {
super(args, opts);
// This method adds support for a `--yo-rc` flag
this.option('yo-rc', {
desc: 'Read and apply options from .yo-rc.json and skip prompting',
type: Boolean,
defaults: false
});
}
initializing() {
this.skipPrompts = false;
if (this.options['yo-rc']) {
const config = this.config.getAll();
this.log(
'Read and applied the following config from ' +
chalk.yellow('.yo-rc.json:\n')
);
this.log(config);
this.log('\n');
this.templateProps = {
projectName: config.projectName,
};
this.skipPrompts = true;
}
}
prompting() {
if (!this.skipPrompts) {
// Have Yeoman greet the user.
this.log(
yosay(
`Yo, welcome to the ${superb()} ${chalk.yellow(
'Baumeister'
)} generator!`
)
);
const prompts = [
{
type: 'input',
name: 'projectName',
message: 'What’s the name of your project?',
// Default to current folder name
default: _s.titleize(this.appname)
}
];
return this.prompt(prompts).then(props => {
this.templateProps = {
projectName: props.projectName
};
});
}
}
See https://github.com/micromata/generator-baumeister/blob/master/app/index.js#L20-L69 for the whole generator code and https://github.com/micromata/generator-baumeister/blob/master/\_\_tests__/yo-rc.json for the corresponding .yo-rc.json file.

Related

Glue42 - Register for updates in excel

I want to link an application to validate updates made in Excel. I am trying to use Glue42 interop methods. Does anyone have a code sample?
I have successfully linked two apps, but not excel
I change a cell in excel and my external app validates this can be changed.
I believe you missed the Glue for Office documentation. There are two sections you can go to from the home page: one for general programming and one for the Office connectors.
Anyways, up to the question, you need first to obtain a reference to a Glue4Office object with code, similar to the one below:
const config = {
// ...,
excel: true // enable Excel integration
}
Glue4Office(config)
.then(g4o => {
const excel = g4o.excel
// interact with Excel
})
.catch(console.error)
Then, once you open a sheet and get a reference to it, you can subscribe to its onChanged event, where you can call the errorCallback argument. Here is an example:
excel.openSheet(config)
.then(sheet => {
sheet.onChanged((data, errorCallback, doneCallback) => {
// process changes here
// errorCallback(...) - call if there are validation errors
// doneCallback() - call if not doing validations or everything's OK
})
})
sheet.onChanged((data, errorCallback, doneCallback) => {
// ...
const errors = []
data.reduce(
(errors, rowData, rowIndex) => {
if (!rowData['firstName']) {
errors.push({
row: rowIndex + 1,
column: 'firstName',
description: 'First name is mandatory'
})
}
if (Number(rowData['subscriptionMonths']) < 6) {
errors.push({
row: rowIndex + 1,
column: 1, // <- note column index this time
description: 'Subscription period must be at least 6 months',
text: '6' // <- replacing what the user typed
})
}
},
[])
// if during the validation pass we've accumulated any errors
// we need to call the errorCallback, otherwise the doneCallback
if (errors.length > 0) {
errorCallback(errors)
}
else {
doneCallback()
}
})
There are convenient declarative approaches as well.

How to create custom Registration and Login API using Strapi?

I am using strapi to create APIs.
I want to implement my own Registration API and Login API.
I checked the documentation of strapi but i am not finding any custom API for this.
can any one help me on this?
Same answer, but in more detail:
Strapi creates an Auth controller automatically for you and you can overwrite its behavior.
Copy the function(s) you need (e.g. register) from this file:
node_modules/strapi-plugin-users-permissions/controllers/Auth.js
to:
your_project_root/extensions/users-permissions/controllers/Auth.js
Now you can overwrite the behavior, e.g. pass a custom field inside the registration process {"myCustomField": "hello world"} and log it to the console:
async register(ctx) {
...
...
// log the custom field
console.log(params.myCustomField)
// do something with it, e.g. check whether the value already exists
// in another content type
const itExists = await strapi.query('some-content-type').findOne({
fieldName: params.myCustomField
});
if (!itExists) {
return ctx.badRequest(...)
} else {
console.log('check success')
}
}
Actually, strapi creates an Auth controller to handle these requests. You can just change them to fit in your need.
The path to the controller is:
plugins/users-permissions/controllers/Auth.js
in order to create custom users-permissons apis on server side you have to create
src/extensions/users-permissions/strapi-server.js
and in that file can write or override existing user-permissions plugin apis
here is the example for users/me
const _ = require('lodash');
module.exports = (plugin) => {
const getController = name => {
return strapi.plugins['users-permissions'].controller(name);
};
// Create the new controller
plugin.controllers.user.me = async (ctx) => {
const user = ctx.state.user;
// User has to be logged in to update themselves
if (!user) {
return ctx.unauthorized();
}
console.log('calling about meeeeeeeeeee------')
return;
};
// Add the custom route
plugin.routes['content-api'].routes.unshift({
method: 'GET',
path: '/users/me',
handler: 'user.me',
config: {
prefix: '',
}
});
return plugin;
};

Make a 'common' login.js include; with nightwatch.js tests

When writing tests for my web app; I have to first simulate login before the rest of my tests can run and see inner pages. Right now I'm working on modulating the code, so that way I can just make an 'include' for the common function; such as my login. But as soon as I move the below code in a separate file, and call the include via require - it no longer runs as expected.
ie. the below logs in and allows my other functions, if, included in the same file. above my other inner screen functions.
// Login screen, create opportunity
this.LoginScreen = function(browser) {
browser
.url(Data.urls.home)
.waitForElementVisible('#login', 2000, false)
.click('#login')
.waitForElementVisible('div.side-panel.open', 4000, false)
.waitForElementVisible('input#email', 2000, false)
.waitForElementVisible('input#password', 2000, false)
.click('input#email')
.pause(500)
.setValue('input#email', Data.ProjMan.username)
.click('input#password')
.pause(500)
.setValue('input#password', Data.ProjMan.password)
.click('input#email')
.pause(500)
.click('div.form.login-form .btn')
.pause(5000)
Errors.checkForErrors(browser);
};
// Inner functions run after here, sequentially
But as soon as I move the above in a separate file, for instance; Logins.js, then call it at the top of the original test file with. (yes, correct path).
var Logins = require("../../lib/Logins.js");
It just doesn't simulate the login anymore. Any thoughts? Should I remove the this.LoginScreen function wrapper, and call it differently to execute from the external file, or do I need to fire it from the original file again, aside from the external require path?
I have also tried wrapping 'module.exports = {' around the login function from separate file, but still failing.
Nightwatch allows you to run your Page object based tests i.e you can externalize your common test functions and use them in your regular tests. This can be achieved using 'page_objects_path' property. I have added the common 'login' functionality and used it in sample 'single test' in the project here.
Working:
Place your common function in .js file and place it under a folder(ex: tests/pages/login.js) and pass the folder path in nighwatch config file as below:
nightwatch_config = {
src_folders : [ 'tests/single' ],
page_objects_path: ['tests/pages'],
Below is an example of common login function (login.js):
var loginCommands = {
login: function() {
return this.waitForElementVisible('body', 1000)
.verify.visible('#userName')
.verify.visible('#password')
.verify.visible('#submit')
.setValue('#userName', 'Enter Github user name')
.setValue('#password', 'Enter Github password')
.waitForElementVisible('body', 2000)
}
};
module.exports = {
commands: [loginCommands],
url: function() {
return 'https://github.com/login';
},
elements: {
userName: {
selector: '//input[#name=\'login\']',
locateStrategy: 'xpath'
},
password: {
selector: '//input[#name=\'password\']',
locateStrategy: 'xpath'
},
submit: {
selector: '//input[#name=\'commit\']',
locateStrategy: 'xpath'
}
}
};
Now, in your regular test file, create an object for the common function as below and use it.
module.exports = {
'Github login Functionality' : function (browser) {
//create an object for login
var login = browser.page.login();
//execute the login method from //tests/pages/login.js file
login.navigate().login();
//You can continue with your tests below:
// Also, you can use similar Page objects to increase reusability
browser
.pause(3000)
.end();
}
};
The above answer is absolutly correct however I did struggle with how to supply login user details.
This is what I ended up using:
var loginCommands = {
login: function() {
return this.waitForElementVisible('body', 1000)
.setValue("#email", "<some rnd email address>")
.setValue('#password', "<some rnd password>")
.click('button[type=submit]')
.pause(1000)
}
};
module.exports = {
commands: [loginCommands],
url: function() {
return 'https://example.com/login';
}
};
This can be used in the same way as the accepted answer just posting for others who come searching.

Structure role-management in meteor-app with alanning:roles

I need some advice for building a correct role schema and management in my meteor-app.
Structure
Im using alanning:roles#1.2.13 for adding role management functionallity to the app.
There are four different user-types: Admin, Editor, Expert and User.
Furthermore there are several modules with different content, i.e. Cars, Maths and Images. Every module is organized in an own meteor-package.
In every module there are several categories, which can be added dynamically by editors.
Categories in modules
Module is structured like this:
elementSchema = new SimpleSchema({
element: {type: String, optional: true}
});
Cars.attachSchema(new SimpleSchema({
title: { type: String },
content: { type: String },
category: { type: [elementSchema], optional: true },
});
As you can see, all available categories are inside of the Collection of the module.
Rights
Admin: Complete rights
Editor: Can edit elements in selected moduls (i.e. editor_1 can edit elements in Cars and Images but not for Maths)
Expert: Can get rights to a complete module or just to some categories of a module (i.e.) expert_1 can edit Images, but only the elements in category "Honda" and "Mercedes" in Cars; no editing to Maths)
User: No editing
This is how I do the authentification technically:
router.js
var filters = {
authenticate: function () {
var user;
if (Meteor.loggingIn()) {
this.layout('login');
this.render('loading');
} else {
user = Meteor.user();
if (!user) {
this.layout('login');
this.render('signin');
return;
}
this.layout('Standard');
this.next();
}
}
}
Router.route('/car/:_id', {
name: 'car',
before: filters.authenticate,
data: function () {
return {
cars: Cars.findOne({ _id: this.params._id })
};
}
});
template
<template name="car">
{{#if isInRole 'cars'}}
Some form for editing
{{else}}
<h1>Restricted area</h1>
{{/if}}
</template>
I put this router.js to every package. Only change is the data function which uses the Collection of each package (Cars, Maths, Images).
Update: As 'Eliezer Steinbock' commented it is necessary to restrict acces to the mongoDB itself. But until now I only did that on the routes.
permissions.js
Cars.allow({
insert: function(userId) {
var loggedInUser = Meteor.user()
if (loggedInUser && Roles.userIsInRole(loggedInUser, ['admin','editor'])) return true;
},
update: function(userId) {
var loggedInUser = Meteor.user()
if (loggedInUser && Roles.userIsInRole(loggedInUser, ['admin','editor'])) return true;
}
});
My problems
1) My first problem is how to use roles and groups. What would be the best way for using groups? And the second problem is, that there are no fixed categories in the modules. Right now I have no idea for a useful role/group schema.
2) How do I check for the roles? As there are different roles which can get access: admin, editor and expert. Also I got the problem with these experts who just get access to defined categories of this module.
3) Wouldn't it be better to make the permission.js more general. I mean, is it possible to make a dynamic function, so I don't have to put everywhere the same code? How do I implement the roles in the permission.js in a useful way?
if the logic for the permissions is the same you could just define it once in permissions.js
App = App || {}; // We are using Namespaces, so you don't have to.. but it's good
App.Permissions = {
insert: function(userId) {
var loggedInUser = Meteor.user()
if (loggedInUser && Roles.userIsInRole(loggedInUser, ['admin','editor'])) return true;
},
update: function(userId) {
var loggedInUser = Meteor.user()
if (loggedInUser && Roles.userIsInRole(loggedInUser, ['admin','editor'])) return true;
}
}
And then you can use it for your Collections:
Cars.allow(App.Permissions); // Or
Cars.allow(App.Permissions.getPermissionsForGroup('cars'))
Define roles somewhere..
Roles
// Give user the role "editor" in "cars" group
Roles.addUsersToRoles(someUserId, ['editor'], 'cars');
Roles.addUsersToRoles(someOtherId, ['admin'], 'cars');
Which you can prepare in permissions.js like this:
Permissions
App = App || {};
App.Permissions = {
insert: function(userId) {...},
update: function(userId) {...},
getPermissionsForGroup: function(group) {
return {
insert: function(userId, doc) {
// Only admin can insert
return Roles.userIsInRole(userId, "admin", group);
},
update: function(userId, doc, fields, modifier) {
// Editor & Admin can edit
return Roles.userIsInRole(userId, ["editor","admin"], group);
},
remove: function(userId, doc) {
// Only admin can remove
return Roles.userIsInRole(userId, "admin", group);
}
}
}
In this example admins can insert and update.. and editors can only update, but insert.
Regarding the documentation of alanning:roles you define and use roles like this:
// Super Admin definition..
Roles.addUsersToRoles(superAdminId, ['admin'], Roles.GLOBAL_GROUP);
Roles.addUsersToRoles(joesUserId, ['manage-team','schedule-game'], 'manchester-united.com')
Roles.addUsersToRoles(joesUserId, ['player','goalie'], 'real-madrid.com')
Roles.userIsInRole(joesUserId, 'manage-team', 'manchester-united.com') // => true
Roles.userIsInRole(joesUserId, 'manage-team', 'real-madrid.com') // => false
Yeah, make sure, that the permission logic will be included before your Collection definition.. obviously :)

How do I configure different environments in Angular.js?

How do you manage configuration variables/constants for different environments?
This could be an example:
My rest API is reachable on localhost:7080/myapi/, but my friend that works on the same code under Git version control has the API deployed on his Tomcat on localhost:8099/hisapi/.
Supposing that we have something like this :
angular
.module('app', ['ngResource'])
.constant('API_END_POINT','<local_end_point>')
.factory('User', function($resource, API_END_POINT) {
return $resource(API_END_POINT + 'user');
});
How do I dynamically inject the correct value of the API endpoint, depending on the environment?
In PHP I usually do this kind of stuff with a config.username.xml file, merging the basic configuration file (config.xml) with the local environment configuration file recognised by the name of the user. But I don't know how to manage this kind of thing in JavaScript?
I'm a little late to the thread, but if you're using Grunt I've had great success with grunt-ng-constant.
The config section for ngconstant in my Gruntfile.js looks like
ngconstant: {
options: {
name: 'config',
wrap: '"use strict";\n\n{%= __ngModule %}',
space: ' '
},
development: {
options: {
dest: '<%= yeoman.app %>/scripts/config.js'
},
constants: {
ENV: 'development'
}
},
production: {
options: {
dest: '<%= yeoman.dist %>/scripts/config.js'
},
constants: {
ENV: 'production'
}
}
}
The tasks that use ngconstant look like
grunt.registerTask('server', function (target) {
if (target === 'dist') {
return grunt.task.run([
'build',
'open',
'connect:dist:keepalive'
]);
}
grunt.task.run([
'clean:server',
'ngconstant:development',
'concurrent:server',
'connect:livereload',
'open',
'watch'
]);
});
grunt.registerTask('build', [
'clean:dist',
'ngconstant:production',
'useminPrepare',
'concurrent:dist',
'concat',
'copy',
'cdnify',
'ngmin',
'cssmin',
'uglify',
'rev',
'usemin'
]);
So running grunt server will generate a config.js file in app/scripts/ that looks like
"use strict";
angular.module("config", []).constant("ENV", "development");
Finally, I declare the dependency on whatever modules need it:
// the 'config' dependency is generated via grunt
var app = angular.module('myApp', [ 'config' ]);
Now my constants can be dependency injected where needed. E.g.,
app.controller('MyController', ['ENV', function( ENV ) {
if( ENV === 'production' ) {
...
}
}]);
One cool solution might be separating all environment-specific values into some separate angular module, that all other modules depend on:
angular.module('configuration', [])
.constant('API_END_POINT','123456')
.constant('HOST','localhost');
Then your modules that need those entries can declare a dependency on it:
angular.module('services',['configuration'])
.factory('User',['$resource','API_END_POINT'],function($resource,API_END_POINT){
return $resource(API_END_POINT + 'user');
});
Now you could think about further cool stuff:
The module, that contains the configuration can be separated into configuration.js, that will be included at your page.
This script can be easily edited by each of you, as long as you don’t check this separate file into git. But it's easier to not check in the configuration if it is in a separate file. Also, you could branch it locally.
Now, if you have a build-system, like ANT or Maven, your further steps could be implementing some placeholders for the values API_END_POINT, that will be replaced during build-time, with your specific values.
Or you have your configuration_a.js and configuration_b.js and decide at the backend which to include.
For Gulp users, gulp-ng-constant is also useful combined with gulp-concat, event-stream and yargs.
var concat = require('gulp-concat'),
es = require('event-stream'),
gulp = require('gulp'),
ngConstant = require('gulp-ng-constant'),
argv = require('yargs').argv;
var enviroment = argv.env || 'development';
gulp.task('config', function () {
var config = gulp.src('config/' + enviroment + '.json')
.pipe(ngConstant({name: 'app.config'}));
var scripts = gulp.src('js/*');
return es.merge(config, scripts)
.pipe(concat('app.js'))
.pipe(gulp.dest('app/dist'))
.on('error', function() { });
});
In my config folder I have these files:
ls -l config
total 8
-rw-r--r--+ 1 .. ci.json
-rw-r--r--+ 1 .. development.json
-rw-r--r--+ 1 .. production.json
Then you can run gulp config --env development and that will create something like this:
angular.module("app.config", [])
.constant("foo", "bar")
.constant("ngConstant", true);
I also have this spec:
beforeEach(module('app'));
it('loads the config', inject(function(config) {
expect(config).toBeTruthy();
}));
To achieve that, I suggest you to use AngularJS Environment Plugin: https://www.npmjs.com/package/angular-environment
Here's an example:
angular.module('yourApp', ['environment']).
config(function(envServiceProvider) {
// set the domains and variables for each environment
envServiceProvider.config({
domains: {
development: ['localhost', 'dev.local'],
production: ['acme.com', 'acme.net', 'acme.org']
// anotherStage: ['domain1', 'domain2'],
// anotherStage: ['domain1', 'domain2']
},
vars: {
development: {
apiUrl: '//localhost/api',
staticUrl: '//localhost/static'
// antoherCustomVar: 'lorem',
// antoherCustomVar: 'ipsum'
},
production: {
apiUrl: '//api.acme.com/v2',
staticUrl: '//static.acme.com'
// antoherCustomVar: 'lorem',
// antoherCustomVar: 'ipsum'
}
// anotherStage: {
// customVar: 'lorem',
// customVar: 'ipsum'
// }
}
});
// run the environment check, so the comprobation is made
// before controllers and services are built
envServiceProvider.check();
});
And then, you can call the variables from your controllers such as this:
envService.read('apiUrl');
Hope it helps.
You could use lvh.me:9000 to access your AngularJS app, (lvh.me just points to 127.0.0.1) and then specify a different endpoint if lvh.me is the host:
app.service("Configuration", function() {
if (window.location.host.match(/lvh\.me/)) {
return this.API = 'http://localhost\\:7080/myapi/';
} else {
return this.API = 'http://localhost\\:8099/hisapi/';
}
});
And then inject the Configuration service and use Configuration.API wherever you need to access the API:
$resource(Configuration.API + '/endpoint/:id', {
id: '#id'
});
A tad clunky, but works fine for me, albeit in a slightly different situation (API endpoints differ in production and development).
We could also do something like this.
(function(){
'use strict';
angular.module('app').service('env', function env() {
var _environments = {
local: {
host: 'localhost:3000',
config: {
apiroot: 'http://localhost:3000'
}
},
dev: {
host: 'dev.com',
config: {
apiroot: 'http://localhost:3000'
}
},
test: {
host: 'test.com',
config: {
apiroot: 'http://localhost:3000'
}
},
stage: {
host: 'stage.com',
config: {
apiroot: 'staging'
}
},
prod: {
host: 'production.com',
config: {
apiroot: 'production'
}
}
},
_environment;
return {
getEnvironment: function(){
var host = window.location.host;
if(_environment){
return _environment;
}
for(var environment in _environments){
if(typeof _environments[environment].host && _environments[environment].host == host){
_environment = environment;
return _environment;
}
}
return null;
},
get: function(property){
return _environments[this.getEnvironment()].config[property];
}
}
});
})();
And in your controller/service, we can inject the dependency and call the get method with property to be accessed.
(function() {
'use strict';
angular.module('app').service('apiService', apiService);
apiService.$inject = ['configurations', '$q', '$http', 'env'];
function apiService(config, $q, $http, env) {
var service = {};
/* **********APIs **************** */
service.get = function() {
return $http.get(env.get('apiroot') + '/api/yourservice');
};
return service;
}
})();
$http.get(env.get('apiroot') would return the url based on the host environment.
Good question!
One solution could be to continue using your config.xml file, and provide api endpoint information from the backend to your generated html, like this (example in php):
<script type="text/javascript">
angular.module('YourApp').constant('API_END_POINT', '<?php echo $apiEndPointFromBackend; ?>');
</script>
Maybe not a pretty solution, but it would work.
Another solution could be to keep the API_END_POINT constant value as it should be in production, and only modify your hosts-file to point that url to your local api instead.
Or maybe a solution using localStorage for overrides, like this:
.factory('User',['$resource','API_END_POINT'],function($resource,API_END_POINT){
var myApi = localStorage.get('myLocalApiOverride');
return $resource((myApi || API_END_POINT) + 'user');
});
Very late to the thread, but a technique I've used, pre-Angular, is to take advantage of JSON and the flexibility of JS to dynamically reference collection keys, and use inalienable facts of the environment (host server name, current browser language, etc.) as inputs to selectively discriminate/prefer suffixed key names within a JSON data structure.
This provides not merely deploy-environment context (per OP) but any arbitrary context (such as language) to provide i18n or any other variance required simultaneously, and (ideally) within a single configuration manifest, without duplication, and readably obvious.
IN ABOUT 10 LINES VANILLA JS
Overly-simplified but classic example: An API endpoint base URL in a JSON-formatted properties file that varies per environment where (natch) the host server will also vary:
...
'svcs': {
'VER': '2.3',
'API#localhost': 'http://localhost:9090/',
'API#www.uat.productionwebsite.com': 'https://www.uat.productionwebsite.com:9090/res/',
'API#www.productionwebsite.com': 'https://www.productionwebsite.com:9090/api/res/'
},
...
A key to the discrimination function is simply the server hostname in the request.
This, naturally, can be combined with an additional key based on the user's language settings:
...
'app': {
'NAME': 'Ferry Reservations',
'NAME#fr': 'Réservations de ferry',
'NAME#de': 'Fähren Reservierungen'
},
...
The scope of the discrimination/preference can be confined to individual keys (as above) where the "base" key is only overwritten if there's a matching key+suffix for the inputs to the function -- or an entire structure, and that structure itself recursively parsed for matching discrimination/preference suffixes:
'help': {
'BLURB': 'This pre-production environment is not supported. Contact Development Team with questions.',
'PHONE': '808-867-5309',
'EMAIL': 'coder.jen#lostnumber.com'
},
'help#www.productionwebsite.com': {
'BLURB': 'Please contact Customer Service Center',
'BLURB#fr': 'S\'il vous plaît communiquer avec notre Centre de service à la clientèle',
'BLURB#de': 'Bitte kontaktieren Sie unseren Kundendienst!!1!',
'PHONE': '1-800-CUS-TOMR',
'EMAIL': 'customer.service#productionwebsite.com'
},
SO, if a visiting user to the production website has German (de) language preference setting, the above configuration would collapse to:
'help': {
'BLURB': 'Bitte kontaktieren Sie unseren Kundendienst!!1!',
'PHONE': '1-800-CUS-TOMR',
'EMAIL': 'customer.service#productionwebsite.com'
},
What does such a magical preference/discrimination JSON-rewriting function look like? Not much:
// prefer(object,suffix|[suffixes]) by/par/durch storsoc
// prefer({ a: 'apple', a#env: 'banana', b: 'carrot' },'env') -> { a: 'banana', b: 'carrot' }
function prefer(o,sufs) {
for (var key in o) {
if (!o.hasOwnProperty(key)) continue; // skip non-instance props
if(key.split('#')[1]) { // suffixed!
// replace root prop with the suffixed prop if among prefs
if(o[key] && sufs.indexOf(key.split('#')[1]) > -1) o[key.split('#')[0]] = JSON.parse(JSON.stringify(o[key]));
// and nuke the suffixed prop to tidy up
delete o[key];
// continue with root key ...
key = key.split('#')[0];
}
// ... in case it's a collection itself, recurse it!
if(o[key] && typeof o[key] === 'object') prefer(o[key],sufs);
};
};
In our implementations, which include Angular and pre-Angular websites, we simply bootstrap the configuration well ahead of other resource calls by placing the JSON within a self-executing JS closure, including the prefer() function, and fed basic properties of hostname and language-code (and accepts any additional arbitrary suffixes you might need):
(function(prefs){ var props = {
'svcs': {
'VER': '2.3',
'API#localhost': 'http://localhost:9090/',
'API#www.uat.productionwebsite.com': 'https://www.uat.productionwebsite.com:9090/res/',
'API#www.productionwebsite.com': 'https://www.productionwebsite.com:9090/api/res/'
},
...
/* yadda yadda moar JSON und bisque */
function prefer(o,sufs) {
// body of prefer function, broken for e.g.
};
// convert string and comma-separated-string to array .. and process it
prefs = [].concat( ( prefs.split ? prefs.split(',') : prefs ) || []);
prefer(props,prefs);
window.app_props = JSON.parse(JSON.stringify(props));
})([location.hostname, ((window.navigator.userLanguage || window.navigator.language).split('-')[0]) ] );
A pre-Angular site would now have a collapsed (no # suffixed keys) window.app_props to refer to.
An Angular site, as a bootstrap/init step, simply copies the dead-dropped props object into $rootScope, and (optionally) destroys it from global/window scope
app.constant('props',angular.copy(window.app_props || {})).run( function ($rootScope,props) { $rootScope.props = props; delete window.app_props;} );
to be subsequently injected into controllers:
app.controller('CtrlApp',function($log,props){ ... } );
or referred to from bindings in views:
<span>{{ props.help.blurb }} {{ props.help.email }}</span>
Caveats? The # character is not valid JS/JSON variable/key naming, but so far accepted. If that's a deal-breaker, substitute for any convention you like, such as "__" (double underscore) as long as you stick to it.
The technique could be applied server-side, ported to Java or C# but your efficiency/compactness may vary.
Alternately, the function/convention could be part of your front-end compile script, so that the full gory all-environment/all-language JSON is never transmitted over the wire.
UPDATE
We've evolved usage of this technique to allow multiple suffixes to a key, to avoid being forced to use collections (you still can, as deeply as you want), and as well to honor the order of the preferred suffixes.
Example (also see working jsFiddle):
var o = { 'a':'apple', 'a#dev':'apple-dev', 'a#fr':'pomme',
'b':'banana', 'b#fr':'banane', 'b#dev&fr':'banane-dev',
'c':{ 'o':'c-dot-oh', 'o#fr':'c-point-oh' }, 'c#dev': { 'o':'c-dot-oh-dev', 'o#fr':'c-point-oh-dev' } };
/*1*/ prefer(o,'dev'); // { a:'apple-dev', b:'banana', c:{o:'c-dot-oh-dev'} }
/*2*/ prefer(o,'fr'); // { a:'pomme', b:'banane', c:{o:'c-point-oh'} }
/*3*/ prefer(o,'dev,fr'); // { a:'apple-dev', b:'banane-dev', c:{o:'c-point-oh-dev'} }
/*4*/ prefer(o,['fr','dev']); // { a:'pomme', b:'banane-dev', c:{o:'c-point-oh-dev'} }
/*5*/ prefer(o); // { a:'apple', b:'banana', c:{o:'c-dot-oh'} }
1/2 (basic usage) prefers '#dev' keys, discards all other suffixed keys
3 prefers '#dev' over '#fr', prefers '#dev&fr' over all others
4 (same as 3 but prefers '#fr' over '#dev')
5 no preferred suffixes, drops ALL suffixed properties
It accomplishes this by scoring each suffixed property and promoting the value of a suffixed property to the non-suffixed property when iterating over the properties and finding a higher-scored suffix.
Some efficiencies in this version, including removing dependence on JSON to deep-copy, and only recursing into objects that survive the scoring round at their depth:
function prefer(obj,suf) {
function pr(o,s) {
for (var p in o) {
if (!o.hasOwnProperty(p) || !p.split('#')[1] || p.split('##')[1] ) continue; // ignore: proto-prop OR not-suffixed OR temp prop score
var b = p.split('#')[0]; // base prop name
if(!!!o['##'+b]) o['##'+b] = 0; // +score placeholder
var ps = p.split('#')[1].split('&'); // array of property suffixes
var sc = 0; var v = 0; // reset (running)score and value
while(ps.length) {
// suffix value: index(of found suffix in prefs)^10
v = Math.floor(Math.pow(10,s.indexOf(ps.pop())));
if(!v) { sc = 0; break; } // found suf NOT in prefs, zero score (delete later)
sc += v;
}
if(sc > o['##'+b]) { o['##'+b] = sc; o[b] = o[p]; } // hi-score! promote to base prop
delete o[p];
}
for (var p in o) if(p.split('##')[1]) delete o[p]; // remove scores
for (var p in o) if(typeof o[p] === 'object') pr(o[p],s); // recurse surviving objs
}
if( typeof obj !== 'object' ) return; // validate
suf = ( (suf || suf === 0 ) && ( suf.length || suf === parseFloat(suf) ) ? suf.toString().split(',') : []); // array|string|number|comma-separated-string -> array-of-strings
pr(obj,suf.reverse());
}
If you're using Brunch, the plugin Constangular helps you to manage variables for different environments.
Have you seen this question and its answer?
You can set a globally valid value for you app like this:
app.value('key', 'value');
and then use it in your services. You could move this code to a config.js file and execute it on page load or another convenient moment.

Categories

Resources