Unable to query many to many relationship as mentioned in amplify datastore docs - aws-appsync

I am facing error while getting data from many to many relationship in amplify datastore. It was working fine but suddenly it was sending error. Below are my tables models
type Company
#model
#auth(rules: [{ allow: owner }, { allow: groups, groupsField: "invites" }]) {
id: ID!
name: String
users: [UserCompany] #connection(keyName: "byCompany", fields: ["id"])
}
type UserCompany
#model
#key(name: "byOwner", fields: ["owner"])
#key(name: "byUser", fields: ["userID", "companyID"])
#key(name: "byCompany", fields: ["companyID", "userID"])
#auth(
rules: [{ allow: owner }, { allow: groups, groupsField: "accessors" }]
) {
id: ID!
userID: ID
companyID: ID
user: User! #connection(fields: ["userID"])
company: Company! #connection(fields: ["companyID"])
owner: String
accessors: [String]
}
type User
#model
#aws_api_key
#aws_cognito_user_pools
#auth(
rules: [
{ allow: owner }
{ allow: groups, groupsField: "user_group" }
{ allow: groups, groupsField: "tenant_group" }
]
) {
id: ID!
company_name: String
company_address: String
contact_name: String
profile_image: String
email: String
phone: String
UserCompanys: [UserCompany]
#connection(keyName: "byUser", fields: ["id"])
owner: String
tenant_group: [String]
user_group: String
};
I was getting UserCompany like this
this.users = (await DataStore.query(UserCompany)).filter( (item) => item.user.id === id );
and it was working fine
but from last Friday suddenly I am facing this error.
Error: Field user is required
at datastore.js:219
at datastore.js:276
at Array.forEach ()
at initializeInstance (datastore.js:274)
at datastore.js:284
at e.i.produce (immer.esm.js:1)
at new UserCompany (datastore.js:283)
at IndexedDBAdapter.modelInstanceCreator (datastore.js:211)
at IndexedDBAdapter.js:516
at Array.map ()
at resolvePromise (zone-evergreen.js:798)
at zone-evergreen.js:705
at rejected (tslib.es6.js:74)
at ZoneDelegate.invoke (zone-evergreen.js:364)
at Object.onInvoke (core.js:28513)
at ZoneDelegate.invoke (zone-evergreen.js:363)
at Zone.run (zone-evergreen.js:123)
at zone-evergreen.js:857
at ZoneDelegate.invokeTask (zone-evergreen.js:399)
at Object.onInvokeTask (core.js:28500)
After this error system is unable to run other functions.
Is there anyway to get pivot table data of many to many relationship using Datastore.

Related

TypeOrm Joins without using QueryBuilder

I have 3 tables
attendance
user
attendance_verification.
attendance->ManyToOne->user
attendance->OneToOne->attendanceVerification
now I want to query attendance for a specific user and want to embed complete user and attendanceVerification data in attendance object and return it.
This is what I have tried but it is giving me error Invalid syntax near where.
I have tried
const listOfAttendance = await this.attendanceRepository.find({
where: {
punchInDateTime: Between(dateFrom, dateTo),
},
order: {
id: 'ASC',
},
join: {
alias: 'attendance',
leftJoinAndSelect: {
user: 'attendance.user',
outlet: 'attendance.outlet',
punchAck: 'attendance.punchAck',
verification: 'attendance.attendanceVerification',
},
},
})
Query being parsed by TypeORM
SELECT
"attendance"."id" AS "attendance_id",
"attendance"."punchInImage" AS "attendance_punchInImage",
"attendance"."punchInDateTime" AS "attendance_punchInDateTime",
"attendance"."punchInComment" AS "attendance_punchInComment",
"attendance"."punchOutDateTime" AS "attendance_punchOutDateTime",
"attendance"."punchOutComment" AS "attendance_punchOutComment",
"attendance"."punchOutImage" AS "attendance_punchOutImage",
"attendance"."status" AS "attendance_status",
"attendance"."approvalStatus" AS "attendance_approvalStatus",
"attendance"."userId" AS "attendance_userId",
"attendance"."outletId" AS "attendance_outletId",
"attendance"."createdAt" AS "attendance_createdAt",
"user"."id" AS "user_id",
"user"."firstName" AS "user_firstName",
"user"."lastName" AS "user_lastName",
"user"."userName" AS "user_userName",
"user"."contact" AS "user_contact",
"user"."gender" AS "user_gender",
"user"."dob" AS "user_dob",
"user"."country" AS "user_country",
"user"."state" AS "user_state",
"user"."postalCode" AS "user_postalCode",
"user"."isAdmin" AS "user_isAdmin",
"user"."isEmployee" AS "user_isEmployee",
"user"."byEmail" AS "user_byEmail",
"user"."byContact" AS "user_byContact",
"user"."address" AS "user_address",
"user"."email" AS "user_email",
"user"."orgId" AS "user_orgId",
"user"."password" AS "user_password",
"user"."salt" AS "user_salt",
"user"."createdBy" AS "user_createdBy",
"user"."status" AS "user_status",
"user"."apiAccessKey" AS "user_apiAccessKey",
"user"."createdAt" AS "user_createdAt",
"user"."metaId" AS "user_metaId",
"outlet"."id" AS "outlet_id",
"outlet"."name" AS "outlet_name",
"outlet"."comment" AS "outlet_comment",
"outlet"."location" AS "outlet_location",
"outlet"."status" AS "outlet_status",
"outlet"."orgId" AS "outlet_orgId",
"outlet"."validated" AS "outlet_validated",
"punchAck"."id" AS "punchAck_id",
"punchAck"."isPunchedIn" AS "punchAck_isPunchedIn",
"punchAck"."isAck" AS "punchAck_isAck",
"punchAck"."userId" AS "punchAck_userId",
"punchAck"."dateTime" AS "punchAck_dateTime",
"punchAck"."rejectComment" AS "punchAck_rejectComment",
"punchAck"."attendanceId" AS "punchAck_attendanceId",
"verification"."id" AS "verification_id",
"verification"."status" AS "verification_status",
"verification"."punchIn" AS "verification_punchIn",
"verification"."punchOut" AS "verification_punchOut",
"verification"."supervisorId" AS "verification_supervisorId",
"verification"."attendanceId" AS "verification_attendanceId",
"verification"."comment" AS "verification_comment",
"verification"."createdAt" AS "verification_createdAt"
FROM
"tenant"."attendance" "attendance"
LEFT JOIN "tenant"."users" "user" ON "user"."id" = "attendance"."userId"
LEFT JOIN "tenant"."outlet" "outlet" ON "outlet"."id" = "attendance"."outletId"
LEFT JOIN "tenant"."punch_ack" "punchAck" ON "punchAck"."attendanceId" = "attendance"."id"
LEFT JOIN "tenant"."attendance_verification" "verification" ON
WHERE
"attendance"."punchInDateTime" BETWEEN $ 1
AND $ 2
ORDER BY
"attendance"."id" ASC
PARAMETERS:[
"2021-02-28T19:00:00.000Z",
"2021-03-31T18:59:59.000Z"
]
AttendanceEntity (Exluded some fields to make code clean)
import { ApiPropertyOptional } from '#nestjs/swagger'
import {
BaseEntity,
Column,
Entity,
ManyToOne,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm'
import { AttendanceVerification } from '#modules/attendance/attendance-verification/attendance_verification.entity'
import { Break } from '#modules/attendance/break/break.entity'
import { PunchAck } from '#modules/attendance/punch-ack/punch_ack.entity'
import { Outlet } from '#modules/outlet/outlet.entity'
import { User } from '#modules/users/users.entity'
#Entity('attendance')
export class Attendance extends BaseEntity {
#ApiPropertyOptional()
#PrimaryGeneratedColumn()
id: number
#ApiPropertyOptional()
#Column()
punchInImage: string
#ApiPropertyOptional()
#Column({ type: 'timestamp' })
punchInDateTime: Date
#ApiPropertyOptional()
#Column()
approvalStatus: string
#ApiPropertyOptional()
#Column()
userId: number
#ApiPropertyOptional()
#Column()
outletId: number
#ManyToOne(type => User, user => user.attendance)
user: User
#OneToOne(type => Outlet, outlet => outlet.attendance)
outlet: Outlet
#OneToMany(type => Break, breaks => breaks.attendance)
breaks: Break[]
#OneToMany(type => PunchAck, punchAck => punchAck.attendance)
punchAck: PunchAck[]
#OneToOne(
type => AttendanceVerification,
attendanceVerification => attendanceVerification.attendance
)
attendanceVerification: AttendanceVerification
}
All solutions I find on internet uses queryBuilder. I don't want to use that lengthy method.
From typeorm docs: https://typeorm.io/#/find-options
You can use something like:
userRepository.find({
join: {
alias: "user",
leftJoinAndSelect: {
profile: "user.profile",
photo: "user.photos",
video: "user.videos"
}
}
});
Or your example may look like:
const returnedResult = await this.attendanceRepository.findOne({
where: { userId: id, status: 'PUNCHED IN' },
join: {
alias: "attendance",
leftJoinAndSelect: {
user: "attendance.user",
outlet: "attendance.outlet",
}
},
});
From my personal experience if you'd need to write complex queries you have to work with QueryBuilder. It looks awkward on the first glance only.

How can I retrieve an id from MongoDB create operation during a transaction?

I am trying to create an audit trail using Apollo Server and Mongoose. When a user initially registers, I create a document in the users collection and a document in the history collection for each piece of data they provided (username, password, email, etc) . For each history collection document, I include the id for the user document to create a relationship. Works perfectly.
However, when I add a transaction in (see below), the userId for the user document comes back as undefined, so I cannot add it to the history entry documents. I am assuming that the id for a document does not get created until the entire transaction has been completed?
Any ideas?
Mutation: {
register: async (_, { data }) => {
// Start a mongo session & transaction
const session = await mongoose.startSession()
session.startTransaction()
try {
// Hash password and create user
const hashedPassword = await bcrypt.hash(data.password, 12)
const user = await User.create(
[{ ...data, password: hashedPassword }],
{ session }
)
// Add history entries
HistoryEntry.create([
{
user: user.id,
action: 'registered'
},
{
user: user.id,
action: 'set',
object: 'profile',
instance: user.id,
property: 'firstName',
value: firstName
},
{
user: user.id,
action: 'set',
object: 'profile',
instance: user.id,
property: 'lastName',
value: lastName
},
{
user: user.id,
action: 'set',
object: 'profile',
instance: user.id,
property: 'password'
}
])
if (loginType === 'email') {
HistoryEntry.create({
user: user.id,
action: 'set',
object: 'profile',
instance: user.id,
property: 'email',
value: login
})
}
if (loginType === 'mobile') {
HistoryEntry.create({
user: user.id,
action: 'set',
object: 'profile',
instance: user.id,
property: 'mobile',
value: login
})
}
// commit the changes if everything was successful
await session.commitTransaction()
return {
ok: true,
user
}
} catch (err) {
// if anything fails above, rollback the changes in the transaction
await session.abortTransaction()
return formatErrors(err)
} finally {
// end the session
session.endSession()
}
}
}
If you think about it, how can you add a HistoryEntry if you haven't added User yet? It's not a 'history' as you are currently doing it. I believe you got two options here - set _id on User manually new Schema({ _id: { type: Schema.ObjectId, auto: true }}) and then generate it within the transaction: var userId = ObjectId(); and use for both User and History Entries.
And the second option, more semantically correct in this context, I believe - you should attach to post-save hook:
schema.post('save', function(doc) {
console.log('%s has been saved', doc._id);
});
So, whenever an User is created, a post-save hook is fired to update History.
Came across the same issue recently, hope you have figured it out already. I may add this for future seekers.
Following create function returns an array of created documents.
const user = await User.create(
[{ ...data, password: hashedPassword }],
{ session }
);
Therefore access the user id as user[0]._id
Pass the session also to HistoryEntry.create()
HistoryEntry.create([{...},{...}], {session})
Note: In this use case, I personally prefer #marek second option to use a post-save hook.

Serverless Framework and DynamoDB: Unexpected token in JSON at JSON.parse

