Friends of Cake search: hasmany associations - search

I seem to recall far better documentation the last time I used it. I need to be able to search across multiple tables, finding Locations which have ShippingAddresses that match search criteria. Here are my Location model showing the association between the Addresses table and Locations table:
class LocationsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('locations');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
$this->addBehavior('Search.Search');
// Stores:
$this->belongsTo('Stores', [
'foreignKey' => 'store_id',
]);
// Shipping addresses:
$this->hasMany('ShippingAddresses', [
'foreignKey' => 'location_id',
'className' => 'Addresses',
'dependent' => true,
])->setConditions(['type' => 'shipping']);
// Billing addresses
$this->hasMany('BillingAddresses', [
'foreignKey' => 'location_id',
'className' => 'Addresses',
'dependent' => true,
])->setConditions(['type' => 'billing']);
// Deliverables
$this->belongsToMany('Deliverables', [
'through' => 'LocationsDeliverables',
]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator): Validator
{
$validator
->integer('id')
->allowEmptyString('id', null, 'create');
$validator
->scalar('name')
->maxLength('name', 255)
->requirePresence('name', 'create')
->notEmptyString('name');
$validator
->scalar('identifier')
->maxLength('identifier', 255)
->allowEmptyString('identifier');
$validator
->scalar('description')
->allowEmptyString('description');
return $validator;
}
/**
* Returns a summarized set of important data.
*
* #param \Cake\ORM\Query $query The current query
* #param array $options An array of options.
* #return \Cake\ORM\Query
*/
public function findSummary(Query $query, array $options): Query
{
// do things.
return $query
->where(['Locations.id' => $options['location_id']])
->contain(
'Deliverables',
function (Query $q) {
return $q
->select(['id', 'parent_id', 'name', 'description'])
->where(['LocationsDeliverables.default_quantity >' => 0]);
}
)
->contain(['ShippingAddresses', 'Stores']);
}
}
Here is the search function call in context in the Locations controller:
/**
* Index method
*
* #return \Cake\Http\Response|null|void Renders view
*/
public function index()
{
$this->Authorization->skipAuthorization();
$locations = $this->paginate($this->Locations, [
'contain' => ['Stores', 'ShippingAddresses'],
'finder' => [
'search' => ['search' => $this->request->getQueryParams()],
],
]);
$this->set(compact('locations'));
}
According to the information provided in this comment, you should just be able to add the additional ShippingAddress columns to the search collection in it's dot-noted version. But of course, hasMany associations are queried by CakePHP as a separate operation, so doing so fails:
class LocationsCollection extends FilterCollection
{
/**
* #return void
*/
public function initialize(): void
{
$this->like('name')
->value('identifier')
->value('store_id')
->add('q', 'Search.Like', [
'before' => true,
'after' => true,
'fieldMode' => 'OR',
'comparison' => 'LIKE',
'wildcardAny' => '*',
'wildcardOne' => '?',
'fields' => ['name', 'description', 'Stores.store_name', 'ShippingAddresses.street_1'],
]);
}
}
Using this search, I get the error message "Column not found: 1054 Unknown column 'ShippingAddresses.street_1' in 'where clause'". That makes sense, because that's not how hasMany associations work. But then, how DO I query this data?
Thanks for your help!

Related

How to import products with variations in Shopware 6

I'm trying to import products from an XML with variations.
The import for the products works so far but it doesn't create the variations.
Here is my code (simplified):
/**
* #return int
* #throws \Exception
*/
public function execute()
{
// avoid reaching memory limit
ini_set('memory_limit', '-1');
// set tax id
$this->setTaxId();
if (empty($this->taxId)) {
return 1;
}
// read products from import xml file
$importProducts = $this->loadProducts();
$csvBatch = array_chunk($importProducts, self::BATCH);
$productNumbers = [];
foreach ($csvBatch as $products) {
$productNumbers[] = $this->processImportProducts($products, false);
}
$this->deleteProducts(array_merge(...$productNumbers));
return 0;
}
/**
* #param $productsData
* #param $progressBar
* #return array
*/
private function processImportProducts($productsData, $progressBar)
{
$products = [];
$productNumbers = [];
foreach ($productsData as $product) {
$products[$product['SKU']['#cdata']] = $this->importProducts($product, $progressBar);
$productNumbers[] = $product['SKU']['#cdata'];
}
// upsert product
try {
$this->cleanProductProperties($products, $this->context);
$this->productRepository->upsert(array_values($products), $this->context);
} catch (WriteException $exception) {
$this->logger->info(' ');
$this->logger->info('<error>Products could not be imported. Message: '. $exception->getMessage() .'</error>');
}
unset($products);
return $productNumbers;
}
/**
* #param $product
* #param $progressBar
* #return array
*/
private function importProducts($product, $progressBar)
{
...
$productData = [
'id' => $productId,
'productNumber' => $productNumber,
'price' => [
[
'currencyId' => Defaults::CURRENCY,
'net' => !empty($product['net']) ? $product['net'] : 0,
'gross' => !empty($product['net']) ? $product['net'] : 0,
'linked' => true
]
],
'stock' => 99999,
'unit' => [
'id' => '3fff95a8077b4f5ba3d1d2a41cb53fab'
],
'unitId' => '3fff95a8077b4f5ba3d1d2a41cb53fab',
'taxId' => $this->taxId,
'name' => $productNames,
'description' => $productDescriptions
];
if(isset($product['Variations'])) {
$variationIds = $product['Variations']['#cdata'] ?? '';
$productData['variation'] = [$this->getProductVariationIds($variationIds)];
}
return $productData;
}
/**
* Get product variation ids
*
* #param string $productVariations
* #return string
*/
private function getProductVariationIds($productVariations)
{
$productVariationIds = explode(',', $productVariations);
// get product variationIds in form of a string list
$ids = $this->productRepository->search(
(new Criteria())->addFilter(new EqualsAnyFilter('productNumber', $productVariationIds)),
$this->context
)->getIds();
return implode(',', $ids);
}
It loads correctly the ids but nothing happen. Also no error.
Anyone an idea how to import variations as well?
The variation field is not meant to be persisted or to create variants of a product. It has the Runtime flag, meaning it's not an actual database column but processed during runtime.
You have to create/update variants just like you create the parent product. Additionally you have to set the parentId and the options. The latter being associations to property_group_option, which you'll have to create first.
So in addition to your existing payload when creating parent products, you'll have to add this data to the variants:
$productData = [
// ...
'parentId' => '...'
'options' => [
['id' => '...'],
['id' => '...'],
['id' => '...'],
// ...
],
];
Finally you'll have to create the product_configurator_setting records. That's one record for each option used across all variants. Also the productId for the records has to be the one of the parent product.
$repository = $this->container->get('product_configurator_setting.repository');
$configuratorSettings = [];
foreach ($options as $option) {
$configuratorSetting = [
'optionId' => $option['id'],
'productId' => $parentId,
];
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('productId', $parentId));
$criteria->addFilter(new EqualsFilter('optionId', $option['id']));
$id = $repository->searchIds($criteria, $context)->firstId();
// if the configurator setting already exists, update or skip
if ($id) {
$configuratorSetting['id'] = $id;
}
$configuratorSettings[] = $configuratorSetting;
}
$repository->upsert(configuratorSettings, $context);
Just as an addition to make things easier. When creating a product with variants you can just update the configuratorSettings of the parent/father/main-product (whatever you call it).
Then Shopware6 will go and create the variant products automatically. Also the uuids of the children are created automatically. So if need to keep track of these you have to query them after the creation process.
But for a fast creation this might be much faster, if you have a lot of variants the only "variation" are the options. So no special images or texts.

error in codeigniter 4 You must set the database table to be used with your query

