I'm trying to create a nearby searching platform with keystone.js
But the nearby queries not working with the keystone js.
How I can run mongoose $nearSphere query or aggregation queries in the keystone?
Hereby added my best try and errors.
Index.js
require('dotenv').config({
path: './.env'
});
const {
Keystone
} = require('#keystonejs/keystone');
const {
GraphQLApp
} = require('#keystonejs/app-graphql');
const {
AdminUIApp
} = require('#keystonejs/app-admin-ui');
const {
PasswordAuthStrategy
} = require('#keystonejs/auth-password');
const {
MongooseAdapter
} = require('#keystonejs/adapter-mongoose');
const initialiseData = require('./initial-data');
const {
Post,
User,
Category,
SubCategory,
Comment,
ForgottenPasswordToken,
} = require('./schema');
const PROJECT_NAME = 'rentospot';
const keystone = new Keystone({
appVersion: {
version: '1.0.0',
addVersionToHttpHeaders: false,
access: false,
},
cookie: {
secure: process.env.NODE_ENV === 'production', // Default to true in production
maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days
sameSite: false,
},
cookieSecret: process.env.COOKIE_SECRET,
adapter: new MongooseAdapter({
mongoUri: 'mongodb+srv://*******:******.mongodb.net/my-app-key-stone'
}),
onConnect: process.env.CREATE_TABLES !== 'true' && initialiseData,
});
const postList = keystone.createList('Post', Post);
keystone.createList('Category', Category);
keystone.createList('SubCategory', SubCategory);
keystone.createList('Comment', Comment);
keystone.createList('User', User);
keystone.createList('ForgottenPasswordToken', ForgottenPasswordToken);
const authStrategy = keystone.createAuthStrategy({
type: PasswordAuthStrategy,
list: 'User',
});
const extendSchema = {
queries: [{
schema: 'nearBy: [Post]',
resolver: async(_) => {
try {
const {
adapter
} = postList;
const result = await adapter.find({
// "feature": true,
"venue.location": {
"$nearSphere": {
"$geometry": {
"type": "Point",
"coordinates": [12, 75],
},
"$minDistance": 0,
"$maxDistance": 500,
},
}
});
console.log(result)
return result;
} catch (e) {
console.log("errrrrrrrrrrrrrror: ", e)
return null
}
},
}, ]
};
keystone.extendGraphQLSchema(extendSchema)
module.exports = {
keystone,
apps: [new GraphQLApp(),
new AdminUIApp({
name: PROJECT_NAME,
enableDefaultRoute: true,
authStrategy
})
],
};
Schema.js
const fetch = require('node-fetch');
const { v4: uuid } = require('uuid');
const { sendEmail } = require('./emails');
const {
File,
Text,
Slug,
Relationship,
Select,
Password,
Checkbox,
CalendarDay,
DateTime,
Integer
} = require('#keystonejs/fields');
const { S3Adapter } = require('#keystonejs/file-adapters');
const { AuthedRelationship } = require('#keystonejs/fields-authed-relationship');
const { formatISO } = require('date-fns');
const { Wysiwyg } = require('#keystonejs/fields-wysiwyg-tinymce');
const LocationGoogle = require('./LocationGoogle');
const fileAdapter = new S3Adapter({
bucket: process.env.BUCKET_NAME,
folder: process.env.S3_PATH,
publicUrl: ({ filename }) =>
// `https://${process.env.CF_DISTRIBUTION_ID}.cloudfront.net/${process.env.S3_PATH}/${filename}`,
`https://${process.env.CF_DISTRIBUTION_ID}/${process.env.S3_PATH}/${filename}`,
s3Options: {
// Optional paramaters to be supplied directly to AWS.S3 constructor
apiVersion: '2006-03-01',
accessKeyId: process.env.API_KEY_ID,
secretAccessKey: process.env.SECRET_ACCESS_KEY,
region: process.env.REGION
},
uploadParams: ({ id }) => ({
Metadata: {
keystone_id: `${id}`,
},
}),
});
// Access control functions
const userIsAdmin = ({ authentication: { item: user } }) => Boolean(user && user.isAdmin);
const userOwnsItem = ({ authentication: { item: user } }) => {
if (!user) {
return false;
}
return { id: user.id };
};
const userIsCurrentAuth = ({ authentication: { item: user } }) => Boolean(user); // item will be undefined for anonymous user
const userIsAdminOrOwner = auth => {
const isAdmin = access.userIsAdmin(auth);
const isOwner = access.userOwnsItem(auth);
return isAdmin ? isAdmin : isOwner;
};
const access = { userIsAdmin, userIsCurrentAuth, userOwnsItem, userIsAdminOrOwner };
// Read: public / Write: admin
const DEFAULT_LIST_ACCESS = {
create: access.userIsCurrentAuth,
read: true,
update: access.userIsAdminOrOwner,
delete: access.userIsAdmin,
};
exports.User = {
access: {
update: access.userIsCurrentAuth,
delete: access.userIsAdmin,
},
fields: {
name: { type: Text },
dob: {
type: CalendarDay,
format: 'do MMMM yyyy',
dateFrom: '1901-01-01',
dateTo: formatISO(new Date(), { representation: 'date' }),
},
phone: {
type: Text,
isUnique: true,
access: { read: access.userIsCurrentAuth },
// hooks: {
// validateInput:
// }
},
email: { type: Text, isUnique: true, access: { read: access.userIsCurrentAuth } },
password: { type: Password, isRequired: true },
isAdmin: { type: Checkbox, access: { update: access.userIsAdmin } },
twitterHandle: { type: Text },
image: { type: File, adapter: fileAdapter },
},
hooks: {
afterChange: async ({ updatedItem, existingItem }) => {
if (existingItem && updatedItem.password !== existingItem.password) {
const url = process.env.SERVER_URL || 'http://localhost:3000';
const props = {
recipientEmail: updatedItem.email,
signinUrl: `${url}/signin`,
};
const options = {
subject: 'Your password has been updated',
to: updatedItem,
from: process.env.MAILGUN_FROM,
domain: process.env.MAILGUN_DOMAIN,
apiKey: process.env.MAILGUN_API_KEY,
};
await sendEmail('password-updated.jsx', props, options);
}
},
},
};
// exports.Organiser = {
// access: DEFAULT_LIST_ACCESS,
// fields: {
// user: { type: Relationship, ref: 'User' },
// order: { type: Integer },
// role: { type: Text },
// },
// };
// TODO: We can't access the existing item at the list update level yet,
// read access needs to check if event is "active" or if the user is admin
// read: ({ existingItem, authentication }) => access.userIsAdmin({ authentication }) || !!(existingItem && existingItem.status === 'active'),
exports.Post = {
access: DEFAULT_LIST_ACCESS,
fields: {
title: { type: Text },
slug: { type: Slug, from: 'title' },
author: {
type: AuthedRelationship,
ref: 'User',
isRequired: true,
access: {
read: userIsCurrentAuth,
update: userIsAdmin,
},
},
status: {
type: Select,
defaultValue: 'published',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
},
feature: { type: Checkbox, access: { update: access.userIsAdmin } },
assured: { type: Checkbox, access: { update: access.userIsAdmin } },
posted: { type: DateTime, format: 'dd/MM/yyyy' },
categories: {
type: Relationship,
ref: 'Category',
many: false,
},
subCategories: {
type: Relationship,
ref: 'SubCategory',
many: true,
},
description: { type: Wysiwyg },
venue: {
type: LocationGoogle,
googleMapsKey: process.env.GOOGLE_MAPS_KEY,
// hooks: {
// resolveInput: async ({ originalInput }) => {
// try {
// const placeId = originalInput.venue;
// if (typeof placeId === 'undefined') {
// // Nothing was passed in, so we can bail early.
// return undefined;
// }
// const r = await fetch(
// `https://maps.googleapis.com/maps/api/geocode/json?place_id=${placeId}&key=${process.env.GOOGLE_MAPS_KEY}`
// )
// const response = await r.json()
// if (response.results && response.results[0]) {
// const { place_id, formatted_address } = response.results[0];
// const { lat, lng } = response.results[0].geometry.location;
// return {
// googlePlaceID: place_id,
// formattedAddress: formatted_address,
// lat,
// lng,
// loc: {
// type: "Point",
// coordinates: [lng, lat]
// }
// };
// }
// return null;
// }
// catch (e) {
// console.log("error:", e)
// }
// },
// validateInput: async (x) => {
// console.log("---------------------------------item 2-------------------------------------- : ",x);
// },
// beforeChange: async (x) => {
// console.log("========", x)
// },
// afterChange: async (x) => {
// console.log("++++", x)
// }
// },
},
locationAddress: { type: Text },
locationDescription: { type: Text },
createdby: {
type: AuthedRelationship,
ref: 'User',
isRequired: true,
access: {
read: userIsCurrentAuth,
update: userIsAdmin,
},
},
image: {
type: File,
adapter: fileAdapter,
hooks: {
beforeChange: async ({ existingItem }) => {
if (existingItem && existingItem.image) {
await fileAdapter.delete(existingItem.image);
}
},
},
},
},
hooks: {
afterDelete: ({ existingItem }) => {
if (existingItem.image) {
fileAdapter.delete(existingItem.image);
}
},
},
adminConfig: {
defaultPageSize: 20,
defaultColumns: 'title, status',
defaultSort: 'title',
},
labelResolver: item => item.title,
};
exports.Category = {
// access: userIsAdmin,
fields: {
name: { type: Text },
slug: { type: Slug, from: 'name' },
image: {
type: File,
adapter: fileAdapter,
hooks: {
beforeChange: async ({ existingItem }) => {
if (existingItem && existingItem.image) {
await fileAdapter.delete(existingItem.image);
}
},
},
},
},
};
exports.SubCategory = {
// access: userIsAdmin,
fields: {
name: { type: Text },
slug: { type: Slug, from: 'name' },
categories: {
type: Relationship,
ref: 'Category',
many: false,
},
image: {
type: File,
adapter: fileAdapter,
hooks: {
beforeChange: async ({ existingItem }) => {
if (existingItem && existingItem.image) {
await fileAdapter.delete(existingItem.image);
}
},
},
},
},
};
exports.Comment = {
access: {
create: userIsCurrentAuth,
update: userIsAdminOrOwner,
},
fields: {
body: { type: Text, isMultiline: true },
originalPost: {
type: Relationship,
ref: 'Post',
},
author: {
type: AuthedRelationship,
ref: 'User',
isRequired: true,
access: {
create: userIsAdmin,
update: userIsAdmin,
},
},
posted: { type: CalendarDay },
},
labelResolver: item => item.body,
};
// exports.Talk = {
// access: DEFAULT_LIST_ACCESS,
// fields: {
// name: { type: Text },
// event: { type: Relationship, ref: 'Event.talks' },
// speakers: { type: Relationship, ref: 'User.talks', many: true },
// isLightningTalk: { type: Checkbox },
// description: { type: Wysiwyg },
// },
// };
// exports.Rsvp = {
// access: {
// create: true,
// read: true,
// update: ({ authentication: { item } }) => {
// if (!item) {
// return false;
// }
// return { user: { id: item.id } };
// },
// delete: access.userIsAdmin,
// },
// fields: {
// event: { type: Relationship, ref: 'Event' },
// user: { type: Relationship, ref: 'User' },
// status: { type: Select, options: 'yes, no' },
// },
// hooks: {
// validateInput: async ({ context, resolvedData, existingItem }) => {
// const { status } = resolvedData;
// const { event: eventId } = existingItem ? existingItem : resolvedData;
// if (status === 'no') {
// return;
// }
// const { data } = await context.executeGraphQL({
// query: `query {
// event: Event(where: { id: "${eventId}" }) {
// id
// startTime
// maxRsvps
// isRsvpAvailable
// }
// allRsvps(where: { event: { id: "${eventId}" }}) {
// id
// }
// }`,
// });
// const { event, allRsvps } = data;
// if (
// !event ||
// !event.isRsvpAvailable ||
// !event.startTime ||
// new Date() > new Date(event.startTime) ||
// allRsvps.length >= event.maxRsvps
// ) {
// throw 'Error rsvping to event';
// }
// },
// },
// };
// exports.Sponsor = {
// access: DEFAULT_LIST_ACCESS,
// fields: {
// name: { type: Text },
// website: { type: Text },
// logo: { type: CloudinaryImage, adapter: cloudinaryAdapter },
// },
// };
exports.ForgottenPasswordToken = {
access: {
create: true,
read: true,
update: access.userIsAdmin,
delete: access.userIsAdmin,
},
fields: {
user: {
type: Relationship,
ref: 'User',
access: {
read: access.userIsAdmin,
},
},
token: {
type: Text,
isRequired: true,
isUnique: true,
access: {
read: access.userIsAdmin,
},
},
requestedAt: { type: DateTime, isRequired: true },
accessedAt: { type: DateTime },
expiresAt: { type: DateTime, isRequired: true },
},
hooks: {
afterChange: async ({ context, updatedItem, existingItem }) => {
if (existingItem) return null;
const now = new Date().toISOString();
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query GetUserAndToken($user: ID!, $now: DateTime!) {
User( where: { id: $user }) {
id
email
}
allForgottenPasswordTokens( where: { user: { id: $user }, expiresAt_gte: $now }) {
token
expiresAt
}
}
`,
variables: { user: updatedItem.user.toString(), now },
});
if (errors) {
console.error(errors, `Unable to construct password updated email.`);
return;
}
const { allForgottenPasswordTokens, User } = data;
const forgotPasswordKey = allForgottenPasswordTokens[0].token;
const url = process.env.SERVER_URL || 'http://localhost:3000';
const props = {
forgotPasswordUrl: `${url}/change-password?key=${forgotPasswordKey}`,
recipientEmail: User.email,
};
const options = {
subject: 'Request for password reset',
to: User.email,
from: process.env.MAILGUN_FROM,
domain: process.env.MAILGUN_DOMAIN,
apiKey: process.env.MAILGUN_API_KEY,
};
await sendEmail('forgot-password.jsx', props, options);
},
},
};
exports.customSchema = {
mutations: [
{
schema: 'startPasswordRecovery(email: String!): ForgottenPasswordToken',
resolver: async (obj, { email }, context) => {
const token = uuid();
const tokenExpiration =
parseInt(process.env.RESET_PASSWORD_TOKEN_EXPIRY) || 1000 * 60 * 60 * 24;
const now = Date.now();
const requestedAt = new Date(now).toISOString();
const expiresAt = new Date(now + tokenExpiration).toISOString();
const { errors: userErrors, data: userData } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserByEmail($email: String!) {
allUsers(where: { email: $email }) {
id
email
}
}
`,
variables: { email: email },
});
if (userErrors || !userData.allUsers || !userData.allUsers.length) {
console.error(
userErrors,
`Unable to find user when trying to create forgotten password token.`
);
return;
}
const userId = userData.allUsers[0].id;
const result = {
userId,
token,
requestedAt,
expiresAt,
};
const { errors } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
mutation createForgottenPasswordToken(
$userId: ID!,
$token: String,
$requestedAt: DateTime,
$expiresAt: DateTime,
) {
createForgottenPasswordToken(data: {
user: { connect: { id: $userId }},
token: $token,
requestedAt: $requestedAt,
expiresAt: $expiresAt,
}) {
id
token
user {
id
}
requestedAt
expiresAt
}
}
`,
variables: result,
});
if (errors) {
console.error(errors, `Unable to create forgotten password token.`);
return;
}
return true;
},
},
{
schema: 'changePasswordWithToken(token: String!, password: String!): User',
resolver: async (obj, { token, password }, context) => {
const now = Date.now();
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromToken($token: String!, $now: DateTime!) {
passwordTokens: allForgottenPasswordTokens(where: { token: $token, expiresAt_gte: $now }) {
id
token
user {
id
}
}
}`,
variables: { token, now },
});
if (errors || !data.passwordTokens || !data.passwordTokens.length) {
console.error(errors, `Unable to find token`);
throw errors.message;
}
const user = data.passwordTokens[0].user.id;
const tokenId = data.passwordTokens[0].id;
const { errors: passwordError } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `mutation UpdateUserPassword($user: ID!, $password: String!) {
updateUser(id: $user, data: { password: $password }) {
id
}
}`,
variables: { user, password },
});
if (passwordError) {
console.error(passwordError, `Unable to change password`);
throw passwordError.message;
}
await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `mutation DeletePasswordToken($tokenId: ID!) {
deleteForgottenPasswordToken(id: $tokenId) {
id
}
}
`,
variables: { tokenId },
});
return true;
},
},
],
};
LocationGoogle is a directory of the files exactly #keystonejs/fields-location-google and small changes in its Implementation.js
Implementation.js
const { Implementation } = require('#keystonejs/fields');
const { MongooseFieldAdapter } = require('#keystonejs/adapter-mongoose');
const fetch = require('node-fetch');
// Disabling the getter of mongoose >= 5.1.0
// https://github.com/Automattic/mongoose/blob/master/migrating_to_5.md#checking-if-a-path-is-populated
class LocationGoogleImplementation extends Implementation {
constructor(_, { googleMapsKey }) {
super(...arguments);
this.graphQLOutputType = 'LocationGoogle';
if (!googleMapsKey) {
throw new Error(
'You must provide a `googleMapsKey` to LocationGoogle Field. To generate a Google Maps API please visit: https://developers.google.com/maps/documentation/javascript/get-api-key'
);
}
this._googleMapsKey = googleMapsKey;
}
get _supportsUnique() {
return false;
}
extendAdminMeta(meta) {
return {
...meta,
googleMapsKey: this._googleMapsKey,
};
}
gqlOutputFields() {
return [`${this.path}: ${this.graphQLOutputType}`];
}
gqlQueryInputFields() {
return [...this.equalityInputFields('String'), ...this.inInputFields('String')];
}
getGqlAuxTypes() {
return [
`
type Location{
coordinates: [Float]
},
type ${this.graphQLOutputType} {
location: Location
googlePlaceID: String
formattedAddress: String
lat: Float
lng: Float
}
`,
];
}
// Called on `User.avatar` for example
gqlOutputFieldResolvers() {
return {
[this.path]: item => {
const itemValues = item[this.path];
if (!itemValues) {
return null;
}
return itemValues;
},
};
}
async resolveInput({ resolvedData }) {
const placeId = resolvedData[this.path];
// NOTE: The following two conditions could easily be combined into a
// single `if (!inputId) return inputId`, but that would lose the nuance of
// returning `undefined` vs `null`.
// Premature Optimisers; be ware!
if (typeof placeId === 'undefined') {
// Nothing was passed in, so we can bail early.
return undefined;
}
if (placeId === null) {
// `null` was specifically set, and we should set the field value to null
// To do that we... return `null`
return null;
}
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?place_id=${placeId}&key=${this._googleMapsKey}`
).then(r => r.json());
if (response.results && response.results[0]) {
const { place_id, formatted_address } = response.results[0];
const { lat, lng } = response.results[0].geometry.location;
return {
location: {
type: "Point",
coordinates: [lng, lat]
},
googlePlaceID: place_id,
formattedAddress: formatted_address,
lat: lat,
lng: lng,
};
}
return null;
}
gqlUpdateInputFields() {
return [`${this.path}: String`];
}
gqlCreateInputFields() {
return [`${this.path}: String`];
}
getBackingTypes() {
const type = `null | {
location: {
type: "Point",
coordinates: [lng, lat]
};
googlePlaceID: string;
formattedAddress: string;
lat: number;
lng: number;
}
`;
return { [this.path]: { optional: true, type } };
}
}
const CommonLocationInterface = superclass =>
class extends superclass {
getQueryConditions(dbPath) {
return {
...this.equalityConditions(dbPath),
...this.inConditions(dbPath),
};
}
};
class MongoLocationGoogleInterface extends CommonLocationInterface(MongooseFieldAdapter) {
addToMongooseSchema(schema) {
const schemaOptions = {
type: {
location: {
type: { type: String, default: "Point" },
coordinates: [Number]
},
googlePlaceID: String,
formattedAddress: String,
lat: Number,
lng: Number,
},
};
schema.add({
[this.path]: this.mergeSchemaOptions(schemaOptions, this.config)
});
}
}
module.exports = {
LocationGoogleImplementation,
MongoLocationGoogleInterface
}
package.json
{
"name": "#keystonejs/example-projects-blank",
"description": "A blank KeystoneJS starter project.",
"private": true,
"version": "5.0.15",
"author": "The KeystoneJS Development Team",
"repository": "https://github.com/keystonejs/keystone/tree/master/packages/create-keystone-app/example-projects/blank",
"homepage": "https://github.com/keystonejs/keystone",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"scripts": {
"dev": "cross-env NODE_ENV=development ENABLE_DEV_FEATURES=false DISABLE_LOGGING=false nodemon --exec keystone dev --harmony",
"build": "cross-env NODE_ENV=production ENABLE_DEV_FEATURES=false keystone build",
"start": "cross-env NODE_ENV=production keystone start",
"create-tables": "cross-env keystone create-tables"
},
"dependencies": {
"#arch-ui/layout": "^0.2.14",
"#arch-ui/typography": "^0.0.18",
"#keystonejs/adapter-mongoose": "^10.1.2",
"#keystonejs/app-admin-ui": "^7.3.13",
"#keystonejs/app-graphql": "^6.2.1",
"#keystonejs/auth-password": "^6.0.0",
"#keystonejs/email": "^5.2.0",
"#keystonejs/fields-authed-relationship": "^1.0.16",
"#keystonejs/fields-location-google": "^3.2.1",
"#keystonejs/fields-wysiwyg-tinymce": "^5.3.15",
"#keystonejs/file-adapters": "^7.0.8",
"#keystonejs/keystone": "^18.1.0",
"cross-env": "^7.0.3",
"date-fns": "^2.16.1",
"dotenv": "^8.2.0",
"google-maps-react": "^2.0.6",
"node-fetch": "^2.6.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "^2.0.7"
}
}
The Error screenshot attached bellow
I'm having issues adding an array of objects to mongodb. The problem is related to when I get the posts ngOnInit() and there is an entry for _id like this before I start adding anything to inviteGroup
If I add this.inviteGroup = [] to get rid of _id first entry, then I can successfully add to the database my invite like in this image. Is there a way to not have that _id that's related to my mongoose schema?
but naturally the this.inviteGroup = [] makes it so I can have only one entry at a time since it erases everything on page load. How can I make that _id entry not there anymore so that when I do a .push() it doesn't cause a page reload because it throws off the .push(). I want to have multiple entries in db for each invite. Is it my mongoose model that's the issue? I appreciate any help!
mongoose schema definition
inviteGroup: {
bidderId: { type: String, lowercase: true, trim: true },
username: { type: String, lowercase: true, trim: true }
}
app.js
app.patch("/api/listings/:id", (req, res) => {
console.log("INVITE GRdddOUP IS");
console.log(req.body);
console.log(req.body[0].biddingUserId);
let invites;
if (req.body[0].biddingUserId) {
invites = req.body;
console.log("INVITE IS");
}
console.log(invites);
if (invites) {
console.log("INVITE GROUP IS");
console.log(req.params.id);
Post.findByIdAndUpdate(
{ _id: req.params.id },
{
inviteGroup: invites
},
function(err, docs) {
if (err) {
console.log(err);
res.json(err);
} else {
return true;
console.log(docs);
}
}
);
component.ts
import {
Component,
OnInit,
ViewChild,
OnDestroy,
AfterViewInit
} from "#angular/core";
import { Router } from "#angular/router";
import {
MatTableDataSource,
MatPaginator,
MatSort,
MatDialog
} from "#angular/material";
import { NgForm, FormControl } from "#angular/forms";
import { SubmitListingService } from "../submit-listing/submit-auction.service";
import { BidderInvite } from "./bidder-invite.model";
import { Observable, Subject } from "rxjs";
import { startWith, map, takeUntil } from "rxjs/operators";
import { Page } from "ngx-pagination/dist/pagination-controls.directive";
import { BidderInviteRetrieved } from "./bidder-invite-retrieved";
#Component({
selector: "app-private-auction-invite",
templateUrl: "./private-auction-invite.component.html",
styleUrls: ["./private-auction-invite.component.css"]
})
export class PrivateAuctionInviteComponent
implements OnInit, AfterViewInit, OnDestroy {
allMyPeopleAreInvited: boolean;
auctionId: string;
dataSource: MatTableDataSource<any> = new MatTableDataSource();
timeout: any = null;
posts: BidderInviteRetrieved[];
artistId: string;
bidderId: string;
inviteGroup: BidderInvite[] = [];
test: any[] = [];
value: string;
usernameFound: string;
userSearched: string;
invites: BidderInvite[] = [];
destroy = new Subject();
inviteName: string;
filteredOptions: Observable<string[]>;
myControl = new FormControl();
selectedValue: string;
url: string;
displayedColumnsInvites: string[] = ["User", "revokeInvite"];
options: string[] = [];
#ViewChild(MatSort, { static: false }) set sort(sort: MatSort) {
this.dataSource.sort = sort;
}
#ViewChild(MatPaginator, { static: false }) set paginator(
paginator: MatPaginator
) {
this.dataSource.paginator = paginator;
}
constructor(
private router: Router,
private submitListingService: SubmitListingService
) {}
ngOnInit() {
this.inviteGroup = [];
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.allMyPeopleAreInvited = false;
this.url = this.router.url;
const value = this.router.url.split("/");
this.auctionId = value[2];
this.artistId = value[3];
this.submitListingService
.getPrivateAuctionInviteList(this.auctionId)
.pipe(takeUntil(this.destroy))
.subscribe(res => {
this.inviteGroup = res.posts;
console.log("res");
console.log(res);
console.log(this.inviteGroup);
if (this.inviteGroup["_id"].length > 2) {
this.inviteGroup = [];
console.log(this.inviteGroup);
}
});
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(""),
map(value => this._filter(value))
);
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.dataSource = new MatTableDataSource(this.inviteGroup);
this.dataSource.data = this.inviteGroup;
}
sendInvite(form: NgForm) {
if (form.invalid) {
return;
}
let counter: number;
counter = 0;
console.log("USER " + this.value);
console.log("POST LEGNTH: " + this.posts.length);
for (let i = 0; i < this.posts.length; i++) {
counter = counter++;
console.log("post");
console.log(form.value.username);
let user = this.posts[i].username.trim().toLowerCase();
let enteredUser = form.value.username.trim().toLowerCase();
console.log("COUNTER LOOP NUMBER: " + counter);
if (enteredUser === user) {
this.bidderId = this.posts[i].id;
console.log(this.inviteGroup);
let invites = this.inviteGroup;
console.log("INVITE LENGTH = " + this.inviteGroup.length);
console.log(invites.indexOf);
this.inviteGroup.push({
biddingUserId: this.bidderId,
username: this.posts[i].username
});
console.log(this.inviteGroup);
console.log("invite group");
console.log(this.inviteGroup);
//this.posts = [];
this.dataSource.data = this.inviteGroup;
console.log("invite group");
}
}
console.log("BIDDER ID " + this.bidderId);
if (this.bidderId === null || this.bidderId === undefined) {
console.log("SOMETHING WENT WRONG");
}
console.log("made it to next section");
let invites = this.inviteGroup;
console.log("invites[0].username");
console.log("filtering....");
invites = invites.filter((obj, pos, arr) => {
return (
arr.map(mapObj => mapObj["bidderId"]).indexOf(obj["bidderId"]) === pos
);
});
console.log("invites");
console.log(invites);
this.submitListingService
.sendPrivateAuctionInvite(this.auctionId, invites)
.pipe(takeUntil(this.destroy))
.subscribe(res => {
console.log("res");
console.log(res);
});
}
private onKeySearch(event: any) {
console.log("EVENT IS ");
console.log(event);
clearTimeout(this.timeout);
var $this = this;
this.timeout = setTimeout(function() {
if (event.keyCode !== 13) {
$this.executeListing(event.target.value);
}
}, 1000);
}
private executeListing(bidderName: string) {
console.log("BIDDERNAME");
console.log(bidderName);
if (bidderName === "[Object object]") {
return;
}
if (bidderName.length < 4) {
return;
}
if (bidderName.length > 3) {
this.submitListingService
.getUserIdAutoComplete(bidderName)
.pipe(takeUntil(this.destroy))
.subscribe(res => {
console.log("res");
console.log(res);
this.posts = res.posts;
console.log(this.posts);
// this.artists = res.posts;
});
}
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
return this.options.filter(
option => option.toLowerCase().indexOf(filterValue) === 0
);
console.log("OPTION IS " + filterValue);
}
storeUserPrivaeAuctionInvite(user: Page) {
console.log("USER VALUE I S" + user);
}
ngOnDestroy() {
this.destroy.next();
this.destroy.complete();
}
}
angular service
sendPrivateAuctionInvite(id: string, inviteGroup1: BidderInvite[]) {
// console.log(inviteGroup1);
return this.http.patch(
`http://localhost:3000/api/listings/${id}/`,
inviteGroup1
);
}
BidderInvite model
export interface BidderInvite {
biddingUserId: string;
username: string;
}
Is your schema definition supposed to be:
inviteGroup: {
type: [inviteSchema]
default: undefined //if you want to unset []
}
invite: {
bidderId: { type: String, lowercase: true, trim: true },
username: { type: String, lowercase: true, trim: true }
}
(See https://mongoosejs.com/docs/schematypes.html#arrays)
Try caching to prevent a reload. (See https://github.com/isaacs/node-lru-cache) and How to stop MongoDB from reloading data every time I refresh a page?
Try projection to exclude _id in a query output with _id: 0. See https://docs.mongodb.com/v3.2/tutorial/project-fields-from-query-results/#return-all-but-the-excluded-field
Try this. Hope it will help you.
Post.findByIdAndUpdate(
{ _id: req.params.id },
{
inviteGroup: invites
},
{select: {_id: 0}}, // sets the document fields to return
function(err, docs) {
if (err) {
console.log(err);
res.json(err);
} else {
return true;
console.log(docs);
}
}
);
If I understood your question right, the problem is related to the lack of operator at the update method.
When you use something like:
Post.findByIdAndUpdate(
{ _id: req.params.id },
{
inviteGroup: invites // <-- Update statement
},
function(err, docs) {
//...
}
);
It will replace the full value of the inviteGroup field.
In order to add an item to an existent array on the database, you will need to use $push or $addToSet operator, along with $each operator.
The $push and $addToSet operators, only add/append one item per time, so the use of $each is necessary to interact with every item present on the invites array. In the following examples, I will include that because I believe is what you will need. But please, take the time to read the linked documentation of every operator so you can find more samples.
The $push operator appends a specified value to an array, making no extra verification if the value that is been added already exists on the field or not. As:
//document on mongodb, before the update
// { _id : "1", inviteGroup : [] }
//Invites from the request
// invites = [ { bidderId:"5e2350c7f88cfb331c4f67de", username:"artist1"} ];
//update method
Post.findByIdAndUpdate(
{ _id: req.params.id }, //req.params.id = "1"
{ $push : { inviteGroup: { $each : invites } } },
function(err, docs) {
//...
}
);
//document on mongodb, after the update
/*
{
_id : "1",
inviteGroup : [ { bidderId:"5e2350c7f88cfb331c4f67de", username:"artist1"} ]
}
*/
If you call the update method again with the same values:
Post.findByIdAndUpdate(
{ _id: req.params.id }, //req.params.id = "1"
{ $push : { inviteGroup: { $each : invites } } },
function(err, docs) { }
);
// the end document will be like:
/*
{
_id : "1",
inviteGroup : [
{ bidderId:"5e2350c7f88cfb331c4f67de", username:"artist1"},
{ bidderId:"5e2350c7f88cfb331c4f67de", username:"artist1"}
]
}
*/
In the same way, the $addToSet operator adds a value to an array unless the value is already present, in which case $addToSet does nothing to that array. Like:
//document on mongodb, before the update
// { _id : "1", inviteGroup : [] }
//Invites from the request
// invites = [ { bidderId:"5e2350c7f88cfb331c4f67de", username:"artist1"} ];
//update method
Post.findByIdAndUpdate(
{ _id: req.params.id }, //req.params.id = "1"
{ $addToSet : { inviteGroup: { $each : invites } } },
function(err, docs) {
//...
}
);
//document on mongodb, after the update
/*
{
_id : "1",
inviteGroup : [ { bidderId:"5e2350c7f88cfb331c4f67de", username:"artist1"} ]
}
*/
If you call the update method again with the same values:
Post.findByIdAndUpdate(
{ _id: req.params.id }, //req.params.id = "1"
{ $addToSet : { inviteGroup: { $each : invites } } },
function(err, docs) { }
);
//the end document will be the same because the same value was already on the list:
/*
{
_id : "1",
inviteGroup : [ { bidderId:"5e2350c7f88cfb331c4f67de", username:"artist1"} ]
}
*/
Well, I hope that was what you looking for. =]