Hi I followed this Serverless + AWS REST API tutorial and it went great, I got it to work.
Now, I'm trying to modify it but have hit a wall while trying to submit data into the DynamoDB table.
Using Postman to submit a valid JSON object I get a 502 response. If I test the function in Lambda, I get the following error:
{
"errorType": "SyntaxError",
"errorMessage": "Unexpected token o in JSON at position 1",
"trace": [
"SyntaxError: Unexpected token o in JSON at position 1",
" at JSON.parse (<anonymous>)",
" at Runtime.module.exports.submit [as handler] (/var/task/api/interview.js:11:28)",
" at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)",
" at process._tickCallback (internal/process/next_tick.js:68:7)"
]
}
After searching for solutions, what I found out is that it seem like the event that is being passed as JSON.parse(event)is undefined.
Here's the serverless.yml:
service: interview
frameworkVersion: ">=1.1.0 <2.0.0"
provider:
name: aws
runtime: nodejs10.x
stage: dev
region: us-east-1
environment:
INTERVIEW_TABLE: ${self:service}-${opt:stage, self:provider.stage}
INTERVIEW_EMAIL_TABLE: "interview-email-${opt:stage, self:provider.stage}"
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
Resource: "*"
resources:
Resources:
CandidatesDynamoDbTable:
Type: 'AWS::DynamoDB::Table'
DeletionPolicy: Retain
Properties:
AttributeDefinitions:
-
AttributeName: "id"
AttributeType: "S"
KeySchema:
-
AttributeName: "id"
KeyType: "HASH"
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
StreamSpecification:
StreamViewType: "NEW_AND_OLD_IMAGES"
TableName: ${self:provider.environment.INTERVIEW_TABLE}
functions:
interviewSubmission:
handler: api/interview.submit
memorySize: 128
description: Submit interview information and starts interview process.
events:
- http:
path: interviews
method: post
and the interview.js
'use strict';
const uuid = require('uuid');
const AWS = require('aws-sdk');
AWS.config.setPromisesDependency(require('bluebird'));
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.submit = (event, context, callback) => {
const requestBody = JSON.parse(event);
const fullname = requestBody.fullname;
const email = requestBody.email;
const test = requestBody.test;
const experience = requestBody.experience;
if (typeof fullname !== 'string' || typeof email !== 'string' || typeof experience !== 'number') {
console.error('Validation Failed');
callback(new Error('Couldn\'t submit interview because of validation errors.'));
return;
}
submitInterviewP(interviewInfo(fullname, email, experience, test))
.then(res => {
callback(null, {
statusCode: 200,
body: JSON.stringify({
message: `Sucessfully submitted interview with email ${email}`,
interviewId: res.id
})
});
})
.catch(err => {
console.log(err);
callback(null, {
statusCode: 500,
body: JSON.stringify({
message: `Unable to submit interview with email ${email}`
})
})
});
};
const submitInterviewP = interview => {
console.log('Submitting interview');
const interviewInfo = {
TableName: process.env.INTERVIEW_TABLE,
Item: interview,
};
return dynamoDb.put(interviewInfo).promise()
.then(res => interview);
};
const interviewInfo = (fullname, email, experience,test) => {
const timestamp = new Date().getTime();
return {
id: uuid.v1(),
fullname: fullname,
email: email,
experience: experience,
test: test,
submittedAt: timestamp,
updatedAt: timestamp,
};
};
If I replace the event param for a valid JSON object and then deploy again. I'm able to successfully insert the object into dynamoDB.
Any clues? Please let me know if there's anything I missing that could help.
Thanks!
API Gateway stringify the request body in event's body property.
Currently you are trying to parse event object const requestBody = JSON.parse(event); which is wrong. You need to parse event.body property:
const requestBody = JSON.parse(event.body);

Sequelize.js - How to create non-trivial associations without raw SQL?

