Populate document metadata using Data APIs
What a cluster. This was a lot of work to work around existing, bad APIs; there is no time to refactor at the moment; this already took much longer than expected.master
parent
65ab92f701
commit
0c24e3d280
|
@ -203,6 +203,10 @@ Program@tie{}XML
|
||||||
Data@tie{}API
|
Data@tie{}API
|
||||||
@end macro
|
@end macro
|
||||||
|
|
||||||
|
@macro dapiref
|
||||||
|
@dapi (@pxref{Data API,,Data@tie{}API})
|
||||||
|
@end macro
|
||||||
|
|
||||||
@c todo: link to reference directly
|
@c todo: link to reference directly
|
||||||
@macro proguicref{ref}
|
@macro proguicref{ref}
|
||||||
`\ref\' @proguicrefsuffix
|
`\ref\' @proguicrefsuffix
|
||||||
|
|
|
@ -40,8 +40,11 @@ Programs are ideally compiled from a @ref{Program XML,,Program@tie{}XML}
|
||||||
@menu
|
@menu
|
||||||
* Program UI::
|
* Program UI::
|
||||||
* Program XML::
|
* Program XML::
|
||||||
|
* Document Metadata:: Document-level data that cannot be modified by
|
||||||
|
the client.
|
||||||
@end menu
|
@end menu
|
||||||
|
|
||||||
|
|
||||||
@node Program UI
|
@node Program UI
|
||||||
@section Program UI
|
@section Program UI
|
||||||
@maintenance{
|
@maintenance{
|
||||||
|
@ -220,3 +223,60 @@ Within the context of the @progxml,
|
||||||
It reads as a sentence:
|
It reads as a sentence:
|
||||||
``@samp{vacant_desc}'' is applicable when we should @tie{}``describe
|
``@samp{vacant_desc}'' is applicable when we should @tie{}``describe
|
||||||
a vacant property''.
|
a vacant property''.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@node Document Metadata
|
||||||
|
@section Document Metadata
|
||||||
|
@dfn{Document metadata} are metadata that describe certain aspects of the document;
|
||||||
|
they are stored adjacent to the bucket in @samp{meta}@tie{}on the
|
||||||
|
document root.@footnote{
|
||||||
|
Terminology note: ``document'' and ``quote'' are the same thing;
|
||||||
|
the latter is transitioning to the former for generality.}
|
||||||
|
They should be used in place of a bucket field any time
|
||||||
|
the client has no business knowing about the data.
|
||||||
|
The @samp{meta} record is called the @dfn{Metabucket}.
|
||||||
|
|
||||||
|
@c don't use a dapi xref here; don't want to confuse the reader by
|
||||||
|
@c directing them away from this section before they continue reading
|
||||||
|
@tip{Metadata in the Metabucket should@tie{}@emph{not} be
|
||||||
|
directly populated by external systems@mdash{
|
||||||
|
}@dapi integration should be used instead (see below).}
|
||||||
|
|
||||||
|
Metadata can be populated using any@tie{}@dapiref@mdash{
|
||||||
|
}return data populate the Metabucket in the same way that they
|
||||||
|
populate the Bucket.
|
||||||
|
Definitions are stored in @code{meta.fields},
|
||||||
|
as shown in @ref{f:meta-fields}.
|
||||||
|
|
||||||
|
@float Figure, f:meta-fields
|
||||||
|
@example
|
||||||
|
"fields":@{
|
||||||
|
["string(name)": @{
|
||||||
|
"desc": "string",
|
||||||
|
"dapi": @{
|
||||||
|
"name": "string",
|
||||||
|
"map": @{
|
||||||
|
"string(dest field)": "string(source field)"
|
||||||
|
@}
|
||||||
|
@}
|
||||||
|
@}
|
||||||
|
@}
|
||||||
|
@end example
|
||||||
|
@caption{Format of @code{meta.fields}.}
|
||||||
|
@end float
|
||||||
|
|
||||||
|
Further, a key-value mapping of all bucket fields that@mdash{
|
||||||
|
}when modified,
|
||||||
|
need to result in a metadata API@tie{}call@mdash{
|
||||||
|
}are stored in the @code{mapis}@tie{}object;
|
||||||
|
this is shown in @ref{f:mapis}.
|
||||||
|
|
||||||
|
@float Figure, f:mapis
|
||||||
|
@example
|
||||||
|
"mapis":@{
|
||||||
|
["string(field name)"]: [ "string(dapi name)", ... ]
|
||||||
|
@}
|
||||||
|
@end example
|
||||||
|
@caption{Format of @code{mapis}.}
|
||||||
|
@end float
|
||||||
|
|
|
@ -56,9 +56,10 @@ The HTTP server is managed by
|
||||||
|
|
||||||
|
|
||||||
@menu
|
@menu
|
||||||
* Requests:: Handling HTTP requests
|
* Requests:: Handling HTTP requests.
|
||||||
* Posting Data:: Handling step saves and other posts.
|
* Posting Data:: Handling step saves and other posts.
|
||||||
* Encryption Service:: Managing sensitive data.
|
* Server-Side Data API Calls:: Accessing external resources on the server.
|
||||||
|
* Encryption Service:: Managing sensitive data.
|
||||||
@end menu
|
@end menu
|
||||||
|
|
||||||
|
|
||||||
|
@ -202,6 +203,11 @@ Once those basic checks have passed,
|
||||||
re-calculated on the server (the values posted by the client have
|
re-calculated on the server (the values posted by the client have
|
||||||
already been discarded by the first step in this list);
|
already been discarded by the first step in this list);
|
||||||
|
|
||||||
|
@item
|
||||||
|
Server-side @dapi{} calls (@pxref{Data API}) are triggered using the
|
||||||
|
diff as input data and an empty bucket for response storage
|
||||||
|
(@pxref{Server-Side Data API Calls});
|
||||||
|
|
||||||
@item
|
@item
|
||||||
@cindex Premium calculation date
|
@cindex Premium calculation date
|
||||||
The last premium calculation date is cleared (indicating that
|
The last premium calculation date is cleared (indicating that
|
||||||
|
@ -226,6 +232,37 @@ Once those basic checks have passed,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@node Server-Side Data API Calls
|
||||||
|
@section Server-Side Data API Calls
|
||||||
|
@maintenance{This makes use of @srcrefjs{server/meta,DapiMetaSource}
|
||||||
|
to encapsulate the horrible API of @srcrefjs{dapi,DataApiManager};
|
||||||
|
the latter needs cleanup to remove the former.}
|
||||||
|
|
||||||
|
@cindex Data API
|
||||||
|
@cindex Document metadata
|
||||||
|
Server-side @dapi{} calls (@pxref{Data API}) are triggered on
|
||||||
|
step save (@pxref{Posting Data}) and are handled much like they are
|
||||||
|
on the client.
|
||||||
|
Such calls are made automatically only for document metadata.
|
||||||
|
Results of sever-side calls are @emph{not} written to the bucket
|
||||||
|
and are therefore useful for data that the client should not be
|
||||||
|
permitted to modify;
|
||||||
|
it also allows data to be kept secret from the client.@footnote{
|
||||||
|
All bucket data is served to the client,
|
||||||
|
with the exception of internal fields if the user is non-internal.}
|
||||||
|
|
||||||
|
@dapi{} results on the client can be mapped back to multiple bucket values;
|
||||||
|
the server, however, has serious concerns with how data are
|
||||||
|
propagated for data integrity and security reasons.
|
||||||
|
Further,
|
||||||
|
document metadata can be structured,
|
||||||
|
unlike the Bucket which has a rigid matrix format (@pxref{Bucket}).
|
||||||
|
Therefore,
|
||||||
|
the entire response is mapped into the parent field;
|
||||||
|
defined return values are used only for filtering.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@node Encryption Service
|
@node Encryption Service
|
||||||
@section Encryption Service
|
@section Encryption Service
|
||||||
@helpwanted
|
@helpwanted
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
const Class = require( 'easejs' ).Class;
|
const Class = require( 'easejs' ).Class;
|
||||||
const HttpDataApi = require( './http/HttpDataApi' );
|
const HttpDataApi = require( './http/HttpDataApi' );
|
||||||
const XhrHttpImpl = require( './http/XhrHttpImpl' );
|
const XhrHttpImpl = require( './http/XhrHttpImpl' );
|
||||||
const NodeHttpImpl = require( './http/NodeHttpImpl' );
|
|
||||||
const JsonResponse = require( './format/JsonResponse' );
|
const JsonResponse = require( './format/JsonResponse' );
|
||||||
const RestrictedDataApi = require( './RestrictedDataApi' );
|
const RestrictedDataApi = require( './RestrictedDataApi' );
|
||||||
const StaticAdditionDataApi = require( './StaticAdditionDataApi' );
|
const StaticAdditionDataApi = require( './StaticAdditionDataApi' );
|
||||||
|
@ -30,7 +29,7 @@ const BucketDataApi = require( './BucketDataApi' );
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates the appropriate DataApi object for the givne service type
|
* Instantiates the appropriate DataApi object for the given service type
|
||||||
*/
|
*/
|
||||||
module.exports = Class( 'DataApiFactory',
|
module.exports = Class( 'DataApiFactory',
|
||||||
{
|
{
|
||||||
|
@ -58,15 +57,7 @@ module.exports = Class( 'DataApiFactory',
|
||||||
switch ( type )
|
switch ( type )
|
||||||
{
|
{
|
||||||
case 'rest':
|
case 'rest':
|
||||||
const impl = ( typeof XMLHttpRequest !== 'undefined' )
|
const impl = this.createHttpImpl();
|
||||||
? XhrHttpImpl( XMLHttpRequest )
|
|
||||||
: NodeHttpImpl(
|
|
||||||
{
|
|
||||||
http: require( 'http' ),
|
|
||||||
https: require( 'https' ),
|
|
||||||
},
|
|
||||||
require( 'url' )
|
|
||||||
);
|
|
||||||
|
|
||||||
api = HttpDataApi.use( JsonResponse )(
|
api = HttpDataApi.use( JsonResponse )(
|
||||||
source,
|
source,
|
||||||
|
@ -93,6 +84,12 @@ module.exports = Class( 'DataApiFactory',
|
||||||
StaticAdditionDataApi( api, nonempty, multiple, static_data ),
|
StaticAdditionDataApi( api, nonempty, multiple, static_data ),
|
||||||
desc
|
desc
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
|
||||||
|
'virtual protected createHttpImpl'()
|
||||||
|
{
|
||||||
|
return XhrHttpImpl( XMLHttpRequest );
|
||||||
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
|
@ -24,19 +24,24 @@ const { Class } = require( 'easejs' );
|
||||||
const {
|
const {
|
||||||
bucket: {
|
bucket: {
|
||||||
bucket_filter,
|
bucket_filter,
|
||||||
|
QuoteDataBucket,
|
||||||
},
|
},
|
||||||
|
|
||||||
dapi: {
|
dapi: {
|
||||||
DataApiFactory,
|
|
||||||
DataApiManager,
|
DataApiManager,
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
Server,
|
Server,
|
||||||
|
|
||||||
|
meta: {
|
||||||
|
DapiMetaSource,
|
||||||
|
},
|
||||||
|
|
||||||
request: {
|
request: {
|
||||||
DataProcessor,
|
DataProcessor,
|
||||||
JsonServerResponse,
|
JsonServerResponse,
|
||||||
|
ServerDataApiFactory,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = require( '../..' );
|
} = require( '../..' );
|
||||||
|
@ -51,6 +56,18 @@ module.exports = Class( 'DocumentServer',
|
||||||
new JsonServerResponse.create(),
|
new JsonServerResponse.create(),
|
||||||
dao,
|
dao,
|
||||||
logger,
|
logger,
|
||||||
enc_service
|
enc_service,
|
||||||
|
|
||||||
|
DataProcessor(
|
||||||
|
bucket_filter,
|
||||||
|
( apis, request ) => DataApiManager(
|
||||||
|
ServerDataApiFactory(
|
||||||
|
origin_url || request.getOrigin(),
|
||||||
|
request
|
||||||
|
),
|
||||||
|
apis
|
||||||
|
),
|
||||||
|
DapiMetaSource( QuoteDataBucket )
|
||||||
|
)
|
||||||
),
|
),
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -48,6 +48,9 @@ const {
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
|
request: {
|
||||||
|
DataProcessor,
|
||||||
|
},
|
||||||
encsvc: {
|
encsvc: {
|
||||||
QuoteDataBucketCipher,
|
QuoteDataBucketCipher,
|
||||||
},
|
},
|
||||||
|
@ -109,13 +112,27 @@ module.exports = Class( 'Server' )
|
||||||
*/
|
*/
|
||||||
'private _cache': null,
|
'private _cache': null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-provided data processor
|
||||||
|
* @type {DataProcessor}
|
||||||
|
*/
|
||||||
|
'private _dataProcessor': null,
|
||||||
|
|
||||||
'public __construct': function( response, dao, logger, encsvc )
|
|
||||||
|
'public __construct': function(
|
||||||
|
response, dao, logger, encsvc, data_processor
|
||||||
|
)
|
||||||
{
|
{
|
||||||
this.response = response;
|
if ( !Class.isA( DataProcessor, data_processor ) )
|
||||||
this.dao = dao;
|
{
|
||||||
this.logger = logger;
|
throw TypeError( "Expected DataProcessor" );
|
||||||
this._encService = encsvc;
|
}
|
||||||
|
|
||||||
|
this.response = response;
|
||||||
|
this.dao = dao;
|
||||||
|
this.logger = logger;
|
||||||
|
this._encService = encsvc;
|
||||||
|
this._dataProcessor = data_processor;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -1115,14 +1132,19 @@ module.exports = Class( 'Server' )
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var filtered = server._sanitizeBucketData(
|
var parsed_data = JSON.parse( post_data.data );
|
||||||
post_data.data, request, program
|
var bucket = quote.getBucket();
|
||||||
|
|
||||||
|
const { filtered, dapis } = server._dataProcessor.processDiff(
|
||||||
|
parsed_data, request, program, bucket
|
||||||
);
|
);
|
||||||
|
|
||||||
quote.setData( filtered );
|
quote.setData( filtered );
|
||||||
|
|
||||||
|
server._monitorMetadataPromise( quote, dapis );
|
||||||
|
|
||||||
// calculated values (store only)
|
// calculated values (store only)
|
||||||
program.initQuote( quote.getBucket(), true );
|
program.initQuote( bucket, true );
|
||||||
}
|
}
|
||||||
catch ( err )
|
catch ( err )
|
||||||
{
|
{
|
||||||
|
@ -1150,33 +1172,27 @@ module.exports = Class( 'Server' )
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
'private _monitorMetadataPromise'( quote, dapis )
|
||||||
* Sanitize the given bucket data
|
|
||||||
*
|
|
||||||
* Ensures that we are storing only "correct" data within our database. This
|
|
||||||
* also strips any unknown bucket values, preventing users from using us as
|
|
||||||
* their own personal database.
|
|
||||||
*/
|
|
||||||
'private _sanitizeBucketData': function(
|
|
||||||
bucket_data, request, program, permit_null
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var data = JSON.parse( bucket_data ),
|
dapis.map( promise => promise
|
||||||
types = program.meta.qtypes,
|
.then( ( { field, index, data } ) =>
|
||||||
ignore = {};
|
this.dao.saveQuoteMeta(
|
||||||
|
quote,
|
||||||
// if we're not internal, filter out the internal questions
|
data,
|
||||||
// (so they can't post to them)
|
null,
|
||||||
if ( request.getSession().isInternal() === false )
|
e => { throw e; }
|
||||||
{
|
)
|
||||||
for ( id in program.internal )
|
)
|
||||||
{
|
.catch( e =>
|
||||||
ignore[ id ] = true;
|
server.logger.log(
|
||||||
}
|
server.logger.PRIORITY_ERROR,
|
||||||
}
|
"Failed to save field %s[%s] metadata: %s",
|
||||||
|
field,
|
||||||
// return the filtered data
|
index,
|
||||||
return bucket_filter.filter( data, types, ignore, permit_null );
|
e.message
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -1619,8 +1635,10 @@ module.exports = Class( 'Server' )
|
||||||
// sanitize, permitting nulls (since the diff will have them)
|
// sanitize, permitting nulls (since the diff will have them)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var filtered = _self._sanitizeBucketData(
|
var data = JSON.parse( post_data.data );
|
||||||
post_data.data, request, program, true
|
|
||||||
|
var filtered = _self._dataProcessor.sanitizeDiff(
|
||||||
|
data, request, program, true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
catch ( e )
|
catch ( e )
|
||||||
|
|
|
@ -108,9 +108,8 @@ exports.init = function( logger, enc_service )
|
||||||
{ native_parser: false, safe: false }
|
{ native_parser: false, safe: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
var dao = MongoServerDao( db );
|
const dao = MongoServerDao( db );
|
||||||
|
server = _createDocumentServer( dao, logger, enc_service );
|
||||||
server = DocumentServer().create( dao, logger, enc_service );
|
|
||||||
|
|
||||||
server_cache = _createCache( server );
|
server_cache = _createCache( server );
|
||||||
server.init( server_cache, exports.rater );
|
server.init( server_cache, exports.rater );
|
||||||
|
@ -149,6 +148,25 @@ exports.init = function( logger, enc_service )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _createDocumentServer( dao, logger, enc_service )
|
||||||
|
{
|
||||||
|
const origin_url = process.env.HTTP_ORIGIN_URL || '';
|
||||||
|
|
||||||
|
if ( !origin_url )
|
||||||
|
{
|
||||||
|
// this allows the system to work without configuration (e.g. for
|
||||||
|
// local development), but is really bad
|
||||||
|
logger.log( logger.PRIORITY_IMPORTANT,
|
||||||
|
"*** HTTP_ORIGIN_URL environment variable not set; " +
|
||||||
|
"system will fall back to using the origin of HTTP requests, " +
|
||||||
|
"meaning an attacker can control where server-side requests go! ***"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentServer().create( dao, logger, enc_service, origin_url );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function _initExportService( db, callback )
|
function _initExportService( db, callback )
|
||||||
{
|
{
|
||||||
db.collection( 'quotes', function( err, collection )
|
db.collection( 'quotes', function( err, collection )
|
||||||
|
|
|
@ -521,6 +521,38 @@ module.exports = Class( 'MongoServerDao' )
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save document metadata (meta field on document)
|
||||||
|
*
|
||||||
|
* Only the provided indexes will be modified (that is---data will be
|
||||||
|
* merged with what is already in the database).
|
||||||
|
*
|
||||||
|
* @param {Quote} quote destination quote
|
||||||
|
* @param {Object} new_meta bucket-formatted data to write
|
||||||
|
* @param {Function} success callback on success
|
||||||
|
* @param {Function} failure callback on error
|
||||||
|
*
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
'public saveQuoteMeta'( quote, new_meta, success, failure )
|
||||||
|
{
|
||||||
|
const update = {};
|
||||||
|
|
||||||
|
for ( var key in new_meta )
|
||||||
|
{
|
||||||
|
var meta = new_meta[ key ];
|
||||||
|
|
||||||
|
for ( var i in meta )
|
||||||
|
{
|
||||||
|
update[ 'meta.' + key + '.' + i ] =
|
||||||
|
new_meta[ key ][ i ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mergeData( quote, update, success, failure );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the quote lock state to the database
|
* Saves the quote lock state to the database
|
||||||
*
|
*
|
||||||
|
|
|
@ -71,6 +71,13 @@ module.exports = Class( 'DapiMetaSource',
|
||||||
data,
|
data,
|
||||||
( err, api_data ) =>
|
( err, api_data ) =>
|
||||||
{
|
{
|
||||||
|
if ( api_data.length > 1 )
|
||||||
|
{
|
||||||
|
reject( Error(
|
||||||
|
"Data API request produced more than one result"
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
dapi_manager.setFieldData(
|
dapi_manager.setFieldData(
|
||||||
dapi.name,
|
dapi.name,
|
||||||
index,
|
index,
|
||||||
|
|
|
@ -0,0 +1,260 @@
|
||||||
|
/**
|
||||||
|
* Manages DataAPI requests and return data
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework.
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Class } = require( 'easejs' );
|
||||||
|
|
||||||
|
const { QuoteDataBucket } = require( '../../' ).bucket;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process data provided by the client
|
||||||
|
*
|
||||||
|
* TOOD: This contains Data API and bucket merging logic that is better done
|
||||||
|
* elsewhere.
|
||||||
|
*/
|
||||||
|
module.exports = Class( 'DataProcessor',
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bucket filter
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _filter': null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct Data API manager
|
||||||
|
* @type {function()}
|
||||||
|
*/
|
||||||
|
'private _dapif': null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata source
|
||||||
|
* @type {DapiMetaSource}
|
||||||
|
*/
|
||||||
|
'private _metaSource': null,
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize processor
|
||||||
|
*
|
||||||
|
* @type {Object} filter bucket filter
|
||||||
|
* @type {function()} dapif data API constructor
|
||||||
|
* @type {DapiMetaSource} meta_source metadata source
|
||||||
|
*/
|
||||||
|
constructor( filter, dapif, meta_source )
|
||||||
|
{
|
||||||
|
this._filter = filter;
|
||||||
|
this._dapif = dapif;
|
||||||
|
this._metaSource = meta_source;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process client-provided data diff
|
||||||
|
*
|
||||||
|
* This performs sanitization to ensure that we are storing only
|
||||||
|
* "correct" data within our database. This also strips any unknown
|
||||||
|
* bucket values, preventing users from using us as their own personal
|
||||||
|
* database.
|
||||||
|
*
|
||||||
|
* @param {Object} data bucket diff data
|
||||||
|
* @param {UserRequest} request submitting request
|
||||||
|
* @param {Program} program active program
|
||||||
|
*
|
||||||
|
* @return {Object} processed diff
|
||||||
|
*/
|
||||||
|
'public processDiff'( data, request, program, bucket )
|
||||||
|
{
|
||||||
|
const filtered = this.sanitizeDiff( data, request, program, false );
|
||||||
|
const dapi_manager = this._dapif( program.apis, request );
|
||||||
|
|
||||||
|
// array of promises for any dapi requests
|
||||||
|
const dapis = this._triggerDapis(
|
||||||
|
dapi_manager, program, data, bucket
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filtered: filtered,
|
||||||
|
dapis: dapis,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize client-provided data
|
||||||
|
*
|
||||||
|
* Internal fields will be stripped if the session is not
|
||||||
|
* internal. Following that, the filter provided via the ctor will be
|
||||||
|
* applied.
|
||||||
|
*
|
||||||
|
* `permit_null` should be used only in the case of bucket diffs, which
|
||||||
|
* contain nulls as terminators.
|
||||||
|
*
|
||||||
|
* @param {Object} data client-provided data
|
||||||
|
* @param {UserRequest} request client request
|
||||||
|
* @param {Program} program active program
|
||||||
|
* @param {boolean} permit_null whether null values should be retained
|
||||||
|
*
|
||||||
|
* @return {Object} filtered data
|
||||||
|
*/
|
||||||
|
'public sanitizeDiff'( data, request, program, permit_null )
|
||||||
|
{
|
||||||
|
permit_null = ( permit_null === undefined ) ? false : permit_null;
|
||||||
|
|
||||||
|
if ( !request.getSession().isInternal() )
|
||||||
|
{
|
||||||
|
this._cleanInternals( data, program );
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = program.meta.qtypes;
|
||||||
|
return this._filter.filter( data, types, {}, permit_null );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip internal fields from diff `data`
|
||||||
|
*
|
||||||
|
* Internal fields are defined by the program `program`.
|
||||||
|
*
|
||||||
|
* @param {Object} data bucket diff data
|
||||||
|
* @param {Program} program active program
|
||||||
|
*
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
'private _cleanInternals'( data, program )
|
||||||
|
{
|
||||||
|
for ( let id in program.internal )
|
||||||
|
{
|
||||||
|
delete data[ id ];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger metadata Data API requests
|
||||||
|
*
|
||||||
|
* @param {DataApiManager} dapi_manager dapi manager
|
||||||
|
* @param {Program} program active program
|
||||||
|
* @param {Object} data client-provided data
|
||||||
|
* @param {Bucket} bucket active bucket
|
||||||
|
*
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
'private _triggerDapis'( dapi_manager, program, data, bucket )
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
mapis = {},
|
||||||
|
meta: {
|
||||||
|
fields = {},
|
||||||
|
},
|
||||||
|
} = program;
|
||||||
|
|
||||||
|
const dapi_fields = this._determineDapiFields( mapis, data );
|
||||||
|
|
||||||
|
return Object.keys( dapi_fields ).map( field =>
|
||||||
|
{
|
||||||
|
const { dapi } = fields[ field ];
|
||||||
|
const indexes = dapi_fields[ field ];
|
||||||
|
|
||||||
|
return indexes.map( i =>
|
||||||
|
this._metaSource.getFieldData(
|
||||||
|
field,
|
||||||
|
i,
|
||||||
|
dapi_manager,
|
||||||
|
dapi,
|
||||||
|
this._mapDapiData( dapi, bucket, i, data )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} ).reduce( ( result, x ) => result.concat( x ), [] );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine which fields require a Data API to be triggered
|
||||||
|
*
|
||||||
|
* @param {Object} mapis metadata dapi descriptors
|
||||||
|
* @param {Object} data client-provided data
|
||||||
|
*
|
||||||
|
* @return {Object} fields with indexes in need of dapi calls
|
||||||
|
*/
|
||||||
|
'private _determineDapiFields'( mapis, data )
|
||||||
|
{
|
||||||
|
return Object.keys( mapis ).reduce(
|
||||||
|
( result, src_field ) =>
|
||||||
|
{
|
||||||
|
if ( data[ src_field ] === undefined )
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = mapis[ src_field ];
|
||||||
|
|
||||||
|
// get each index that changed
|
||||||
|
fields.forEach( field =>
|
||||||
|
{
|
||||||
|
result[ field ] = result[ field ] || [];
|
||||||
|
|
||||||
|
Object.keys( data[ src_field ] ).forEach( i =>
|
||||||
|
{
|
||||||
|
if ( data[ src_field ][ i ] !== undefined )
|
||||||
|
{
|
||||||
|
result[ field ][ i ] = i;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map data from bucket to dapi inputs
|
||||||
|
*
|
||||||
|
* @param {Object} dapi Data API descriptor
|
||||||
|
* @param {Bucket} bucket active (source) bucket
|
||||||
|
* @param {number} index field index
|
||||||
|
* @param {Object} diff_data client-provided data
|
||||||
|
*
|
||||||
|
* @return {Object} key/value dapi input data
|
||||||
|
*/
|
||||||
|
'private _mapDapiData'( dapi, bucket, index, diff_data )
|
||||||
|
{
|
||||||
|
const { mapsrc } = dapi;
|
||||||
|
|
||||||
|
return Object.keys( mapsrc ).reduce(
|
||||||
|
( result, srcid ) =>
|
||||||
|
{
|
||||||
|
const bucketid = mapsrc[ srcid ];
|
||||||
|
|
||||||
|
const bdata = ( diff_data[ bucketid ] || [] )[ index ] ||
|
||||||
|
( bucket.getDataByName( bucketid ) || [] )[ index ];
|
||||||
|
|
||||||
|
result[ srcid ] = bdata || [];
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} );
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* Instantiate appropriate DataApi
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework.
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { Class } = require( 'easejs' );
|
||||||
|
const {
|
||||||
|
DataApiFactory,
|
||||||
|
http: {
|
||||||
|
NodeHttpImpl,
|
||||||
|
SpoofedNodeHttpImpl,
|
||||||
|
},
|
||||||
|
} = require( '../..' ).dapi;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the appropriate DataApi object for the given service type
|
||||||
|
*/
|
||||||
|
module.exports = Class( 'ServerDataApiFactory' )
|
||||||
|
.extend( DataApiFactory,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Origin URL
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
'private _origin': '',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request on behalf of user session
|
||||||
|
* @type {UserSession}
|
||||||
|
*/
|
||||||
|
'private _session': null,
|
||||||
|
|
||||||
|
|
||||||
|
constructor( origin, session )
|
||||||
|
{
|
||||||
|
this._origin = ''+origin;
|
||||||
|
this._session = session;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
'override protected createHttpImpl'()
|
||||||
|
{
|
||||||
|
return NodeHttpImpl.use( SpoofedNodeHttpImpl( this._session ) )(
|
||||||
|
{
|
||||||
|
http: require( 'http' ),
|
||||||
|
https: require( 'https' ),
|
||||||
|
},
|
||||||
|
require( 'url' ),
|
||||||
|
this._origin
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} );
|
|
@ -551,6 +551,22 @@ module.exports = Class.extend( require( 'events' ).EventEmitter,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
'public getHostAddr': function()
|
||||||
|
{
|
||||||
|
return this.request.headers['x-forwarded-host']
|
||||||
|
|| this.request.headers.host;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
'public getOrigin': function()
|
||||||
|
{
|
||||||
|
const referrer = this.request.headers.referrer || "";
|
||||||
|
|
||||||
|
return this.request.headers.origin
|
||||||
|
|| ( referrer.match( '^[a-z]+://[^/]+' ) || [] )[ 0 ];
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
'public getUserAgent': function()
|
'public getUserAgent': function()
|
||||||
{
|
{
|
||||||
return this.request.headers['user-agent'];
|
return this.request.headers['user-agent'];
|
||||||
|
|
|
@ -110,14 +110,27 @@ describe( "DapiMetaSource", () =>
|
||||||
failc( e );
|
failc( e );
|
||||||
};
|
};
|
||||||
|
|
||||||
return Sut( () => getStubBucket() )
|
return expect(
|
||||||
.getFieldData( 'name', 0, dapim, {}, {} )
|
Sut( () => getStubBucket() )
|
||||||
.catch( given_e =>
|
.getFieldData( 'name', 0, dapim, {}, {} )
|
||||||
{
|
).to.eventually.be.rejectedWith( e );
|
||||||
expect( given_e ).to.equal( e );
|
} );
|
||||||
|
|
||||||
return true;
|
|
||||||
} );
|
it( "rejects if more than one result is returned from dapi", () =>
|
||||||
|
{
|
||||||
|
const dapim = createStubDapiManager();
|
||||||
|
|
||||||
|
dapim.getApiData = ( _, __, callback ) =>
|
||||||
|
{
|
||||||
|
// more than one result
|
||||||
|
callback( null, [ {}, {} ] );
|
||||||
|
};
|
||||||
|
|
||||||
|
return expect(
|
||||||
|
Sut( () => getStubBucket() )
|
||||||
|
.getFieldData( 'name', 0, dapim, {}, {} )
|
||||||
|
).to.eventually.be.rejectedWith( Error );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,307 @@
|
||||||
|
/**
|
||||||
|
* Manages DataAPI requests and return data
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework.
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Class } = require( 'easejs' );
|
||||||
|
const { expect } = require( 'chai' );
|
||||||
|
const Sut = require( '../../../' ).server.request.DataProcessor;
|
||||||
|
|
||||||
|
|
||||||
|
describe( 'DataProcessor', () =>
|
||||||
|
{
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: "strips internal field data when not internal",
|
||||||
|
data: {
|
||||||
|
internal: [ "foo", "bar" ],
|
||||||
|
foo: [ "bar", "baz" ],
|
||||||
|
},
|
||||||
|
internals: { internal: true },
|
||||||
|
internal: false,
|
||||||
|
expected: {
|
||||||
|
foo: [ "bar", "baz" ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "keeps internal field data when internal",
|
||||||
|
data: {
|
||||||
|
internal: [ "foo", "bar" ],
|
||||||
|
foo: [ "bar", "baz" ],
|
||||||
|
},
|
||||||
|
internals: { internal: true },
|
||||||
|
internal: true,
|
||||||
|
expected: {
|
||||||
|
internal: [ "foo", "bar" ],
|
||||||
|
foo: [ "bar", "baz" ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].forEach( ( { label, internal, data, internals = {}, expected } ) =>
|
||||||
|
{
|
||||||
|
const { request, program, sut } =
|
||||||
|
createSutFromStubs( internal, internals );
|
||||||
|
|
||||||
|
it( label, () =>
|
||||||
|
{
|
||||||
|
expect(
|
||||||
|
sut.processDiff( data, request, program ).filtered
|
||||||
|
).to.deep.equal( expected );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "passes data to bucket filter", () =>
|
||||||
|
{
|
||||||
|
const { request, program, meta_source } = createStubs();
|
||||||
|
const data = {};
|
||||||
|
const types = {};
|
||||||
|
|
||||||
|
program.meta.qtypes = types;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
filter( given_data, given_types, given_ignore, given_null )
|
||||||
|
{
|
||||||
|
expect( given_data ).to.equal( data );
|
||||||
|
expect( given_types ).to.equal( types );
|
||||||
|
expect( given_null ).to.equal( false );
|
||||||
|
|
||||||
|
// not used
|
||||||
|
expect( given_ignore ).to.deep.equal( {} );
|
||||||
|
|
||||||
|
data.filtered = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Sut( filter, () => {}, meta_source )
|
||||||
|
.processDiff( data, request, program );
|
||||||
|
|
||||||
|
expect( data.filtered ).to.equal( true );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "instantiates dapi manager using program and session", done =>
|
||||||
|
{
|
||||||
|
const { filter, request, program } = createStubs();
|
||||||
|
|
||||||
|
const dapi_factory = ( given_apis, given_request ) =>
|
||||||
|
{
|
||||||
|
expect( given_apis ).to.equal( program.apis );
|
||||||
|
expect( given_request ).to.equal( request );
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
Sut( filter, dapi_factory )
|
||||||
|
.processDiff( {}, request, program );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "invokes dapi manager when monitored bucket value changes", () =>
|
||||||
|
{
|
||||||
|
const triggered = {};
|
||||||
|
|
||||||
|
// g prefix = "given"
|
||||||
|
const getFieldData = function( gfield, gindex, gdapim, gdapi, gdata)
|
||||||
|
{
|
||||||
|
triggered[ gdapi.name ] = triggered[ gdapi.name ] || [];
|
||||||
|
triggered[ gdapi.name ][ gindex ] = arguments;
|
||||||
|
|
||||||
|
return Promise.resolve( true );
|
||||||
|
};
|
||||||
|
|
||||||
|
const dapi_manager = {};
|
||||||
|
|
||||||
|
const {
|
||||||
|
request,
|
||||||
|
program,
|
||||||
|
filter,
|
||||||
|
meta_source,
|
||||||
|
} = createStubs( false, {}, getFieldData );
|
||||||
|
|
||||||
|
const sut = Sut( filter, () => dapi_manager, meta_source );
|
||||||
|
|
||||||
|
program.meta.fields = {
|
||||||
|
foo: {
|
||||||
|
dapi: {
|
||||||
|
name: 'dapi_foo',
|
||||||
|
mapsrc: { ina: 'src', inb: 'src1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
dapi: {
|
||||||
|
name: 'dapi_bar',
|
||||||
|
mapsrc: { ina: 'src1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
baz: {
|
||||||
|
dapi: {
|
||||||
|
name: 'dapi_no_call',
|
||||||
|
mapsrc: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
program.mapis = {
|
||||||
|
src: [ 'foo', 'bar' ], // change
|
||||||
|
src1: [ 'foo' ], // change
|
||||||
|
src2: [ 'baz' ], // do not change
|
||||||
|
};
|
||||||
|
|
||||||
|
// data changed
|
||||||
|
const data = {
|
||||||
|
src: [ 'src0', 'src1' ],
|
||||||
|
src1: [ undefined, 'src11' ],
|
||||||
|
};
|
||||||
|
|
||||||
|
const bucket = createStubBucket( {
|
||||||
|
src: [ 'bsrc0', 'bsrc1' ],
|
||||||
|
src1: [ 'bsrc10', 'bsrc11' ],
|
||||||
|
} );
|
||||||
|
|
||||||
|
const { dapis } = sut.processDiff( data, request, program, bucket );
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
dapi_foo: [
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
data: {
|
||||||
|
ina: data.src[ 0 ],
|
||||||
|
inb: bucket.data.src1[ 0 ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'foo',
|
||||||
|
data: {
|
||||||
|
ina: data.src[ 1 ],
|
||||||
|
inb: data.src1[ 1 ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dapi_bar: [
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'bar',
|
||||||
|
data: {
|
||||||
|
ina: data.src1[ 1 ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
for ( let dapi_name in expected )
|
||||||
|
{
|
||||||
|
let expected_call = expected[ dapi_name ];
|
||||||
|
|
||||||
|
for ( let i in expected_call )
|
||||||
|
{
|
||||||
|
let chk = expected_call[ i ];
|
||||||
|
|
||||||
|
if ( chk === undefined )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [ gfield, gindex, gdapi_manager, gdapi, gdata ] =
|
||||||
|
triggered[ dapi_name ][ i ];
|
||||||
|
|
||||||
|
expect( gfield ).to.equal( chk.name );
|
||||||
|
expect( gdapi.name ).to.equal( dapi_name );
|
||||||
|
expect( +gindex ).to.equal( +i );
|
||||||
|
expect( gdapi_manager ).to.equal( dapi_manager );
|
||||||
|
|
||||||
|
// see mapsrc
|
||||||
|
expect( gdata ).to.deep.equal( chk.data );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect( triggered.dapi_no_call ).to.equal( undefined );
|
||||||
|
|
||||||
|
return Promise.all( dapis );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
function createSutFromStubs( /* see createStubs */ )
|
||||||
|
{
|
||||||
|
const { request, program, filter, meta_source } =
|
||||||
|
createStubs.apply( null, arguments );
|
||||||
|
|
||||||
|
return {
|
||||||
|
request: request,
|
||||||
|
program: program,
|
||||||
|
filter: filter,
|
||||||
|
meta_source: meta_source,
|
||||||
|
sut: Sut( filter, () => {}, meta_source ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createStubs( internal, internals, getFieldData )
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
request: createStubUserRequest( internal || false ),
|
||||||
|
program: createStubProgram( internals || {} ),
|
||||||
|
filter: { filter: _ => _ },
|
||||||
|
meta_source: createStubDapiMetaSource( getFieldData ),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createStubUserRequest( internal )
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
getSession: () => ( {
|
||||||
|
isInternal: () => internal
|
||||||
|
} )
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createStubProgram( internals )
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
internal: internals,
|
||||||
|
meta: { qtypes: {}, fields: {} },
|
||||||
|
apis: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createStubDapiMetaSource( getFieldData )
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
getFieldData: getFieldData ||
|
||||||
|
function( field, index, dapi_manager, dapi, data ){},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createStubBucket( data )
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
|
||||||
|
getDataByName( name )
|
||||||
|
{
|
||||||
|
return data[ name ];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue