Loopback custom connector implementation - loopbackjs

I am trying to implement a custom loopback connector and it's not clear to me how this all works.
Here are my models:
{
"customer": {
"dataSource": "qb",
"public": false
},
"company": {
"dataSource": "qb",
"public": true
},
"payment": {
"dataSource": "qb",
"public": false
},
"invoice": {
"dataSource": "qb",
"public": false
}
}
The most important part to the model (and to save space) is
{
"relations": {
"company": {
"type": "belongsTo",
"model": "company",
"foreignKey": "id",
"primaryKey": "id"
}
}
}
And, in company.json
{
"name": "company",
"plural": "companies",
"base": "Model",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"id": {
"type": "string",
"required": true
}
},
"validations": [],
"relations": {
"customers": {
"type": "hasMany",
"model": "customer",
"foreignKey": "customerId"
},
"payments": {
"type": "hasMany",
"model": "payment",
"foreignKey": "customerId"
},
"invoices": {
"type": "hasMany",
"model": "customer",
"foreignKey": "customerId"
}
},
"acls": [],
"methods": {}
}
which, as expected, produces URLs like:
/companies/${id}/customers/${fk}
So, I try the swagger UI and submit: GET /companies/4620816365214377730/customers/456
The problem I have is now 2 fold:
It calls the all function on my connector every time - right away, that doesn't make sense. I've given it 2 specific ID's why would it possible want all of anything?
I managed the above and produced the results asked, but then loopback reports a 404:
{
"error": {
"statusCode": 404,
"name": "Error",
"message": "could not find a model with id 4620816365214377730",
"code": "MODEL_NOT_FOUND",
"stack": "Error: could not find a model with id 4620816365214377730"
}
}
So, I definitely don't get it - the first param in callback is the err, and the second is the result. I have literally hardcoded it to be right (I think)
How do I implement simple CRUD? Why does it not call my findById function? I have breakpoints everywhere
const {Connector: connector} = require('loopback-connector')
const util = require("util");
exports.initialize = function initializeDataSource(dataSource, callback) {
dataSource.connector = new QbConnector(dataSource.settings);
dataSource.connector.dataSource = dataSource;
};
exports.QbConnector = QbConnector
function QbConnector(settings, datasource) {
connector.call(this, 'quickbooks', settings)
this.datasource = datasource
this.client = require(`./qb`)(require('./axios'))
}
util.inherits(QbConnector, connector);
// connector.defineAliases(QbConnector.prototype, 'find', 'findById');
QbConnector.prototype.create = function(data, callback) {
console.log()
}
QbConnector.prototype.replaceOrCreate = function(model, data, options, cb) {
console.log()
}
QbConnector.prototype.findOne = function (filter,cb) {
console.log()
}
QbConnector.prototype.all = function(model, filter, callback) {
this.client[model]?.get(filter.where.id)
?.then(data => callback(null,{id: filter.where.id}))
?.catch(e => callback(JSON.stringify(e.response.data,null,4)))
}
QbConnector.prototype.count = function (whereClause,callback) {
console.log()
}
QbConnector.prototype.save = function(model, data, options, cb) {
console.log()
}
QbConnector.prototype.findById = function (id, filter, options) {
console.log()
}
When I step into the callback it's definition is a guaranteed error (the message I am seeing)
(function anonymous(number, plural, select, pluralFuncs, fmt
) {
return function(d) { return "could not find a model with id " + d["0"]; }
})

Related

AWS DynamoDB batchWriteItem in Lamda is not working properly