Here is my situation:
I'm using postgres 9.4, Sequelize ORM and have following models:
Service
serviceCode - primary key, string of 6 characters
serviceTitle - string
ServiceGroup
serviceCodePrefixes - array of strings that are prefixes for Service.serviceCode
serviceGroupTitle - string
Task
serviceCode - reference to Service
I need to build Task object populated with Service and ServiceGroup objects. Example:
In database:
Service {
serviceCode: '123232',
serviceTitle: 'svc title #1',
}
ServiceGroup {
serviceCodePrefix: ['12', '13', '92', ...],
serviceGroupTitle: 'svc grp title #1',
}
Task {
serviceCode: '123232',
}
Result:
Task {
service: {
serviceTitle: 'svc title #1',
},
serviceGroup: {
serviceGroupTitle: 'svc grp title #1',
},
}
The problem is that serviceCodePrefix contains not simple IDs, which can be used to create association using hasOne/belongsTo/etc., but prefix for ID.
So questions is: how this can be done without raw sql?
Turns out that right now Sequelize has experimental feature: 'on' option for 'include'. This option allows users to customize joining conditions. So my problem can be solved this way:
const Service = sequelize.define('service', {
serviceTitle: Sequelize.STRING,
serviceCode: Sequelize.STRING,
});
const ServiceGroup = sequelize.define('service_group', {
serviceGroupTitle: Sequelize.STRING,
// Array of prefixes (e.g. ['01%', '023%'])
serviceCodePrefix: Sequelize.ARRAY(Sequelize.STRING),
});
const Task = sequelize.define('task', {
taskTitle: Sequelize.STRING,
serviceCode: Sequelize.STRING,
});
Task.belongsTo(Service, { foreignKey: 'serviceCode' });
// Hack needed to allow 'include' option to work
Task.hasMany(ServiceGroup, { foreignKey: 'serviceCodePrefix', constraints: false });
// And finally
Task.findAll({
include: [
{ model: Service },
{
model: ServiceGroup,
on: [' "task"."serviceCode" LIKE ANY("serviceGroup"."serviceCodePrefix") '],
},
],
});
Not sure about the performance though.

Sails.js controller not inserting into Mongo database

I've been all over SO and Sailsjs.org trying to figure out what's going wrong, and to no avail. Just trying to learn the basics of SailsJS. I have a UserController, whose create() method gets called when a POST request is sent to /user.
create: function (req, res) {
var params = req.params.all();
User.create({
name: params.FirstName + ' ' + params.LastName,
email: params.Email,
password: params.Password,
jobTitle: params.JobTitle
}).exec(function createCB(err,created)
{
created.save(function(err)
{
// No error . . . still nothing in db
});
return res.json({name: created.name, jobTitle: created.jobTitle, email: created.email, password: created.password});
});
}
No errors here. All the request params are coming in fine and going back to the client without trouble. But nothing is actually being written to the database.
In development.js:
connections: {
mongo: {
adapter: 'sails-mongo',
host: 'localhost',
port: 27017,
// user: 'username',
// password: 'password',
database: 'sails_test'
}
},
models: {
connection: 'mongo'
}
I've tried this with the above both there in development.js, as well as separately in connections.js and models.js, respectively. No difference.
In User.js:
attributes: {
FirstName : { type: 'string' },
LastName : { type: 'string' },
Email : { type: 'string' },
Password : { type: 'string' },
JobTitle : { type: 'string' }
}
My front end request:
$.ajax({
method: 'post',
url: '/user',
data: {
FirstName: 'Yo',
LastName: 'Momma',
Email: 'yourmom#yourdadshouse.com',
Password: 'YouWish123',
JobTitle: 'Home Maker Extraordinaire'
},
success: function (sailsResponse)
{
$('#result').html(sailsResponse).fadeIn();
},
error: function()
{
console.log('error');
}
});
Again, none of this is producing an explicit error. There is just nothing being inserted into the database. Or if there is, I don't know how to find it. I've confirmed the existence of this db in the mongo shell, thusly:
show dbs
My db, sails_test shows up in the list. And I've confirmed that there isn't anything in it like so:
db.sails_test.find()
I would very much appreciate some guidance here :)
Update:
Turns out the data is being written just fine. I'm just unable to query the database from the command line. I confirmed this by first creating a sample user, and then using Waterline's findOne() method:
User.findOne({FirstName: params.FirstName}).exec(function (err, user) {
if (err) {
res.send(400);
} else if (user) {
return res.json({firstName: user.FirstName, lastName: user.LastName, jobTitle: user.JobTitle, email: user.Email, password: user.Password});
} else {
return res.send('no users match those criteria');
}
});
The above works as expected. So my problem now is simply that I cannot interact with the database from the command line. db.<collectionName>.find({}) produces nothing.
This was simply a failure to understand the MongoDb docs. I read db.collection.find({}) as DatabaseName.CollectionName.find({}), when you literally need to use db. So if my database is Test, and my collection is Users, the query is use Test, and then db.Users.find({}).
Also of note, 3T Mongo Chef is a pretty rockin' GUI (graphical user interface) for nosql databases, and it's free for non-commercial use.