Creating forms in Aposstrophe CMS - node.js

I am following this tutorial to create a form in my project, which is leveraging the Apostrophe CMS. When i follow the tutorial, I am able to create the form and submit, and understand how to view the form submission in the Admin Console.
However, when I begin to customize the form to fit my project-specific needs, I break the form, causing the form validation to fail. The error i get upon submitting the form is:
TypeError: Cannot read property 'length' of undefined
at Object.convertString [as convert] (user.js:727)
at user.js:145
at async.js:181
at iterate (async.js:262)
at Object.async.forEachOfSeries.async.eachOfSeries (async.js:281)
at Object.async.forEachSeries.async.eachSeries (async.js:214)
at Object.self.convert (user.js:127)
at convert (always.js:57)
at async.js:718
at iterate (async.js:262)
My changes, other then adjusting the field definition object of my contact-form:index.js file, are mostly in the contact-form-widgets:widget.html file.
In the tutorial, the contact-form-widgets:widget.html view imports apostrophe-schemas:macros.html, and uses html files from the apostrophe-schemas and apostrophe-ui modules to build the html of the form. My specific questions therefore have to do with the importance of those two modules. Are those modules simply used for the front end display of the form? Do the contents of the views of those modules have any bearing on the submission and post-submissions tasks of the form? If the answer is negative, this means I am not configuring the form correctly, so therefore, are there any resources to help solve that problem?
Here is my contact-form:index.js config file:
var async = require('async');
module.exports = {
extend: 'apostrophe-pieces',
name: 'contact-form',
label: 'Contact Form',
alias: 'contactForm',
addFields: [
{
name: 'name',
type: 'string',
label: 'First & Last Name',
required: true
},
{
name: 'company',
type: 'string',
label: 'Company Name',
required: true
},
{
name: 'email',
type: 'string',
label: 'Email Address',
required: true
},
{
name: 'phone',
type: 'string',
label: 'Phone Number & Extension',
required: true
},
{
name: 'subject',
type: 'string',
label: 'Subject',
required: true
},
{
name: 'message',
type: 'string',
label: 'Message',
textarea: true,
placeholder: "Don't be stupid"
}
],
permissionsFields: false,
afterConstruct: function(self) {
self.setSubmitSchema();
},
construct: function(self, options) {
self.setSubmitSchema = function() {
self.submitSchema = self.apos.schemas.subset(self.schema,
[ 'name', 'company', 'email', 'phone', 'subject', 'message' ]
);
};
self.submit = function(req, callback) {
var piece = {};
return async.series([
convert,
insert
], callback);
function convert(callback) {
return self.apos.schemas.convert(req, self.schema, 'form', req.body, piece, callback);
}
function insert(callback) {
return self.insert(req, piece, { permissions: false }, callback);
}
};
}
};

I'm the lead developer of Apostrophe at P'unk Avenue. We had some conversation in another forum but just for the record, there turned out to be two issues:
The form was submitting on any button click because that is what button elements do when they are of type "submit," and "submit" is the default value for type (at least in some browsers). It's not an Apostrophe issue and can be resolved by setting the type attribute as desired.
The form markup was custom and did not have wrapper elements such as data-name="title", etc. (corresponding to the schema) around the actual form field elements. You may use custom markup but all of the "moving parts" found in our official schemaMacros.html need to be there, i.e. the data attributes need to exist.
We do plan to make it easier to use custom markup without those wrappers, at least in cases where we can figure out which fields are which without them.
FYI, an element of type="file" will not work out of the box, but check out the attachment field type in apostrophe's schemas, which is pretty amazing for that role.

Related

Joi arr conditional length

