Skip to content

Commit

Permalink
handle data association with filter (#152)
Browse files Browse the repository at this point in the history
* handle data association with filter

* 2.6.50
  • Loading branch information
kbarbounakis authored Jul 11, 2024
1 parent bb98840 commit 6231278
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 3 deletions.
146 changes: 146 additions & 0 deletions OnNestedQueryOptionsListener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
const { QueryExpression } = require('@themost/query');
// eslint-disable-next-line no-unused-vars
const { DataEventArgs } = require('./types');
const { instanceOf } = require('./instance-of');
require('@themost/promise-sequence');


/**
* @implements {BeforeExecuteEventListener}
*/
class OnNestedQueryOptionsListener {
/**
*
* @param {DataEventArgs} event
* @param {function} callback
*/
beforeExecute(event, callback) {
OnNestedQueryOptionsListener.prototype.beforeExecuteAsync(event).then(function () {
return callback();
}).catch(function (err) {
return callback(err);
});
}

beforeExecuteAsync(event) {
const query = event.emitter && event.emitter.query;
const context = event.model.context;
if (query == null) {
return Promise.resolve();
}
// handle only select statements
if (query.$select == null) {
return Promise.resolve();
}
if (Object.prototype.hasOwnProperty.call(query, '$expand')) {
// exit if expand is null or undefined
if (query.$expand == null) {
return Promise.resolve();
}
/**
* @type {Array<{ $entity:{ model:string }}>}
*/
const expand = Array.isArray(query.$expand) ? query.$expand : [query.$expand];
if (expand.length) {
const sources = expand.map(function (item) {
return function () {
// if entity is already a query expression
if (instanceOf(item.$entity, QueryExpression)) {
// do nothing
return Promise.resolve();
}
if (item.$entity && item.$entity.model) {
// get entity alias (which is a field of current model)
let options = null;
if (item.$entity.$as != null) {
// get current model
let currentModel = event.model;
/**
* get attribute by name e.g. products.getAttributes('productionDescription')
* @type {DataField}
*/
let attribute = currentModel.getAttribute(item.$entity.$as);
if (attribute == null) {
if (item.$with) {
// find another join by name
const [key] = Object.keys(item.$with);
if (key) {
// get name e.g. orderedItem.id => orderedItem
const [name] = key.split('.');
if (name) {
// find expand by name (which is join alias)
// e.g. expand.find(x => x.$entity.$as === 'orderedItem')
const findExpand = expand.find((x) => x.$entity.$as === name);
if (findExpand) {
// get model by name
// e.g. context.model('Producn')
currentModel = context.model(findExpand.$entity.model);
if (currentModel) {
// get attribute by name
// e.g. products.getAttributes('productionDescription')
attribute = currentModel.getAttribute(item.$entity.$as);
}
}
}
}
}
if (attribute == null) {
return Promise.resolve();
}
}
const mapping = currentModel.inferMapping(attribute.name);
if (mapping == null) {
return Promise.resolve();
}
options = mapping.options;
if (options == null) {
return Promise.resolve();
}
if (options.$filter == null) {
return Promise.resolve();
}
}
/**
* @type {import('./data-model').DataModel}
*/
const nestedModel = context.model(item.$entity.model);
// if model exists
if (nestedModel != null) {
if (item.$entity.$as != null) {
// change view to follow entity alias defined by the current query
// this operation will be used while parsing filter and creating a new query expression
nestedModel.view = item.$entity.$as;
}
return nestedModel.filterAsync(options).then((q) => {
/**
* @typedef {object} QueryExpressionWithPrepared
* @property {*} $prepared
*/
/** @type {QueryExpressionWithPrepared} */
const { query } = q;
if (query && query.$prepared) {
item.$with = {
$and: [
item.$with,
query.$prepared
]
}
}
return Promise.resolve();
});
}
}
return Promise.resolve();
}
});
return Promise.sequence(sources);
}
}
return Promise.resolve();
}

}

module.exports = {
OnNestedQueryOptionsListener
}
1 change: 1 addition & 0 deletions data-model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export declare class DataModel extends SequentialEventEmitter{
search(text: string): DataQueryable;
asQueryable(): DataQueryable;
filter(params: any, callback?: (err?: Error, res?: any) => void): void;
filterAsync(params: any): Promise<DataQueryable>;
find(obj: any):DataQueryable;
select(...attr: any[]): DataQueryable;
orderBy(attr: any): DataQueryable;
Expand Down
6 changes: 6 additions & 0 deletions data-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var {DataPermissionEventListener} = require('./data-permission');
var {DataField} = require('./types');
var {ZeroOrOneMultiplicityListener} = require('./zero-or-one-multiplicity');
var {OnNestedQueryListener} = require('./OnNestedQueryListener');
var {OnNestedQueryOptionsListener} = require('./OnNestedQueryOptionsListener');
var {OnExecuteNestedQueryable} = require('./OnExecuteNestedQueryable');
var {hasOwnProperty} = require('./has-own-property');
var {SyncSeriesEventEmitter} = require('@themost/events');
Expand Down Expand Up @@ -624,6 +625,7 @@ function unregisterContextListeners() {
this.on('before.execute', DataCachingListener.prototype.beforeExecute);
}
this.on('before.execute', OnExecuteNestedQueryable.prototype.beforeExecute);
this.on('before.execute', OnNestedQueryOptionsListener.prototype.beforeExecute);
this.on('before.execute', OnNestedQueryListener.prototype.beforeExecute);
//register after execute caching
if (this.caching==='always' || this.caching==='conditional') {
Expand Down Expand Up @@ -920,6 +922,10 @@ DataModel.prototype.filter = function(params, callback) {
}
};

DataModel.prototype.filterAsync = function(params) {
return this.filter(params);
};

/**
* Prepares a data query with the given object as parameters and returns the equivalent DataQueryable instance
* @param {*} obj - An object which represents the query parameters
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.49",
"version": "2.6.50",
"description": "MOST Web Framework Codename Blueshift - Data module",
"main": "index.js",
"types": "index.d.ts",
Expand Down
75 changes: 75 additions & 0 deletions spec/ZeroOneMultiplicy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ import { DataContext } from '../index';
import { TestApplication } from './TestApplication';
import {TestUtils} from "./adapter/TestUtils";

declare interface DataContextWithCulture extends DataContext {
culture(value?: string): string;
}

Object.assign(DataContext.prototype, {
culture(value: string) {
if (typeof value === 'undefined') {
return this._culture || 'en';
}
this._culture = value;
return this._culture;
}
})


fdescribe('ZeroOrOneMultiplicity', () => {
let app: TestApplication;
let context: DataContext;
Expand Down Expand Up @@ -38,6 +53,66 @@ fdescribe('ZeroOrOneMultiplicity', () => {
});
});

it('should use $filter expression', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product')
let product = await Products.where('name').equal('Samsung Galaxy S4').getItem();
expect(product).toBeTruthy();
const productDescriptions = [{
description: 'This is a new product description',
inLanguage: 'en'
}, {
description: 'Ceci est une nouvelle description de produit',
inLanguage: 'fr'
}];
await Products.silent().save(Object.assign(product, {
productDescriptions
}));
(context as DataContextWithCulture).culture('fr');
product = await Products.where('name').equal('Samsung Galaxy S4').expand('productDescription').getItem();
expect(product).toBeTruthy();
expect(product.productDescription).toBeTruthy();
expect(product.productDescription.description).toEqual('Ceci est une nouvelle description de produit');
});
});

it('should use $filter expression with nested select', async () => {
await TestUtils.executeInTransaction(context, async () => {
const Products = context.model('Product')
let product = await Products.where('name').equal('Samsung Galaxy S4').getItem();
expect(product).toBeTruthy();
const productDescriptions = [{
description: 'This is a new product description',
inLanguage: 'en'
}, {
description: 'Ceci est une nouvelle description de produit',
inLanguage: 'fr'
}];
await Products.silent().save(Object.assign(product, {
productDescriptions
}));
const Orders = context.model('Order');
(context as DataContextWithCulture).culture('en');
let items = await Orders.select(
'id',
'orderedItem/name as product',
'orderedItem/productDescription/description as productDescription'
).where('orderedItem/name').equal('Samsung Galaxy S4').silent().getItems();
expect(items).toBeTruthy();
expect(items.length).toBeGreaterThan(0);
expect(items[0].productDescription).toEqual('This is a new product description');
(context as DataContextWithCulture).culture('fr');
items = await Orders.select(
'id',
'orderedItem/name as product',
'orderedItem/productDescription/description as productDescription'
).where('orderedItem/name').equal('Samsung Galaxy S4').silent().getItems();
expect(items).toBeTruthy();
expect(items.length).toBeGreaterThan(0);
expect(items[0].productDescription).toEqual('Ceci est une nouvelle description de produit');

});
});


});
30 changes: 30 additions & 0 deletions spec/test2/config/models/Product.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,36 @@
}
]
}
},
{
"name": "productDescription",
"type": "ProductDescription",
"readonly": true,
"many": true,
"multiplicity": "ZeroOrOne",
"mapping": {
"parentModel": "Product",
"parentField": "id",
"childModel": "ProductDescription",
"childField": "product",
"associationType": "association",
"options": {
"$filter" : "inLanguage eq lang()"
}
}
},
{
"name": "productDescriptions",
"type": "ProductDescription",
"nested": true,
"mapping": {
"parentModel": "Product",
"parentField": "id",
"childModel": "ProductDescription",
"childField": "product",
"associationType": "association",
"cascade": "delete"
}
}
],
"constraints": [
Expand Down
33 changes: 33 additions & 0 deletions spec/test2/config/models/ProductDescription.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "https://themost-framework.github.io/themost/models/2018/2/schema.json",
"name": "ProductDescription",
"title": "ProductDescription",
"version": "2.0",
"inherits": "StructuredValue",
"fields": [
{
"name": "description",
"type": "Note"
},
{
"name": "inLanguage",
"type": "Text",
"size": 5,
"nullable": false
},
{
"name": "product",
"type": "Product",
"nullable": false
}
],
"constraints": [
{
"type": "unique",
"fields": [
"product",
"inLanguage"
]
}
]
}

0 comments on commit 6231278

Please sign in to comment.