i am working codeigniter 4. i am trying to fetch data from database by creating method in Model but am getting error
You must set the database table to be used with your query
even I have mentioned the table name in query builder. i dun know why this is happening ? following is my code
public function Login($values)
{
$db = \Config\Database::connect();
$result= $db->table('tbl_adminuser')
->where(['username',$values['username']])
->where(['password',$values['password']])
->get()
->getResult();
print_r($result);
}
There are two types of getting data from the database.
Using the Model file
Load db in the controller file
Check both the below types to get data from the database.
1. Using the Model file
First, create a User model and mention the name of the database table.
Here is the sample of model file:
<?php
namespace App\Models;
use CodeIgniter\Model;
class UserMasterModel extends Model
{
protected $table = 'user_master';
}
Then create a controller file and load the model using use App\Models\UserMasterModel;.
You should following the below sample of the controller file there is mention load the model and get the data using a model file.
<?php
namespace App\Controllers;
use CodeIgniter\Controller;
use App\Models\UserMasterModel;
public function __construct()
{
$this->db = \Config\Database::connect();
}
public function Login($values){
$UserMasterModel = new UserMasterModel();
$result = $UserMasterModel->where('username',$values['username'])
->where('password',$values['password'])
->findAll();
print_r($result);
}
2. Load db in controller file
When using \Config\Database::connect() the controller file looks like below:
<?php
namespace App\Controllers;
use CodeIgniter\Controller;
public function __construct()
{
$this->db = \Config\Database::connect();
$this->user_master = $this->db->table('user_master');
}
public function Login($values){
$this->user_master->select('*');
$this->user_master->where('username',$values['username']);
$this->user_master->where('password',$values['password']);
$result = $this->user_master->get()->getResult();
print_r($result);
}
create model for users
<?php namespace Myth\Auth\Models;
use CodeIgniter\Model;
use Myth\Auth\Authorization\GroupModel;
use Myth\Auth\Entities\User;
class UserModel extends Model
{
protected $table = 'users';
protected $primaryKey = 'id';
protected $returnType = User::class;
protected $useSoftDeletes = false;
protected $allowedFields = [
'email', 'username', 'password_hash', 'reset_hash', 'reset_at', 'reset_expires', 'activate_hash',
'status', 'status_message', 'active', 'force_pass_reset', 'permissions',
'first_name',
'last_name',
'image',
'address',
'phone',
'email',
'gender',
'country',
'city',
'created_at',
'updated_at',
'deleted_at',
];
protected $useTimestamps = false;
protected $validationRules = [
'email' => 'if_exist|required|valid_email|is_unique[users.email,id,{id}]',
'phone' => 'if_exist|required|is_unique[users.phone,id,{id}]',
'username' => 'if_exist|required|alpha_numeric_punct|min_length[3]|max_length[30]|is_unique[users.username,id,{id}]',
'password_hash' => 'if_exist|required',
];
protected $validationMessages = [];
protected $skipValidation = false;
protected $afterInsert = ['addToGroup'];
/**
* The id of a group to assign.
* Set internally by withGroup.
*
* #var int|null
*/
protected $assignGroup;
/**
* Logs a password reset attempt for posterity sake.
*
* #param string $email
* #param string|null $token
* #param string|null $ipAddress
* #param string|null $userAgent
*/
public function logResetAttempt(string $email, string $token = null, string $ipAddress = null, string $userAgent = null)
{
$this->db->table('auth_reset_attempts')->insert([
'email' => $email,
'ip_address' => $ipAddress,
'user_agent' => $userAgent,
'token' => $token,
'created_at' => date('Y-m-d H:i:s')
]);
}
/**
* Logs an activation attempt for posterity sake.
*
* #param string|null $token
* #param string|null $ipAddress
* #param string|null $userAgent
*/
public function logActivationAttempt(string $token = null, string $ipAddress = null, string $userAgent = null)
{
$this->db->table('auth_activation_attempts')->insert([
'ip_address' => $ipAddress,
'user_agent' => $userAgent,
'token' => $token,
'created_at' => date('Y-m-d H:i:s')
]);
}
/**
* Sets the group to assign any users created.
*
* #param string $groupName
*
* #return $this
*/
public function withGroup(string $groupName)
{
$group = $this->db->table('auth_groups')->where('name', $groupName)->get()->getFirstRow();
$this->assignGroup = $group->id;
return $this;
}
/**
* Clears the group to assign to newly created users.
*
* #return $this
*/
public function clearGroup()
{
$this->assignGroup = null;
return $this;
}
/**
* If a default role is assigned in Config\Auth, will
* add this user to that group. Will do nothing
* if the group cannot be found.
*
* #param mixed $data
*
* #return mixed
*/
protected function addToGroup($data)
{
if (is_numeric($this->assignGroup)) {
$groupModel = model(GroupModel::class);
$groupModel->addUserToGroup($data['id'], $this->assignGroup);
}
return $data;
}
}
too use model
$model = new usermodel();
$model->asObject()->where('active', '1')->findAll();

GuzzleHttp Parallel Progress For Async Client in Azure and Flysystem