A user will be sending contact information as an array of objects like the following, with the phone number being optional and the email being required:
{
"contact": [{
"type": "phone",
"value": "555-555-5555"
}, {
"type": "email",
"value": "test#test.com"
}]
}
I would like to ensure there is an email object inside the array. I tried a Joi validation like this:
contact: Joi.array().items(Joi.object().keys({
type: Joi.string().valid('phone', 'email'),
value: Joi.string()
.when('contact.type', { is: 'phone', then: Joi.string() })
.when('contact.type', { is: 'email', then: Joi.string().email().required() })
}))
.when('contact.type', { is: 'phone', then: Joi.array().min(2).required() }),
But I get the following error:
Error: Item cannot come after itself: contact
It seems it doesn't like me giving it a length in this manner but I can't figure out any other way to do it. Any help would be appreciated. Thanks.
This schema combining .when, .has, and .unique, should work:
Joi.object({
contact: Joi.array().items(
Joi.object().keys({
type: Joi.string().valid('phone', 'email').required(),
value: Joi.string().when('type', { is: 'email', then: Joi.required() })
}).required(),
)
.has(Joi.object({ type: 'email', value: Joi.exist() }))
.unique('type').min(1).max(2)
})
Let's see the rules:
the object with the type 'email' must exist;
That's why I've added
.has(Joi.object({ type: 'email', value: Joi.exist() }))
This means that the array must have at least one of these elements.
We don't want duplicates, right?
.unique('type').min(1).max(2)
The array will have either 1 element, or 2, with different types.
When the type is 'email', the value should be required, and optional otherwise. That's what we are saying here:
value: Joi.string().when('type', { is: 'email', then: Joi.required() })
Thank you #soltex for your answer.
First I'm not sure we should exclude duplicates. Some people have multiple phone numbers.
Second, your answer did not quite work. Here is my updated answer based on what you wrote:
contact: Joi.array().items(Joi.object().keys({
type: Joi.string().valid('phone', 'email').required(),
value: Joi.string().required(),
}))
.has(Joi.object({ type: 'email', value: Joi.string().email() }))
Once I include the .has method then there is no reason to include the .when method. I also want the email to be a valid email not just that it exists. I changed value to required because if the user sends type phone I still want them to include the value.
Once again thanks for your guidance.

When sails default model attributes get their values?