I have an app created via AWS Amplify and I created an AWS Lambda function that aim to insert multiple data to one of the table in DynamoDB.
Firstly, I tried map attributes to documentClient.put({...params}).promise() and run Promise.all() on it. But some items got lost. For example, I add 40 items at once, but only 5-10 got added. I thought this could be some lambda limit issue. So I switch to batchWriteItem() and seems it remain the same or even worse (only 1 out of 40 got added). Here is the code I wrote:
export const addAvailabilities = async (
docClient: AWS.DynamoDB.DocumentClient,
newAvailabilities: Availability[],
expertId: string,
cognitoUserId: string
) => {
console.info('#addAvailabilities: Start')
try {
let restArray = [...newAvailabilities]
const promiseArray = []
while (restArray.length > 0) {
const executingArray = restArray.slice(0, 25)
const temp = batchAddAvailability(docClient, executingArray, expertId, cognitoUserId)
promiseArray.push(temp)
restArray = restArray.slice(25)
}
const result = await Promise.all(promiseArray)
console.info(result)
console.info('#addAvailabilities: End')
return result
} catch (err) {
console.error('#addAvailabilities: Error')
throw err
}
}
const mapBatchAddAvailabilityParams = (newAvailabilities: Availability[], expertId: string, cognitoUserId: string) => {
return newAvailabilities.map((availability, index) => {
const currentTime = `${moment().utc().format('YYYY-MM-DD[T]HH:mm:ss.SS')}${index}Z`
return {
PutRequest: {
Item: {
id: uuid(),
__typename: 'ExpertAvailability',
expertId: expertId,
owner: cognitoUserId,
startTime: availability.start,
status: 'available',
createdAt: currentTime,
updatedAt: currentTime
}
}
}
})
}
const batchAddAvailability = async (
docClient: AWS.DynamoDB.DocumentClient,
newAvailabilities: Availability[],
expertId: string,
cognitoUserId: string
) => {
console.info('#batchAddAvailability: Start')
try {
const batchParams = mapBatchAddAvailabilityParams(newAvailabilities, expertId, cognitoUserId)
const param = {
RequestItems: {
[process.env.API_TAP_EXPERTAVAILABILITYTABLE_NAME]: batchParams
}
}
console.info('params', JSON.stringify(param))
return docClient.batchWrite(param).promise()
} catch (err) {
console.error('#batchAddAvailability: Error')
throw err
}
}
I add this const currentTime = ${moment().utc().format('YYYY-MM-DD[T]HH:mm:ss.SS')}${index}Z` because I saw this post has similar issue as mine and that's the solution solve his problem. But it doesn't solve mine.
From the logs, params get format correctly like this:
{
"RequestItems": {
"Availability": [
{
"PutRequest": {
"Item": {
"id": "66a7b63e-a14b-4ba2-94a9-0dd7bf457efe",
"__typename": "ExpertAvailability",
"expertId": "8d30c1a4-685e-40bf-b884-6e50bb422e99",
"owner": "test-keycloak_eeb71cb1-6c11-4fb9-a721-ce5dc7d06269",
"startTime": "2021-10-03T23:00:00.000Z",
"status": "available",
"createdAt": "2021-09-08T10:24:28.880Z",
"updatedAt": "2021-09-08T10:24:28.880Z"
}
}
},
{
"PutRequest": {
"Item": {
"id": "162d839d-7fde-417e-994b-2dc12336c4cf",
"__typename": "ExpertAvailability",
"expertId": "8d30c1a4-685e-40bf-b884-6e50bb422e99",
"owner": "test-keycloak_eeb71cb1-6c11-4fb9-a721-ce5dc7d06269",
"startTime": "2021-10-04T00:00:00.000Z",
"status": "available",
"createdAt": "2021-09-08T10:24:28.881Z",
"updatedAt": "2021-09-08T10:24:28.881Z"
}
}
},
{
"PutRequest": {
"Item": {
"id": "dc257c75-9a27-482a-88c5-1747ffe97361",
"__typename": "ExpertAvailability",
"expertId": "8d30c1a4-685e-40bf-b884-6e50bb422e99",
"owner": "test-keycloak_eeb71cb1-6c11-4fb9-a721-ce5dc7d06269",
"startTime": "2021-10-04T01:00:00.000Z",
"status": "available",
"createdAt": "2021-09-08T10:24:28.932Z",
"updatedAt": "2021-09-08T10:24:28.932Z"
}
}
},
{
"PutRequest": {
"Item": {
"id": "75b2e911-e842-4f11-99ed-702c6cf1c485",
"__typename": "ExpertAvailability",
"expertId": "8d30c1a4-685e-40bf-b884-6e50bb422e99",
"owner": "test-keycloak_eeb71cb1-6c11-4fb9-a721-ce5dc7d06269",
"startTime": "2021-10-04T02:00:00.000Z",
"status": "available",
"createdAt": "2021-09-08T10:24:28.943Z",
"updatedAt": "2021-09-08T10:24:28.943Z"
}
}
},
{
"PutRequest": {
"Item": {
"id": "df65e151-1699-446d-ab3b-06aca707a2fb",
"__typename": "ExpertAvailability",
"expertId": "8d30c1a4-685e-40bf-b884-6e50bb422e99",
"owner": "test-keycloak_eeb71cb1-6c11-4fb9-a721-ce5dc7d06269",
"startTime": "2021-10-04T03:00:00.000Z",
"status": "available",
"createdAt": "2021-09-08T10:24:28.944Z",
"updatedAt": "2021-09-08T10:24:28.944Z"
}
}
},
{
"PutRequest": {
"Item": {
"id": "98a5bdd2-60d3-4d5d-9913-40b5c84c6d62",
"__typename": "ExpertAvailability",
"expertId": "8d30c1a4-685e-40bf-b884-6e50bb422e99",
"owner": "test-keycloak_eeb71cb1-6c11-4fb9-a721-ce5dc7d06269",
"startTime": "2021-10-04T04:00:00.000Z",
"status": "available",
"createdAt": "2021-09-08T10:24:28.945Z",
"updatedAt": "2021-09-08T10:24:28.945Z"
}
}
}
]
}
}
There is no UnprocessedItems or any errors return from the execution. But the items are just missing. The capacity of the table is on-demand so I think capacity shouldn't be a problem. Any ideas what's wrong? Many thanks

Loopback access both super model and extended models from same relation

I am using Loopback 3 and i have the following three models:
UserFile.json
{
"name": "UserFile",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"id": {
"type": "number",
"id": true,
"generated": true
},
"name": {
"type": "string",
"required": true
},
"size": {
"type": "number",
"default": 0
},
"uploadedAt": {
"type": "date",
"default": "$now"
}
},
"relations": {
"hasFile": {
"type": "hasMany",
"model": "UploadedFile",
"foreignKey": "fileId"
}
}
}
UploadedFile.json
{
"name": "UploadedFile",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"hashId": {
"type": "string",
"required": true
},
......
},
"relations": {
"file": {
"type": "belongsTo",
"model": "UserFile",
"foreignKey": "fileId"
}
}
}
PartitionedFile.json
{
"name": "PartitionedFile",
"base": "UploadedFile",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"partSize": {
"type": "number",
"default": 0
}
},
"relations": {
"file": {
"type": "belongsTo",
"model": "UserFile",
"foreignKey": "fileId"
}
}
}
The idea is that the UserFile has either one UploadedFile or (two or more) many PartitionedFile.
I want to be able to get from the UserFile, from the same relation the file, whether it is an UploadedFile or a PartitionedFile. Is it possible to achieve this just from the models' definitions or it can only be done with a remote method?
A relation from a model to another is bound to only one model; so you cannot have one relation called "file" to point to different models. In your example, it's bound to UploadedFile. In order for your model UserFile to have relations with both UploadedFile and PartitionedFile, you'll need two different relations.
For your UserFile to have either one UploadedFile or many PartitionedFile, it needs to have them both within its relations:
{
"name": "UserFile",
...
"relations": {
"uploadedFiles": {
"type": "hasOne", // Zero on one uploaded file
"model": "UploadedFile",
"foreignKey": "uploadedFileId"
},
"partitionedFiles": {
"type": "hasMany", // Zero or many partitioned files
"model": "PartitionedFile"
}
}
}
Then, to retrieve files from both UploadedFiles and PartitionedFiles through UserFile, you don't need a remote, but to include the models within your request.
For example, in ReactJS, it would look like this:
const response = await Axios.get(`/api/UserFile`, {
params: {
filter: {
include: [
{ relation: 'uploadedFiles' },
{ relation: 'partitionedFiles' },
]
}
}
}
Another solution would be to specify another model File that contains all these relations (UploadedFile, PartitionedFile, etc), so that the model UserFile can refer to the table File as one unique relation to retrieve the files...

Send a function along with the scheme from backend in Alpacajs

I am having an API in backend to return the full json (schema and options) for a AlpacaJS form. The content-type of the response is application/json. Following is a sample response,
{
"options": {
"fields": {
"students": {
"items": {
"type": "tablerow"
},
"type": "table"
}
},
"form": {
"buttons": {
"submit": {
"click": "function(){alert(\"sample\");}"
}
}
}
},
"schema": {
"properties": {
"students": {
"items": {
"properties": {
"name": {
"required": true,
"title": "Name",
"type": "string"
},
"contact-number": {
"title": "Age",
"type": "string"
}
},
"type": "object"
},
"type": "array"
}
},
"type": "object"
}
}
When I click on the Submit button, I get the following error in browser console,
Uncaught TypeError: t.call is not a function
I think the issue is that the function is considered as a string in the following section of the response.
"form": {
"buttons": {
"submit": {
"click": "function(){alert(\"sample\");}"
}
}
}
Is there a way in AlpacaJS to send a javascript function from backend, or is there a way to convert the function string to a javascript function in frontend?
In order to get that, you should transform the stringified function to a function by doing new Function('return ' + val)(); (beware this is a form of eval and eval is evil).
Here's a working fiddle for that.
Tell me if it didn't work for you.

scope in inherited model

I have a contact db table and model. Employee model inherits from contact.
If i do GET employees/ it returns all the contacts.
How should I set up my employee.json if I want to return only the contacts with partnerId = 1?
{
"name": "employee",
"base": "contact",
"strict": false,
"idInjection": false,
"options": {
"validateUpsert": true,
"postgresql": {
"schema": "public",
"table": "contact"
}
},
"scope": {
"where": {
"partnerId": 1
}
},
//...
}
Debug says calling GET employees/ makes the following query:
SELECT "name", "position", "email", "password", "id" FROM "public"."contact" ORDER BY "id"
It does not seem that scope is added.
models/partner.json
{
"name": "partner",
// ...
"properties": {
"name": {
"type": "string",
"required": true
},
// ...
},
"validations": [],
"relations": {
"contacts": {
"type": "hasMany",
"model": "contact"
}
//...
},
"acls": [],
"methods": {}
}
Try using the where filter, either in the REST API
/employees?filter[where][partnerId]=1
or in your Employee.js
Employee.find({ where: {partnerId:1} });
https://docs.strongloop.com/display/APIC/Where+filter

loopback hasMany relation remoteMethod hook

I am using loopback for develop my own website.
But recently I had a problem of hasMany remoteMethod.
Here is the problem:
I have two models :
person.json:
{
"name": "Person",
"base": "PersistedModel",
"strict": true,
"idInjection": true,
"properties": {
/*...
....
*/
},
"validations": [],
"relations": {
"friends": {
"type": "hasMany",
"model": "Friend",
"foreignKey": "personId"
}
},
"acls": [],
"methods": []
}
friend.json
friend.json:
{
"name": "friend",
"base": "PersistedModel",
"strict": true,
"idInjection": true,
"properties": {
/*...
....
*/
},
"validations": [],
"relations": {
},
"acls": [],
"methods": []
}
I want to use beforeRemote when I call POST /api/Persons/{id}/friends.
So I code in person.js
module.exports = function(Person) {
Person.beforeRemote('__create__friends', function(ctx, instance, next) {
/*
code here
*/
});
};
But it does not work!
At the beginning I think it's the matter of '__create__friends',but when I
code in person.js like :
module.exports = function(Person) {
Person.disableRemoteMethod('__create__friends');
};
I could disable the '__create__friends' successfully.
So what's the problem?
Can anyone help me?
Because methods for related models are attached to Person prototype, you should register hook like this:
Person.beforeRemote('prototype.__create__friends', function() {
next()
})