I would like to get the actual block progress and not the Progress of all the transfers. Currently i don't know how to detect the blockId of each individual transfer. The information on the progress callback im currently retrieving is pointless.
Here's the progress function, contained within ServiceRestProxy.php
Original Function https://github.com/Azure/azure-storage-php/blob/master/azure-storage-common/src/Common/Internal/ServiceRestProxy.php#L99
/**
* Create a Guzzle client for future usage.
*
* #param array $options Optional parameters for the client.
*
* #return Client
*/
private static function createClient(array $options)
{
$verify = true;
//Disable SSL if proxy has been set, and set the proxy in the client.
$proxy = getenv('HTTP_PROXY');
// For testing with Fiddler
// $proxy = 'localhost:8888';
// $verify = false;
if (!empty($proxy)) {
$options['proxy'] = $proxy;
}
if (!empty($options['verify'])) {
$verify = $options['verify'];
}
$downloadTotal = 0;
return (new \GuzzleHttp\Client(
array_merge(
$options,
array(
"defaults" => array(
"allow_redirects" => true,
"exceptions" => true,
"decode_content" => true,
),
'cookies' => true,
'verify' => $verify,
'progress' => function (
$downloadTotal,
$downloadedBytes,
$uploadTotal,
$uploadedBytes
){
// i need to detect which block the progress is for.
echo ("progress: download: {$downloadedBytes}/{$downloadTotal}, upload: {$uploadedBytes}/{$uploadTotal}");
}
)
)
));
}
I got a solution to get each block progress.
I needed to use the Async Function for this. Updated version.
/**
* Send the requests concurrently. Number of concurrency can be modified
* by inserting a new key/value pair with the key 'number_of_concurrency'
* into the $requestOptions of $serviceOptions. Return only the promise.
*
* #param callable $generator the generator function to generate
* request upon fulfillment
* #param int $statusCode The expected status code for each of the
* request generated by generator.
* #param ServiceOptions $options The service options for the concurrent
* requests.
*
* #return \GuzzleHttp\Promise\Promise|\GuzzleHttp\Promise\PromiseInterface
*/
protected function sendConcurrentAsync(
callable $generator,
$statusCode,
ServiceOptions $options
) {
$client = $this->client;
$middlewareStack = $this->createMiddlewareStack($options);
$progress = [];
$sendAsync = function ($request, $options) use ($client, $progress) {
if ($request->getMethod() == 'HEAD') {
$options['decode_content'] = false;
}
$options["progress"] = function(
$downloadTotal,
$downloadedBytes,
$uploadTotal,
$uploadedBytes) use($request, $progress){
// extract blockid from url
$url = $request->getUri()->getQuery();
parse_str($url, $array);
// this array can be written to file or session etc
$progress[$array["blockid"]] = ["download_total" => $downloadTotal, "downloaded_bytes" => $downloadedBytes, "upload_total" => $uploadTotal, "uploaded_bytes" => $uploadedBytes];
};
return $client->sendAsync($request, $options);
};
$handler = $middlewareStack->apply($sendAsync);
$requestOptions = $this->generateRequestOptions($options, $handler);
$promises = \call_user_func(
function () use (
$generator,
$handler,
$requestOptions
) {
while (is_callable($generator) && ($request = $generator())) {
yield \call_user_func($handler, $request, $requestOptions);
}
}
);
$eachPromise = new EachPromise($promises, [
'concurrency' => $options->getNumberOfConcurrency(),
'fulfilled' => function ($response, $index) use ($statusCode) {
//the promise is fulfilled, evaluate the response
self::throwIfError(
$response,
$statusCode
);
},
'rejected' => function ($reason, $index) {
//Still rejected even if the retry logic has been applied.
//Throwing exception.
throw $reason;
}
]);
return $eachPromise->promise();
}

Undefined index: code use Export to Excel in Laravel 5.8