When sails fill default global attributes which we added on config/models.js ,
default settings looks like :
attributes: {
id: { type: 'number', autoIncrement: true },
createdAt: { type: 'number', autoCreatedAt: true },
updatedAt: { type: 'number', autoUpdatedAt: true },
}
Now if we add sth like creatorId to this default attributes , how we should fill it once for all our models ?
attributes: {
id: { type: 'number', autoIncrement: true },
createdAt: { type: 'number', autoCreatedAt: true },
updatedAt: { type: 'number', autoUpdatedAt: true },
creatorId: { type: 'number'}
}
After this change , all models have creatorId with 0 value , how I can set userId to all of my models creatorId before save without repeating my self?
In the controller you are creating the entry in the database this should be quite straight forward. Let's assume that you have two models, User, which comes with Sails built-in authentication, and a Thing, something that someone can own.
In the Thing model, I'd change the ownerId to be owner and associate it with the User model like so:
attributes: {
id: { ... },
createdAt: { ... },
updatedAt: { ... },
owner: {
model: 'User',
required: yes // Enable this when all the stuff in the db has this set
},
}
This creates an association or one-to-many relationship if you know database terminology.
Now in the controller where you create your object to be inserted:
Thing.create({
someAttribute: inputs.someValue,
someOtherAttribute: inputs.someOtherValue,
owner: this.req.me.id
});
If you want to use the created object right away, append .fetch() to the chain after .create({...}) like so:
var thing = await Thing.create({ ... }).fetch();
Let me know if something is unclear.
I'd actually recommend you invest the $9 in buying the SailsJS course. It's an official course, taught by the creator of SailsJS, Mike McNeil. It takes you from npm i sails -g to pushing to production on the Heroku cloud platform. It teaches basic Vue (parasails flavour), using MailGun, Stripe payments, and more. They link to the course on the site here
Update
Did some further digging, and was inspired by a couple of similar cases.
What you can do is expand your model with a custom method that wraps the .create() method. This method can receive the request object from your controllers, but doing this, rather than the previous suggestion, will probably be more work than just adding ownerId: this.req.me.id, to existing calls. I1ll demonstrate anyway.
// Your model
module.exports = {
attributes: { ... },
proxyCreate(req, callback) {
if(!req.body.ownerId){
req.body.ownerId = req.me.id // or req.user.id, cant remember
// which works here
}
Thing.create(request.body, callback);
}
}
And in your controller:
...
// Change from:
Thing.create(req.body);
// To:
Thing.proxyCreate(req);
...
Update #2
Another idea I had was adding the middleware on a per-route basis. I don't know the complexity of your routes, but you can create a custom middleware for only those routes.
In router.js you edit your routes (I'll show one for brevity):
....
'POST /api/v1/things/upload-thing': [
{ action: 'helpers/add-userid-to-ownerid' },
{ action: 'new-thing' }
],
....
In helpers/add-userid-to-ownerid:
module.exports: {
fn: function(req, res) {
if(!req.body.ownerId){
req.body.ownerId = req.me.id;
}
}
}

Mongoose Schema validation issues when using Array of Mixed [{ }]

I am building a schema in mongoose (v4.13.8) with an Array of Mixed values. I have come up with the following Schema:
var deviceConfigSchema = new mongoose.Schema({
capabilities: {
type: [capabilitySchema],
required: true,
validator: [isValidCapabilities, "Not a valid capability array"]
},
services: {
type: [{}],
required: true,
validator: [isValidServices, "Not a valid service array"]
}
});
The problem is that I get a validation error saying that services: Path 'services' is required. when I try to submit data. What is strange is that the data I send for the 'capabilities' works fine and the only difference is that I specify a schema explicitly.
Removing the required: true from services causes there to be an empty array object in the returned values.
I am submitting the data using an API POST request with the data in the body of the request. I am using Postman to submit the request, with x-www-form-urlencoded checked. This is copied from the body key-value input
capabilities[0][field_map][field]:pressure
capabilities[0][field_map][type]:float
capabilities[0][field_map][format]:hPa
services[0][name]:rest
services[0][receive][0][capability_id]:0
services[0][receive][0][path]:/api/relay/0
Update:
I'd like to apologise as this was a mistake on my part. I dynamically create a configuration based on the request and at one point the copied services were being made null, doh!
However, having got the required: true validation to pass, the custom validator is still not being executed. I also can't find any documentation about the order in which validators and are executed which would be very useful. Below is the validator snippet for reference:
function isValidServices(services) {
for (const service of services) {
if (typeof service.name !== 'string') return false;
}
return true;
}
Having experimented with various approaches and looking in more detail on the mongoose API docs, I found that there is a validate option for schemas too. http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate
I changed my Schema from this:
var deviceConfigSchema = new mongoose.Schema({
capabilities: {
type: [capabilitySchema],
required: true,
validator: [isValidCapabilities, "Not a valid capability array"]
},
services: {
type: [{}],
required: true,
validator: [isValidServices, "Not a valid service array"]
}
});
To this [notice the validate instead of validator]...
var deviceConfigSchema = new mongoose.Schema({
capabilities: {
type: [capabilitySchema],
required: true,
validate: [isValidCapabilities, "Not a valid capability array"]
},
services: {
type: [{}],
required: true,
validate: [isValidServices, "Not a valid service array"]
}
});
After this my validator functions were being executed without any issues. Hopefully this helps someone.

Node.js waterline-orientdb update fail

I am trying to create a simple server application in Node.js using the waterline-orientdb package where there are several users who can invoke several methods. Before a user can do anything, the user needs to authenticate with his username and password. Within this authentication the user object is given a token that will be piggybacked with the future requests.
When a user is given a token, an update query is invoked. When invoking the update request I get the following error:
ERROR err: { [OrientDB.RequestError: expression item ']' cannot be resolved because current record is NULL]
name: 'OrientDB.RequestError',
message: 'expression item \']\' cannot be resolved because current record is NULL',
data: {},
previous: [],
id: 1,
type: 'com.orientechnologies.orient.core.exception.OCommandExecutionException',hasMore: 0 }
The strange thing is that the update is executed, so this error doesn't have influence on the update request. But because I want to catch all errors, I can't just ignore this.
My model looks like this:
module.exports = {
tableName: 'User',
identity: 'dbuser',
schema: true,
attributes: {
id: {
type: 'string',
primaryKey: true,
columnName: '#rid'
},
username: {
type: 'string',
required: true,
unique: true
},
password: {
type: 'string',
required: false
},
token: {
type: 'string'
},
follows: {
collection: 'dbuser',
via: 'followed',
dominant: true
},
followed: {
collection : 'dbuser',
via: 'follows'
}
};
As you can see, I'm associating two users with eachother so that one user can follow the activities of the other user. When I delete the association (so follows and followed) the error also dissapears.
The piece of code where the updates happens looks like this:
user[0].token = generateToken(user[0])
dbuser.update({
id: user[0].id
}, user[0]).exec(function (error, data) {
if (error) res.json(401, {
code: 401,
error: "Token could not be updated"
})
res.json(user);
});
Does anyone has an idea on how to avoid this behavior or what the error even means?
It seems to be a bug in the adapter.
You could try using:
npm install appscot/waterline-orientdb#refactor_collection
Apparently will be resolved in v.0.10.40
More info about it: https://github.com/appscot/waterline-orientdb/issues/43#issuecomment-75890992

Grid store config throws "Uncaught TypeError: Cannot read property 'buffered' of undefined"

I have form and grid. the user must enter data in form fields then display related records in the grid.
I want to implement a search form, e.g: user will type the name and gender of the student,
then will get a grid of all students have the same name and gender.
So, I use Ajax to send form fields value to PHP and then create a json_encode which will be used in grid store.
I am really not sure if my idea is good. But I haven't found another way to do that.
The problem is there is a mistake in my store but I couldn't figure out what it is. I get this error:
Uncaught TypeError: Cannot read property 'buffered' of undefined
My View:
{
xtype: 'panel',
layout: "fit",
id: 'searchResult',
flex: 7,
title: '<div style="text-align:center;"/>SearchResultGrid</div>',
items: [{
xtype: 'gridpanel',
store: 'advSearchStore',
id: 'AdvSearch-grid',
columns: [{
xtype: 'gridcolumn',
dataIndex: 'name',
align: 'right',
text: 'name'
}, {
xtype: 'gridcolumn',
dataIndex: 'gender',
align: 'right',
text: 'gender'
}
],
viewConfig: {
id: 'Arr',
emptyText: 'noResult'
},
requires: ['MyApp.PrintSave_toolbar'],
dockedItems: [{
xtype: 'PrintSave_tb',
dock: 'bottom',
}]
}]
}
My Controller:
.
.
.
xmlhttp.open("GET","AdvSearch.php?search_name="+search_name,true);
xmlhttp.send(null);
My PHP script:
if (!$con) {
throw new Exception("Error in connection to DB");
}
$query ="SELECT name, gender FROM students WHERE name ILIKE '%$search_name%' ";
$result = pg_query($query);
while ($row = pg_fetch_array($result)) {
$Arr[] = array('name' => $row[0], 'gender' => $row[1]);
}
$searchResult_list = array();
$searchResult_list['success'] = true;
$searchResult_list['Arr'] = $Arr;
$searchResult_list['totalCount'] = count( $searchResult_list['Arr'] );
echo json_encode($searchResult_list);
if (!$result)
die("Error in query: " . pg_last_error());
pg_close($con);
My Store, Model:
Ext.define('AdvSearchPost', {
extend: 'Ext.data.Model',
proxy: {
type: 'ajax',
url: 'AdvSearch.php',
reader: {
type: 'json',
root: 'Arr',
totalProperty: 'totalCount'
}
},
fields: [{
name: 'name'
}, {
name: 'type_and_cargo'
}
]
});
advSearchStore = Ext.create('Ext.data.Store', {
pageSize: 10,
model: 'AdvSearchPost'
});
Well it is just a typo of your storename.
The error Uncaught TypeError: Cannot read property 'buffered' of undefinedonly indicates that the store could not be bound. It may be a bit misleading.
Try the grid with either
store: advSearchPost
or
store: Ext.StoreMgr.lookup('AdvSearchPost') // if in any form a controller takes care about your store
and it will work.
Edit
I guess you haven't any controller so I recommend you to create your store like this
Ext.create('Ext.data.Store', {
pageSize: 10,
model: 'AdvSearchPost',
storeId: 'AdvSearchPost'
});
That will enable you to receive the store by the StoreManager from everywhere (after it is created). That will also enable the last statement to work without any controller.
Even if you call like this..
store: Ext.StoreMgr.lookup('bla bla bla') won't throw any error in the console.
Replace store parameter with storeId and then assign your actual store to it which will connect to your actual store. storeId:advSearchPost

Resources