I'm executing Export, Import to Excel in Laravel. But I have an error
Undefined index: code
in a file AlumniImport.php.
Thank you for help!
AlumniImport.php
namespace App\Imports;
use App\Models\User;
use Maatwebsite\Excel\Concerns\ToModel;
use Illuminate\Support\Facades\Hash;
class AlumniImport implements ToModel
{
/**
* #param array $row
*
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
return new User([
'code' => $row["code"],
'first_name' => $row["first_name"],
'last_name' => $row["last_name"],
'username' => $row["username"],
'password' => Hash::make($row["password"]),
'tel' => $row["tel"],
'email' => $row["email"],
'gender' => $row["gender"],
'birthday' => $row["birthday"],
'address' => $row["address"],
'status_id' => $row["status_id"],
]);
}
}
AlumniController.php
// Excel
use App\Imports\AlumniImport;
use App\Exports\AlumniExport;
use Excel;
class AlumniController extends Controller
{
public function import()
{
Excel::import(new AlumniImport,request()->file('file'));
return back();
}
}
Example of data in Excel:
code first_name last_name username password tel email gender birthday address status_id
B8888 John Smith johnsmith 123456 123456 johnsmith#gmail.com Male 4/9/1998 USA 1
B7777 Tom Cruise tomcruise 123456 123456 tomcruies#gmail.com Male 4/5/1998 Canada 1
B6666 Lena Do lenado 123456 123456 lenado#gmail.com Male 9/4/1997 USA 2
You can confirm my suggestion by doing a var_dump($row);die();
what i found on the package Maatwebsite, the $row has numeric indexes.
try this
class AlumniImport implements ToModel
{
/**
* #param array $row
*
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
return new User([
'code' => $row[0],
'first_name' => $row[1],
'last_name' => $row[2],
'username' => $row[3],
'password' => Hash::make($row[4]),
'tel' => $row[5],
'email' => $row[6],
'gender' => $row[7],
'birthday' => $row[8],
'address' => $row[9],
'status_id' => $row[10],
]);
}
}
-----edit-----
dont forget to put the fields in the $fillable of the User::class
class User extend Model
{
protected $fillable = ['code','first_name', 'last_name', 'username', 'password', 'tel', 'email', 'gender', 'birthday', 'address', 'status_id'];
.....
}
if you dont wanna put these fields as fillable and i dont recommend you to do it (especially for the password field) you can do it this way;
class AlumniImport implements ToModel
{
/**
* #param array $row
*
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
$user = new User();
$user->code = $row[0];
$user->first_name = $row[1];
$user->last_name = $row[2];
$user->username = $row[3];
$user->password = Hash::make($row[4]);
$user->tel = $row[5];
$user->email = $row[6];
$user->gender = $row[7];
$user->birthday = $row[8];
$user->address = $row[9];
$user->status_id = $row[10];
return $user;
}
}
Add this line in your AlumniImport class:
use Maatwebsite\Excel\Concerns\WithHeadingRow;
After this, include WithHeadingRow in your AlumniImport class like this:
class AlumniImport implements ToModel,WithHeadingRow

How to overload mongoose model instance in nodejs & Typescript

I'm trying to change the save() method, but I don't find where I can overload it. I use typescript and node.js.
For the moment, I have a UserModel that contains a mongoose.Schema and a mongoose.Model.
When I call UserModel.getModel() I retrieve the mongoose.Model from the UserModel.
I basically use a DAO to retrieve the Model class object.
user = message.getDataByKey('user');
user.save(function(err, data) {
// do stuff
});
I want to overload automatically the user object to use my own .save() method to check if there is error and always handle them by the same way.
When I set the Model, I do it like that:
public static model: any = model.Models.MongooseModel.getNewInstance(UserModel.modelName, UserModel._schema);
And in the parent:
public static getNewInstance(modelName, schema){
var Model: any = mongoose.model(modelName, schema);
// Overload methods.
//console.log(new Model());
// Return overloaded Model class.
return Model;
}
I would like to know if there is any way to overload the Model to make sure that each new instance from it will have my own .save method.
I thought use the statics/methods (methods actually, I guess) but it's empty or I know that the final object will have save/remove/update methods. So I don't know why it's not already into the object, I tried to console.log(Model and new Model()) but no save() method.
So I'm a little desappointed, maybe I missed something.
The fact is, I can't update directly the new Model() because they will be created later, in another context, I need to update the Model directly to make sure that the new instance from this model will have my extra function.
And I don't want to rewrite the basic .save() method, I just want to overload it to add extra validation.
Any idea? I'm kinda lost here, it's not that easy. Thx.
I found a solution to do so, I'm using typescript so I'll post both .ts and .js to everybody understand.
I use CommonJs compilation
Model.ts (Super model, parent of all models)
///<reference path='./../../lib/def/defLoader.d.ts'/>
/**
* Package that contains all Models used to interact with the database.
* TODO Use options http://mongoosejs.com/docs/guide.html
*/
export module Models {
/**
* Interface for all Models, except the parent class.
*/
export interface IModel{
/**
* Name of the model.
* It's a helper to always get the name, from instance or static.
* MUST start by uppercase letter!
*/
modelName: string;
/**
* Contains the static value of the public schema as object.
* It's a helper to always get the schema, from instance or static.
*/
schema: mongoose.Schema;
/**
* Contains the static value of the object used to manipulate an instance of the model.
* It's a helper to always get the model, from instance or static.
*/
model: any;
}
/**
* Parent class for all models.
* A model contains a mongoose schema and a mongoose model and other things.
*/
export class Model{
/**
* Suffix used to load automatically models.
*/
public static suffix: string = 'Model';
/**
* Suffix used to load automatically models.
* It's a helper to always get the schema, from instance or static.
*/
public suffix: string;
/**
* Name of the model.
* MUST start by uppercase letter!
*/
public static modelName: string = '';
/**
* Readable schema as object.
*/
public static schema: any;
/**
* Schema as mongoose Schema type.
*/
public static Schema: mongoose.Schema;
/**
* The mongoose model that uses the mongoose schema.
*/
public static model: any;
/**
* Use static values as instance values.
*/
constructor(){
// Use static values as instance values.
this.suffix = Model.suffix;
}
/**
* Returns a new mongoose.Schema customized instance.
* #param ChildModel Child model that made the call.
* #returns {*}
* #see http://mongoosejs.com/docs/2.7.x/docs/methods-statics.html
*/
public static getNewSchemaInstance(ChildModel): mongoose.Schema{
var schema: any = new mongoose.Schema(ChildModel.schema, {collection: ChildModel.modelName.toLowerCase()});
// Overload methods.
//schema.methods.toObject = function(callback){}
// Return overloaded instance.
return schema;
}
/**
* Retrieves a new Model instance and overload it to add statics methods available for all Models.
* #param ChildModel
* #returns {*}
* #see http://mongoosejs.com/docs/2.7.x/docs/methods-statics.html
*/
public static getNewModelInstance(ChildModel): any{
// Get the Model class.
var Model: any = mongoose.model(ChildModel.modelName, ChildModel.Schema);
/**
**************************************************************************************************
************************ Extended Model static methods for all Models ****************************
**************************************************************************************************
*/
/**
* Handler for all database/mongoose errors.
* #param err Error.
* #param data Data. Contains the model and the emitter. (+ more)
* #param callback Callback function to execute.
*/
Model.errorHandler = (err: any, data: any, callback: (message: (any) => any) => any) => {
// Extract data.
var _Model = data.model;
var __function = data.__function;
var __line = data.__line;
// Will contains the error.
var message:any = [];
// Mongo error.
if(err && err.name && err.name == 'MongoError'){
var _err = MongoError.parseMongoError(err);
if(err.code == 11000){
// Duplicate key on create.
message[0] = '__19';
message[1] = [_err.value, _err.field];
}else if(err.code == 11001){
// Duplicate key on update.
message[0] = '__20';
message[1] = [_err.value, _err.field];
}else{
// Non-managed mongo error.
if(dev()){
// Return not only the message but also some information about the error.
message[0] = [];
// Message. [0][1] could be args.
message[0][0] = '__21';
// Data.
message[1] = {
err: err,
model: _Model.modelName
};
}else{
message = '__21';
}
}
fs.appendFile(__config.path.base + __config.mongo.error.log, new Date() + ': ' + JSON.stringify({error: err, model: _Model.modelName, _err: _err}) + '\n');
}else if(err && err.name && err.name == 'ValidationError'){
// Validation error from mongoose.
var _err = MongoError.parseValidationError(err);
message[0] = [];
// Message. [0][1] could be args.
message[0][0] = '__24';
message[0][1] = [_err[0].value, _err[0].field, _err[0].type];
if(dev()){
// Will be send as args but not displayed in the message.
message[1] = {
err: _err,
model: _Model.modelName
};
}
fs.appendFile(__config.path.base + __config.mongo.error.log, new Date() + ': ' + JSON.stringify({error: err, model: _Model.modelName, _err: _err}) + '\n');
}else{
// Another error? I don't know if that could happens, but manage it anyway.
message[0] = '__22';
if(dev()){
message[1] = [err, _Model.modelName];// Will be send as args but not displayed in the message.
}
fs.appendFile(__config.path.base + __config.mongo.error.log, new Date() + ': ' + JSON.stringify({error: err, model: _Model.modelName}) + '\n');
}
callback(message);// return an error.
};
/**
* Check if the object exists and returns it in this case.
* #param object Object to find.
* #param callback Callback to execute.
* #return
* err Error if it happens. [null]
* found Found object or false.
*/
Model.exists = (object, callback): any => {
// If object is null or false or empty or whatever, don't do the research, the result could be wrong!
if(!object){
callback (null, false);
}else{
Model.findOne(object, function (err, found) {
if (err){
Model.errorHandler(err, ChildModel, callback);
}else if (found){
callback(null, found);
}else{
callback (null, false);
}
});
}
};
// Return overloaded instance.
return Model;
}
}
/**
* Class that manage MongoDb errors, used statically.
*/
export class MongoError{
/**
* Parse a mongo error to returns data from it because Mongo returns really bad errors.
* #param err The mongo error.
* #returns {*}
*/
public static parseMongoError(err): any{
var _err: any = {};
var _message: string = err.err;
if(err.code == 11000 || err.code == 11001){
var message = _message.split(':');
// Get the table where the error was generated.
_err.table = message[1].split('.')[1];
// Get the field name where the error was generated.
_err.field = message[1].split('.')[2].split(' ')[0].replace('$', '');
_err.field = _err.field.substr(0, _err.field.lastIndexOf('_'));
// Get the
_err.value = message[3].split('"')[1].replace('\\', '');
}
return _err;
}
/**
* Parse a mongoose validation error, probably generated during a save/update function.
* #param err The mongoose error.
* #returns {*}
*/
public static parseValidationError(err): any{
var _errors: any = new Array();
var i = 0;
for(var error in err.errors){
_errors[i] = [];
_errors[i]['field'] = err.errors[error]['path'];
_errors[i]['value'] = err.errors[error]['value'];
_errors[i]['type'] = err.errors[error]['type'];
i++;
}
return _errors;
}
}
}
The JS version:
http://pastebin.com/xBTr1ZVe
Error messages (__21, etc.) are:
"__19": "Unable to add the element, the value **_$0** for the field _$1 already exists, it cannot be duplicated.",
"__20": "Unable to update the element, the value **_$0** for the field _$1 already exists, it cannot be duplicated.",
"__21": "Unable to execute the requested operation. Database error 21. Please report to an administrator.",
"__22": "Unable to execute the requested operation. Database error 22. Please report to an administrator.",
"__23": "Validation error, the requested operation was aborted. Please check that all your data are correct and retry. Please report to an administrator. (code 23)",
"__24": "Unable to perform the operation, the value ***_$0** for the field _$1 didn't pass successfully the validation. Error: _$2",
Basically all my models should manage by themself these exception, of course. But if I forgot to do it, I'll get a managed exception, better.
Now I'll post a real Model, UserModel inheriting the parent Model.
///<reference path='./../../lib/def/defLoader.d.ts'/>
import model = require('./Model');
export module Models {
/**
* Model used to manage users.
* The model is primary static, but, to make it easy to use, some things are also stored for each instance.
* That allows the code to use both Model or instance of Model such as:
* Model.schema
* model.Schema
*/
export class UserModel extends model.Models.Model implements model.Models.IModel{
/**
*************************************************************************************************
****************************** Public methods & attributes **************************************
*************************************************************************************************
*/
/**
* Name of the model.
* MUST start by uppercase letter!
*/
public static modelName: string = 'User';
/**
* Readable schema as object.
*/
public static schema: any = require('../schemas/userSchema.js');
/**
* Schema as mongoose Schema type.
*/
public static Schema: mongoose.Schema = model.Models.Model.getNewSchemaInstance(UserModel);
/**
* The mongoose Model that uses the mongoose schema.
*/
public static model: any = model.Models.Model.getNewModelInstance(UserModel);
/**
* Helpers to always get the property, from instance or static.
*/
public modelName: string = UserModel.modelName;
public schema: mongoose.Schema = UserModel.schema;
public model: mongoose.Model<any> = UserModel.model;
/**
*************************************************************************************************
***************************** Extended methods & attributes **************************************
*************************************************************************************************
*/
/**
* These fields are protected, the user password is required to access to them.
* These fields are basically shared between applications.
* #private
*/
private static _protectedFields: string[] = [
'login',
'email'
];
/**
* Method to use to hash the user password.
*/
private static _passwordHashMethod: string = 'sha256';
/**
* Digest to use to hash the user password.
*/
private static _passwordDigest: string = 'hex';
/**
* Returns the protected fields.
* #returns {string[]}
*/
public static getProtectedFields(): string[]{
return this._protectedFields;
}
/**
* Hash a user password depending on the password hash configuration. Currently SHA256 in hexadecimal.
* Assuming crypto is global.
* #param password User password.
* #returns {string} Hashed password.
*/
public static hashPassword(password: string): string{
return crypto
.createHash(UserModel._passwordHashMethod)
.update(password)
.digest(UserModel._passwordDigest)
}
}
/**
* Don't forget that some methods such as exists() are written in the Model class and available for all Models.
* The following methods belong ONLY to the mongoose model instance, not to the Model class itself!
*
*************************************************************************************************
******************************** Extended Model methods *****************************************
*************************************************************************************************
*/
/**
* Connect a user to the game.
* #param user User to check. {}
* #param callback Callback to execute.
*/
UserModel.model.checkAuthentication = (user, callback) => {
// Force to provide login and password.
UserModel.model.exists({login: user.login, password: UserModel.hashPassword(user.password)}, function(err, userFound){
// Load public profile.
UserModel.model._getProtectedInformation(userFound, function(userPublic){
// Provides only public fields.
callback(new __message("__17", {err: err, user: userPublic}, !err && userFound ? true: false));
});
});
};
/**
* Get the protected fields for the found user.
* #param user User to find.
* #param callback Callback to execute.
*/
UserModel.model.getProtectedInformation = (user, callback) => {
// We are looking for an unique user.
UserModel.model.exists(user, function(err, userFound){
if(err){
UserModel.model.errorHandler(err, UserModel, callback);
}else{
// Load public profile.
UserModel.model._getProtectedInformation(userFound, function(userPublic){
// Provides only public fields.
callback(new __message('', {err: err, user: userPublic}, err ? false: true));
});
}
});
};
/**
* Get the protected fields of a user.
* #param user Instance of model.
* #param callback Callback to execute.
* #private
*/
UserModel.model.hashPassword = (user, callback): any => {
var err = false;
if(user && user.password){
user.password = UserModel.hashPassword(user.password);
}else{
err = true;
}
callback(new __message(err ? '__18': '', {user: user}, err ? false: true));
};
/**
*************************************************************************************************
*************************** Methods to use only locally (private) *******************************
*************************************************************************************************
*/
/**
* Get the protected fields of a user.
* #param user Instance of model.
* #param callback Callback to execute.
* #private
*/
UserModel.model._getProtectedInformation = (user, callback): any => {
var userPublic = {};
// Get fields to share.
var publicFields = UserModel.getProtectedFields();
// Fill the userPublic var with public fields only.
for(var field in publicFields){
userPublic[publicFields[field]] = user[publicFields[field]];
}
callback(userPublic);
};
}
The JS version:
http://pastebin.com/0hiaMH25
The schema:
/**
* Schema ued to create a user.
* #see http://mongoosejs.com/docs/2.7.x/docs/schematypes.html
*/
module.exports = userSchema = {
/**
* User Login, used as id to connect between all our platforms.
*/
login: {
type: 'string',
//match: /^[a-zA-Z0-9_-]{'+userSchema.login.check.minLength+','+userSchema.login.check.maxLength+'}$/,
trim: true,
required: true,
notEmpty: true,
unique: true,
check: {
minLength: 4,
maxLength: 16
}
},
/**
* User email.
*/
email: {
type: 'string',
lowercase: true,
match: /^[a-zA-Z0-9._-]+#[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/,
required: true,
notEmpty: true,
unique: true,
check: {
minLength: 6,
maxLength: 30
}
},
/**
* User private password, the one hashed in SHA512 and stored on the database.
*/
password: {
type: 'string',
required: true,
check: {
length: 128
}
},
/**
* Salt to use to decrypt the password.
*/
passwordSalt: {
type: 'string',
check: {
length: 64
}
},
/**
* Password sent from user interface but hashed before be send on the network.
* Used to basically connect an user or generate the final password.
* Not stored in the DB.
*/
passwordProtected: {
type: 'string',
check: {
length: 64
}
},
/**
* Password wrote by the user on the GUI, not hashed or encrypted.
* Will be encrypted to respect the "passwordProtected" rules.
* Not stored in the DB.
*/
passwordPublic: {
type: 'string',
check: {
minLength: 8,
maxLength: 25
}
},
/**
* User banned status (Temporary of Definitive)
*/
banned: {
temporary : {
type : "number",
default : Date.now
},
definitive: {
type: 'boolean',
default: false
}
},
/**
* User right
*/
right : {
admin : {
type : "boolean",
default : false,
required: true
},
moderator : {
type : "boolean",
default : false,
required: true
}
}
};
So, what the code does?
Basically, in the Model.getNewModelInstance() I bind to the created model the errorHandler method that I will call if I found a DB error in the controller.
**UserController.js**
User.exists({email: user.email}, function(err, emailFound){
// If we got an err => Don't find couple User/pass
if (err) {
User.errorHandler(err, {model: User, __filename: __filename,__function: __function || 'subscription#exists', __line: __line}, function(err){
res.json(__format.response(err));
});
)
});
The __filename and so on are global functions that I use to get the current data, useful to debug. I'm still looking for a way to add this automatically but so far I couldn't. The __function doesn't exists when the function is anonymous. But it helps me to debug.
Any suggestion? That's a lot of piece of code.

Resources