json-api-query
Advanced tools
| import { IBookingException } from "./IBookingException"; | ||
| import { IProfileDetails } from "./IProfileDetails"; | ||
| import { IVenue } from "./IVenue"; | ||
| // export type RecurrenceFrequencyType = 'None' | 'Daily' |'Weekly' | 'Monthly' |'Yearly' | ||
| export enum RecurrenceFrequencyType{ | ||
| None = 0, | ||
| // Secondly = 1, | ||
| // Minutely = 2, | ||
| // Hourly = 3, | ||
| Daily = 4, | ||
| Weekly = 5, | ||
| Monthly = 6, | ||
| Yearly = 7, | ||
| } | ||
| export enum Frequency { | ||
| YEARLY = 0, | ||
| MONTHLY = 1, | ||
| WEEKLY = 2, | ||
| DAILY = 3, | ||
| HOURLY = 4, | ||
| MINUTELY = 5, | ||
| SECONDLY = 6 | ||
| } | ||
| // export enum RRulerFrequency { | ||
| // YEARLY = 0, | ||
| // MONTHLY = 1, | ||
| // WEEKLY = 2, | ||
| // DAILY = 3, | ||
| // HOURLY = 4, | ||
| // MINUTELY = 5, | ||
| // SECONDLY = 6 | ||
| // } | ||
| export class IBooking { | ||
| approvedBy: string; | ||
| id: string; | ||
| firstName: string; | ||
| lastName: string; | ||
| email: string; | ||
| stageName: string; | ||
| avatar: string; | ||
| dateRequested: Date; | ||
| approved: boolean; | ||
| rejected: boolean; | ||
| progress: number; | ||
| phone: string; | ||
| city: string; | ||
| artists: Array<IProfileDetails>; | ||
| recurrenceCount: number; | ||
| recurrenceFrequency: RecurrenceFrequencyType; | ||
| frequencyType: number; | ||
| startDate: string; | ||
| endDate: string; | ||
| duration: number; | ||
| venue: IVenue; | ||
| "booking-exceptions": Array<IBookingException>; | ||
| approvedDate: Date | ||
| calFile: string | ||
| recurrencePattern: string | ||
| } | ||
| import { IProfileDetails } from "./IProfileDetails"; | ||
| import { IBooking } from "./IBooking"; | ||
| export interface IBookingException { | ||
| id: string; | ||
| artists: Array<IProfileDetails>; | ||
| booking: IBooking; | ||
| exceptionDate: string; | ||
| "booking-id": string; | ||
| } |
| export class IConnectedAccounts { | ||
| google: boolean; | ||
| github: boolean; | ||
| stack: boolean; | ||
| } |
| export interface IDeactivateAccount { | ||
| confirm: boolean; | ||
| } |
| export interface IEmailPreferences { | ||
| successfulPayments: boolean; | ||
| payouts: boolean; | ||
| freeCollections: boolean; | ||
| customerPaymentDispute: boolean; | ||
| refundAlert: boolean; | ||
| invoicePayments: boolean; | ||
| webhookAPIEndpoints: boolean; | ||
| } |
| export interface IImage { | ||
| id: string; | ||
| url: string; | ||
| } |
| export interface INotifications { | ||
| notifications: { | ||
| email: boolean; | ||
| phone: boolean; | ||
| }; | ||
| billingUpdates: { | ||
| email: boolean; | ||
| phone: boolean; | ||
| }; | ||
| newTeamMembers: { | ||
| email: boolean; | ||
| phone: boolean; | ||
| }; | ||
| completeProjects: { | ||
| email: boolean; | ||
| phone: boolean; | ||
| }; | ||
| newsletters: { | ||
| email: boolean; | ||
| phone: boolean; | ||
| }; | ||
| } |
| import { ITown } from "./ITown"; | ||
| import { IImage } from "./IImage"; | ||
| import { IBooking } from "./IBooking"; | ||
| import { IBookingException } from "./IBookingException"; | ||
| export interface IProfileDetails { | ||
| id: string; | ||
| avatar: string; | ||
| firstName: string; | ||
| lastName: string; | ||
| email: string; | ||
| stageName: string; | ||
| isActive: boolean; | ||
| facebookUrl: string; | ||
| youTubeUrl: string; | ||
| instagramUrl: string; | ||
| legacyId: string; | ||
| images: IImage[]; | ||
| description: string; | ||
| mobile: string; | ||
| videoUrl: string; | ||
| biography: string; | ||
| keywords: string; | ||
| quote1: string; | ||
| quote2: string; | ||
| quote3: string; | ||
| quote4: string; | ||
| address1: string; | ||
| address2: string; | ||
| city: string; | ||
| postcode: string; | ||
| termsAcknowledged: boolean; | ||
| dbsApproved: boolean; | ||
| dbsExpires: Date; | ||
| pliApproved: boolean; | ||
| pliExpiry: Date; | ||
| caeApproved: boolean; | ||
| longitude: number; | ||
| latitude: number; | ||
| travelMiles: number; | ||
| rating: number; | ||
| town: ITown; | ||
| company: string; | ||
| contactPhone: string; | ||
| companySite: string; | ||
| country: string; | ||
| language: string; | ||
| timeZone: string; | ||
| currency: string; | ||
| communications: { | ||
| email: boolean | undefined; | ||
| phone: boolean | undefined; | ||
| }; | ||
| allowMarketing: boolean; | ||
| "booking-exceptions": IBookingException[] | ||
| } |
| export interface ITown { | ||
| id: string; | ||
| name: string; | ||
| } |
| export interface IUpdateEmail { | ||
| newEmail: string; | ||
| confirmPassword: string; | ||
| } |
| export interface IUpdatePassword { | ||
| currentPassword: string; | ||
| newPassword: string; | ||
| passwordConfirmation: string; | ||
| } |
| import { ITown } from "./ITown"; | ||
| export interface IVenue { | ||
| longitude: string; | ||
| latitude: string; | ||
| id: string; | ||
| name: string; | ||
| address1: string; | ||
| address2: string; | ||
| address3: string; | ||
| postCode: string; | ||
| email: string; | ||
| phone: string; | ||
| notes: string; | ||
| invoiceEmail: string; | ||
| xeroId: string; | ||
| town: ITown; | ||
| } |
| import { NestedTestClass } from "./NestedTestClass"; | ||
| export interface NestedNestedTestClass { | ||
| property1Nested: string; | ||
| property2Nested: boolean; | ||
| nested: NestedTestClass; | ||
| } |
| import { NestedNestedTestClass } from "./NestedNestedTestClass"; | ||
| export interface NestedTestClass { | ||
| property1Nested: string; | ||
| property2Nested: boolean; | ||
| nestedAgain: NestedNestedTestClass; | ||
| } |
| { | ||
| "$schema": "http://json-schema.org/draft-07/schema#", | ||
| "definitions": { | ||
| "Frequency": { | ||
| "enum": [ | ||
| 0, | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| 6 | ||
| ], | ||
| "type": "number" | ||
| }, | ||
| "IBooking": { | ||
| "properties": { | ||
| "approved": { | ||
| "type": "boolean" | ||
| }, | ||
| "approvedBy": { | ||
| "type": "string" | ||
| }, | ||
| "approvedDate": { | ||
| "format": "date-time", | ||
| "type": "string" | ||
| }, | ||
| "artists": { | ||
| "items": { | ||
| "$ref": "#/definitions/IProfileDetails" | ||
| }, | ||
| "type": "array" | ||
| }, | ||
| "avatar": { | ||
| "type": "string" | ||
| }, | ||
| "booking-exceptions": { | ||
| "items": { | ||
| "$ref": "#/definitions/IBookingException" | ||
| }, | ||
| "type": "array" | ||
| }, | ||
| "calFile": { | ||
| "type": "string" | ||
| }, | ||
| "city": { | ||
| "type": "string" | ||
| }, | ||
| "dateRequested": { | ||
| "format": "date-time", | ||
| "type": "string" | ||
| }, | ||
| "duration": { | ||
| "type": "number" | ||
| }, | ||
| "email": { | ||
| "type": "string" | ||
| }, | ||
| "endDate": { | ||
| "type": "string" | ||
| }, | ||
| "firstName": { | ||
| "type": "string" | ||
| }, | ||
| "frequencyType": { | ||
| "type": "number" | ||
| }, | ||
| "id": { | ||
| "type": "string" | ||
| }, | ||
| "lastName": { | ||
| "type": "string" | ||
| }, | ||
| "phone": { | ||
| "type": "string" | ||
| }, | ||
| "progress": { | ||
| "type": "number" | ||
| }, | ||
| "recurrenceCount": { | ||
| "type": "number" | ||
| }, | ||
| "recurrenceFrequency": { | ||
| "$ref": "#/definitions/RecurrenceFrequencyType" | ||
| }, | ||
| "recurrencePattern": { | ||
| "type": "string" | ||
| }, | ||
| "rejected": { | ||
| "type": "boolean" | ||
| }, | ||
| "stageName": { | ||
| "type": "string" | ||
| }, | ||
| "startDate": { | ||
| "type": "string" | ||
| }, | ||
| "venue": { | ||
| "$ref": "#/definitions/IVenue" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "IBookingException": { | ||
| "properties": { | ||
| "artists": { | ||
| "items": { | ||
| "$ref": "#/definitions/IProfileDetails" | ||
| }, | ||
| "type": "array" | ||
| }, | ||
| "booking": { | ||
| "$ref": "#/definitions/IBooking" | ||
| }, | ||
| "booking-id": { | ||
| "type": "string" | ||
| }, | ||
| "exceptionDate": { | ||
| "type": "string" | ||
| }, | ||
| "id": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "IConnectedAccounts": { | ||
| "properties": { | ||
| "github": { | ||
| "type": "boolean" | ||
| }, | ||
| "google": { | ||
| "type": "boolean" | ||
| }, | ||
| "stack": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "IDeactivateAccount": { | ||
| "properties": { | ||
| "confirm": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "IEmailPreferences": { | ||
| "properties": { | ||
| "customerPaymentDispute": { | ||
| "type": "boolean" | ||
| }, | ||
| "freeCollections": { | ||
| "type": "boolean" | ||
| }, | ||
| "invoicePayments": { | ||
| "type": "boolean" | ||
| }, | ||
| "payouts": { | ||
| "type": "boolean" | ||
| }, | ||
| "refundAlert": { | ||
| "type": "boolean" | ||
| }, | ||
| "successfulPayments": { | ||
| "type": "boolean" | ||
| }, | ||
| "webhookAPIEndpoints": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "IImage": { | ||
| "properties": { | ||
| "id": { | ||
| "type": "string" | ||
| }, | ||
| "url": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "INotifications": { | ||
| "properties": { | ||
| "billingUpdates": { | ||
| "properties": { | ||
| "email": { | ||
| "type": "boolean" | ||
| }, | ||
| "phone": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "completeProjects": { | ||
| "properties": { | ||
| "email": { | ||
| "type": "boolean" | ||
| }, | ||
| "phone": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "newTeamMembers": { | ||
| "properties": { | ||
| "email": { | ||
| "type": "boolean" | ||
| }, | ||
| "phone": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "newsletters": { | ||
| "properties": { | ||
| "email": { | ||
| "type": "boolean" | ||
| }, | ||
| "phone": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "notifications": { | ||
| "properties": { | ||
| "email": { | ||
| "type": "boolean" | ||
| }, | ||
| "phone": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "IProfileDetails": { | ||
| "properties": { | ||
| "address1": { | ||
| "type": "string" | ||
| }, | ||
| "address2": { | ||
| "type": "string" | ||
| }, | ||
| "allowMarketing": { | ||
| "type": "boolean" | ||
| }, | ||
| "avatar": { | ||
| "type": "string" | ||
| }, | ||
| "biography": { | ||
| "type": "string" | ||
| }, | ||
| "booking-exceptions": { | ||
| "items": { | ||
| "$ref": "#/definitions/IBookingException" | ||
| }, | ||
| "type": "array" | ||
| }, | ||
| "caeApproved": { | ||
| "type": "boolean" | ||
| }, | ||
| "city": { | ||
| "type": "string" | ||
| }, | ||
| "communications": { | ||
| "properties": { | ||
| "email": { | ||
| "type": "boolean" | ||
| }, | ||
| "phone": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "company": { | ||
| "type": "string" | ||
| }, | ||
| "companySite": { | ||
| "type": "string" | ||
| }, | ||
| "contactPhone": { | ||
| "type": "string" | ||
| }, | ||
| "country": { | ||
| "type": "string" | ||
| }, | ||
| "currency": { | ||
| "type": "string" | ||
| }, | ||
| "dbsApproved": { | ||
| "type": "boolean" | ||
| }, | ||
| "dbsExpires": { | ||
| "format": "date-time", | ||
| "type": "string" | ||
| }, | ||
| "description": { | ||
| "type": "string" | ||
| }, | ||
| "email": { | ||
| "type": "string" | ||
| }, | ||
| "facebookUrl": { | ||
| "type": "string" | ||
| }, | ||
| "firstName": { | ||
| "type": "string" | ||
| }, | ||
| "id": { | ||
| "type": "string" | ||
| }, | ||
| "images": { | ||
| "items": { | ||
| "$ref": "#/definitions/IImage" | ||
| }, | ||
| "type": "array" | ||
| }, | ||
| "instagramUrl": { | ||
| "type": "string" | ||
| }, | ||
| "isActive": { | ||
| "type": "boolean" | ||
| }, | ||
| "keywords": { | ||
| "type": "string" | ||
| }, | ||
| "language": { | ||
| "type": "string" | ||
| }, | ||
| "lastName": { | ||
| "type": "string" | ||
| }, | ||
| "latitude": { | ||
| "type": "number" | ||
| }, | ||
| "legacyId": { | ||
| "type": "string" | ||
| }, | ||
| "longitude": { | ||
| "type": "number" | ||
| }, | ||
| "mobile": { | ||
| "type": "string" | ||
| }, | ||
| "pliApproved": { | ||
| "type": "boolean" | ||
| }, | ||
| "pliExpiry": { | ||
| "format": "date-time", | ||
| "type": "string" | ||
| }, | ||
| "postcode": { | ||
| "type": "string" | ||
| }, | ||
| "quote1": { | ||
| "type": "string" | ||
| }, | ||
| "quote2": { | ||
| "type": "string" | ||
| }, | ||
| "quote3": { | ||
| "type": "string" | ||
| }, | ||
| "quote4": { | ||
| "type": "string" | ||
| }, | ||
| "rating": { | ||
| "type": "number" | ||
| }, | ||
| "stageName": { | ||
| "type": "string" | ||
| }, | ||
| "termsAcknowledged": { | ||
| "type": "boolean" | ||
| }, | ||
| "timeZone": { | ||
| "type": "string" | ||
| }, | ||
| "town": { | ||
| "$ref": "#/definitions/ITown" | ||
| }, | ||
| "travelMiles": { | ||
| "type": "number" | ||
| }, | ||
| "videoUrl": { | ||
| "type": "string" | ||
| }, | ||
| "youTubeUrl": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "ITown": { | ||
| "properties": { | ||
| "id": { | ||
| "type": "string" | ||
| }, | ||
| "name": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "IUpdateEmail": { | ||
| "properties": { | ||
| "confirmPassword": { | ||
| "type": "string" | ||
| }, | ||
| "newEmail": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "IUpdatePassword": { | ||
| "properties": { | ||
| "currentPassword": { | ||
| "type": "string" | ||
| }, | ||
| "newPassword": { | ||
| "type": "string" | ||
| }, | ||
| "passwordConfirmation": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "IVenue": { | ||
| "properties": { | ||
| "address1": { | ||
| "type": "string" | ||
| }, | ||
| "address2": { | ||
| "type": "string" | ||
| }, | ||
| "address3": { | ||
| "type": "string" | ||
| }, | ||
| "email": { | ||
| "type": "string" | ||
| }, | ||
| "id": { | ||
| "type": "string" | ||
| }, | ||
| "invoiceEmail": { | ||
| "type": "string" | ||
| }, | ||
| "latitude": { | ||
| "type": "string" | ||
| }, | ||
| "longitude": { | ||
| "type": "string" | ||
| }, | ||
| "name": { | ||
| "type": "string" | ||
| }, | ||
| "notes": { | ||
| "type": "string" | ||
| }, | ||
| "phone": { | ||
| "type": "string" | ||
| }, | ||
| "postCode": { | ||
| "type": "string" | ||
| }, | ||
| "town": { | ||
| "$ref": "#/definitions/ITown" | ||
| }, | ||
| "xeroId": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "NestedNestedTestClass": { | ||
| "properties": { | ||
| "nested": { | ||
| "$ref": "#/definitions/NestedTestClass" | ||
| }, | ||
| "property1Nested": { | ||
| "type": "string" | ||
| }, | ||
| "property2Nested": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "NestedTestClass": { | ||
| "properties": { | ||
| "nestedAgain": { | ||
| "$ref": "#/definitions/NestedNestedTestClass" | ||
| }, | ||
| "property1Nested": { | ||
| "type": "string" | ||
| }, | ||
| "property2Nested": { | ||
| "type": "boolean" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| }, | ||
| "RecurrenceFrequencyType": { | ||
| "enum": [ | ||
| 0, | ||
| 4, | ||
| 5, | ||
| 6, | ||
| 7 | ||
| ], | ||
| "type": "number" | ||
| }, | ||
| "TestClass": { | ||
| "properties": { | ||
| "PropertyAny": { | ||
| "type": "string" | ||
| }, | ||
| "a": { | ||
| "type": "string" | ||
| }, | ||
| "c": { | ||
| "type": "string" | ||
| }, | ||
| "caeApproved": { | ||
| "type": "boolean" | ||
| }, | ||
| "firstName": { | ||
| "type": "string" | ||
| }, | ||
| "isActive": { | ||
| "type": "boolean" | ||
| }, | ||
| "lastName": { | ||
| "type": "string" | ||
| }, | ||
| "nested": { | ||
| "$ref": "#/definitions/NestedTestClass" | ||
| }, | ||
| "nestedArray": { | ||
| "items": { | ||
| "$ref": "#/definitions/NestedTestClass" | ||
| }, | ||
| "type": "array" | ||
| }, | ||
| "not1": { | ||
| "type": "string" | ||
| }, | ||
| "numProp": { | ||
| "type": "number" | ||
| }, | ||
| "or1": { | ||
| "type": "string" | ||
| }, | ||
| "or3": { | ||
| "type": "string" | ||
| }, | ||
| "prop12": { | ||
| "type": "string" | ||
| }, | ||
| "property1": { | ||
| "type": "string" | ||
| }, | ||
| "property2": { | ||
| "type": "boolean" | ||
| }, | ||
| "stageName": { | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "type": "object" | ||
| } | ||
| } | ||
| } | ||
| import { IConnectedAccounts } from "./IConnectedAccounts" | ||
| import { IDeactivateAccount } from "./IDeactivateAccount" | ||
| import { IEmailPreferences } from "./IEmailPreferences" | ||
| import { INotifications } from "./INotifications" | ||
| import { IUpdateEmail } from "./IUpdateEmail" | ||
| import { IUpdatePassword } from "./IUpdatePassword" | ||
| export const updateEmail: IUpdateEmail = { | ||
| newEmail: 'support@keenthemes.com', | ||
| confirmPassword: '', | ||
| } | ||
| export const updatePassword: IUpdatePassword = { | ||
| currentPassword: '', | ||
| newPassword: '', | ||
| passwordConfirmation: '', | ||
| } | ||
| export const connectedAccounts: IConnectedAccounts = { | ||
| google: true, | ||
| github: true, | ||
| stack: false, | ||
| } | ||
| export const emailPreferences: IEmailPreferences = { | ||
| successfulPayments: false, | ||
| payouts: true, | ||
| freeCollections: false, | ||
| customerPaymentDispute: true, | ||
| refundAlert: false, | ||
| invoicePayments: true, | ||
| webhookAPIEndpoints: false, | ||
| } | ||
| export const notifications: INotifications = { | ||
| notifications: { | ||
| email: true, | ||
| phone: true, | ||
| }, | ||
| billingUpdates: { | ||
| email: true, | ||
| phone: true, | ||
| }, | ||
| newTeamMembers: { | ||
| email: true, | ||
| phone: false, | ||
| }, | ||
| completeProjects: { | ||
| email: false, | ||
| phone: true, | ||
| }, | ||
| newsletters: { | ||
| email: false, | ||
| phone: false, | ||
| }, | ||
| } | ||
| export const deactivateAccount: IDeactivateAccount = { | ||
| confirm: false, | ||
| } |
| import { NestedTestClass } from "./NestedTestClass" | ||
| export interface TestClass { | ||
| property1: string | ||
| property2: boolean | ||
| nested: NestedTestClass | ||
| nestedArray: NestedTestClass[] | ||
| a: string | ||
| c: string | ||
| not1: string | ||
| or1: string | ||
| or3: string | ||
| PropertyAny: string | ||
| prop12: string | ||
| numProp: number | ||
| stageName: string | ||
| lastName: string | ||
| firstName: string | ||
| caeApproved: boolean | ||
| isActive: boolean | ||
| } | ||
| { | ||
| "compilerOptions": { | ||
| "outDir": "dist", | ||
| "module": "commonjs", | ||
| "declaration": true, | ||
| "noImplicitAny": false, | ||
| "removeComments": true, | ||
| "noLib": false, | ||
| "emitDecoratorMetadata": true, | ||
| "experimentalDecorators": true, | ||
| "target": "es6", | ||
| "lib": ["es2017"], | ||
| "sourceMap": false, | ||
| "strict": false, | ||
| "strictPropertyInitialization": false, | ||
| "resolveJsonModule": true | ||
| }, | ||
| "include": ["test/Models"], | ||
| "exclude": ["node_modules","src"], | ||
| "compileOnSave": false, | ||
| "buildOnSave": false | ||
| } |
+6
-4
| { | ||
| "name": "json-api-query", | ||
| "version": "1.0.19", | ||
| "version": "2.0.0-next.0", | ||
| "description": "A query builder for JSONAPIDotNetCore", | ||
| "main": "index.js", | ||
| "scripts": { | ||
| "test": "nyc ./node_modules/.bin/_mocha 'test/**/*.test.ts'", | ||
| "build": "tsc" | ||
| "schema": "node_modules/typescript-json-schema/bin/typescript-json-schema tsconfig.schema.json \"*\" > test/Models/schema.json", | ||
| "test": "node_modules/typescript-json-schema/bin/typescript-json-schema tsconfig.schema.json \"*\" > test/Models/schema.json && nyc ./node_modules/.bin/_mocha 'test/**/*.test.ts'", | ||
| "build": "node_modules/typescript-json-schema/bin/typescript-json-schema tsconfig.schema.json \"*\" > test/Models/schema.json && tsc" | ||
| }, | ||
@@ -29,4 +30,5 @@ "repository": "https://github.com/boon-bo/json-api-query", | ||
| "prettier": "^2.6.2", | ||
| "source-map-support": "^0.5.21" | ||
| "source-map-support": "^0.5.21", | ||
| "typescript-json-schema": "^0.53.0" | ||
| } | ||
| } |
+39
-13
@@ -11,10 +11,16 @@ ## JSON-API-QUERY | ||
| The query builder supports all of the terms found in the [since v4.0] docs found here: https://www.jsonapi.net/usage/reading/filtering.html | ||
| The query builder aims to support all of the terms found in the [since v4.0] docs found here: https://www.jsonapi.net/usage/reading/filtering.html | ||
| Legacy syntax is not supported, there is some consideration in the code to add this later (as well as supporting evolved syntax later on) but I am not sure how worthwhile that would be | ||
| Legacy syntax is not supported, there is some consideration in the code to add this later (as well as supporting evolved syntax later on) but I am not sure how worthwhile that would be. | ||
| The latest version of this package now uses `typescript-json-schema`. This was requireed so that the types can be infered in the query bulder code. Unlike C#, Typescript's type system is unavailable at runtime, so tailoring the query generation for `HasMany` relations is AFAICS impossible without providing some schema information. | ||
| To this end I have introduced `typescript-json-schema`. Generating schemas for your models is as easy as adding `"typescript-json-schema tsconfig.schema.json \"*\" > test/Models/schema.json` to your NPM commands section. See this projects `package.json` for an example. | ||
| ### Basic filtering: | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -35,3 +41,5 @@ where: { | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -52,3 +60,5 @@ where: { | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -69,3 +79,5 @@ where: { | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -86,3 +98,5 @@ where: { | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -103,3 +117,5 @@ where: { | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -125,3 +141,5 @@ where: [ | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -144,3 +162,5 @@ where: { | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -163,3 +183,5 @@ relations: { | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -180,3 +202,5 @@ fields: { | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -203,3 +227,5 @@ order: { | ||
| ```typescript | ||
| new QueryBuilder<TestClass>() | ||
| import * as schema from "./Models/schema.json" | ||
| new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| .find({ | ||
@@ -206,0 +232,0 @@ where: { |
| import { IComparisonOperator } from '../../IComparisonOperator' | ||
| import * as TJS from "typescript-json-schema"; | ||
@@ -7,3 +8,3 @@ export class EqualsOperator implements IComparisonOperator { | ||
| constructor(public property: string, public value: string) { | ||
| constructor(public property: string, public value: string, public parent: string = null) { | ||
| this._property = property | ||
@@ -14,2 +15,12 @@ this._value = value | ||
| toString(): string { | ||
| // if(this.parent){ | ||
| // // special case for null | ||
| // if (this._value + '' != 'null' || this._value != null) { | ||
| // return `filter[${this.parent}]=equals(${this._property},'${this._value}')` | ||
| // } else { | ||
| // return `filter[${this.parent}]=equals(${this._property},${this._value})` | ||
| // } | ||
| // } | ||
| // special case for null | ||
@@ -16,0 +27,0 @@ if (this._value + '' != 'null' || this._value != null) { |
+172
-36
@@ -16,21 +16,38 @@ import { | ||
| } from './operators/dialect' | ||
| import {IComparisonOperator} from './IComparisonOperator' | ||
| import {IPageInfo} from './IPageInfo' | ||
| import {FindOptionsWhere} from './FindOptionsWhere' | ||
| import {FindManyOptions} from './FindManyOptions' | ||
| import {InstanceChecker} from './InstanceChecker' | ||
| import {SparseFieldSet} from './SparseFieldSet' | ||
| import {FindOperator} from './FindOperator' | ||
| import {FindOptionsRelations} from './FindOptionsRelations' | ||
| import {FindOptionsSelect} from './FindOptionsSelect' | ||
| import {SparseField} from './SparseField' | ||
| import {FindOptionsOrder} from './FindOptionsOrder' | ||
| import {FindOptionsOrderValue} from './FindOptionsOrderValue' | ||
| import {Sorts} from './Sorts' | ||
| import {SortField} from './SortField' | ||
| import { IComparisonOperator } from './IComparisonOperator' | ||
| import { IPageInfo } from './IPageInfo' | ||
| import { FindOptionsWhere } from './FindOptionsWhere' | ||
| import { FindManyOptions } from './FindManyOptions' | ||
| import { InstanceChecker } from './InstanceChecker' | ||
| import { SparseFieldSet } from './SparseFieldSet' | ||
| import { FindOperator } from './FindOperator' | ||
| import { FindOptionsRelations } from './FindOptionsRelations' | ||
| import { FindOptionsSelect } from './FindOptionsSelect' | ||
| import { SparseField } from './SparseField' | ||
| import { FindOptionsOrder } from './FindOptionsOrder' | ||
| import { FindOptionsOrderValue } from './FindOptionsOrderValue' | ||
| import { Sorts } from './Sorts' | ||
| import { SortField } from './SortField' | ||
| import { resolve } from 'path' | ||
| import * as TJS from 'typescript-json-schema' | ||
| import { Definition, DefinitionOrBoolean } from 'typescript-json-schema' | ||
| export class IOpDef { | ||
| prop: string | ||
| op: FindOperator<any> | ||
| } | ||
| // For convenience | ||
| type Primitive = string | number | bigint | boolean | undefined | symbol | ||
| // To infinity and beyond >:D | ||
| export type PropertyStringPath<T, Prefix = ''> = { | ||
| [K in keyof T]: T[K] extends Primitive | Array<any> | ||
| ? `${string & Prefix}${string & K}` | ||
| : `${string & Prefix}${string & K}` | PropertyStringPath<T[K], `${string & Prefix}${string & K}.`> | ||
| }[keyof T] | ||
| export class QueryBuilder<T> { | ||
| private _operators: Array<IComparisonOperator> = [] | ||
| private readonly _model: string = '' | ||
| private _pageInfo: IPageInfo | null = null | ||
@@ -40,11 +57,31 @@ private _includes: Array<string> = [] | ||
| private _fields: SparseFieldSet | null = null | ||
| private readonly _childQueryBuilder: QueryBuilder<T> | null = null | ||
| private _childQueryBuilders: QueryBuilder<T>[] = [] | ||
| private _findOptions: FindManyOptions<T> | undefined | ||
| private readonly _originalSchema: TJS.Definition | TJS.DefinitionOrBoolean | ||
| constructor(public childQueryBuilder: QueryBuilder<T> | null = null, public model: string = '') { | ||
| this._childQueryBuilder = childQueryBuilder | ||
| this._model = model | ||
| constructor( | ||
| public modelType: string = '', | ||
| public schema: Definition | DefinitionOrBoolean = null, | ||
| public property: string = '', | ||
| public isToManyFromParent: boolean = false, | ||
| public parentQueryBuilder: QueryBuilder<T> | null = null, | ||
| public childQueryBuilder: QueryBuilder<T> | null = null, | ||
| ) { | ||
| this.childQueryBuilder = childQueryBuilder | ||
| this.property = property | ||
| this._originalSchema = schema | ||
| this.schema = (schema as TJS.Definition).definitions[modelType] | ||
| this.isToManyFromParent = isToManyFromParent | ||
| this.modelType = modelType | ||
| this.parentQueryBuilder = parentQueryBuilder | ||
| } | ||
| public getPrentPath() { | ||
| if (this.parentQueryBuilder && this.parentQueryBuilder.getPrentPath() !== '') { | ||
| return `${this.parentQueryBuilder.getPrentPath()}.${this.property}` | ||
| } | ||
| return this.property | ||
| } | ||
| /** | ||
@@ -78,3 +115,3 @@ * Finds entities that match given find options. | ||
| protected buildSorts(selects: FindOptionsOrder<T> | undefined): Sorts { | ||
| let fields = new Sorts(this._model) | ||
| let fields = new Sorts(this.property) | ||
| for (let key in selects) { | ||
@@ -129,3 +166,3 @@ if (typeof selects[key] === 'string') { | ||
| protected buildSparseFieldsets(selects: FindOptionsSelect<T>): SparseFieldSet { | ||
| let fields = new SparseFieldSet(this._model) | ||
| let fields = new SparseFieldSet(this.property) | ||
| for (let key in selects) { | ||
@@ -159,8 +196,2 @@ if (typeof selects[key] === 'boolean' && (selects[key] as boolean) == true) { | ||
| ...where.map((whereItem) => { | ||
| // for (let key in whereItem) { | ||
| // if (typeof whereItem[key] == "object" && !InstanceChecker.isFindOperator(whereItem[key])) { | ||
| // throw Error('You can\'t do an implicit OR using nested properties') | ||
| // } | ||
| // } | ||
| return this.buildWhere(whereItem) | ||
@@ -178,2 +209,9 @@ }), | ||
| if (where[key] === undefined || where[key] === null) continue | ||
| let isToMany = false | ||
| let schema = (this.schema as TJS.Definition).properties[key] as TJS.Definition | ||
| if (schema.type && schema.type === 'array') isToMany = true | ||
| if (!InstanceChecker.isFindOperator(where[key])) { | ||
@@ -185,12 +223,48 @@ if (where[key] == null) { | ||
| // create the child QB otherwise we need to do Equals(parent.child,'something') | ||
| let cqb = new QueryBuilder(null, key) | ||
| cqb.find({where: where[key]}) | ||
| this._childQueryBuilders.push(cqb) | ||
| let t = isToMany | ||
| ? ((this.schema as TJS.Definition).properties[key] as TJS.Definition).items['$ref'].replace( | ||
| '#/definitions/', | ||
| '', | ||
| ) | ||
| : ((this.schema as TJS.Definition).properties[key] as TJS.Definition)['$ref'].replace( | ||
| '#/definitions/', | ||
| '', | ||
| ) | ||
| if (isToMany) { | ||
| let cqb = new QueryBuilder( | ||
| t, | ||
| this._originalSchema as TJS.Definition, | ||
| key, | ||
| isToMany, | ||
| this, | ||
| null, | ||
| ) | ||
| cqb.find({ where: where[key] }) | ||
| this._childQueryBuilders.push(cqb) | ||
| } else { | ||
| // search down the where[key] until we get a findOperator | ||
| // use the path to build the operator | ||
| let op: IComparisonOperator[] | null = this.getChildOperators(where[key], key) | ||
| if (op) { | ||
| ops.push(...op) | ||
| } | ||
| } | ||
| continue | ||
| } else { | ||
| ops.push(new EqualsOperator(key, where[key])) | ||
| ops.push(new EqualsOperator(key, where[key], key)) | ||
| continue | ||
| } | ||
| } | ||
| let op: IComparisonOperator | null = this.getOperator(where[key], key) | ||
| let path = this.getPrentPath() | ||
| if (path && !this.isToManyFromParent) { | ||
| path = `${path}.${key}` | ||
| } else { | ||
| path = key | ||
| } | ||
| let op: IComparisonOperator | null = this.getOperator(where[key], `${path}`) | ||
| if (op) { | ||
@@ -211,2 +285,64 @@ ops.push(op) | ||
| private getChildOperators(whereElement: any, parentKey: string) { | ||
| let ops = this.findOperators(whereElement) | ||
| let result = [] | ||
| for (var i = 0; i < ops.length; i++) { | ||
| let o = ops[i] | ||
| //for (let key in o as any) { | ||
| let path = `${parentKey}.${this.getPath(whereElement, o.prop)}` | ||
| let op: IComparisonOperator | null = this.getOperator(o.op, `${path}`) | ||
| if (op) { | ||
| result.push(op) | ||
| } | ||
| //} | ||
| } | ||
| return result | ||
| } | ||
| getPath(obj, key) { | ||
| let paths = [] | ||
| function getPaths(obj, path) { | ||
| if (obj instanceof Object && !(obj instanceof Array)) { | ||
| for (var k in obj) { | ||
| paths.push(path + '.' + k) | ||
| getPaths(obj[k], path + '.' + k) | ||
| } | ||
| } | ||
| } | ||
| getPaths(obj, '') | ||
| return paths | ||
| .map(function (p) { | ||
| return p.slice(p.lastIndexOf('.') + 1) == key ? p.slice(1) : '' | ||
| }) | ||
| .sort(function (a, b) { | ||
| return b.split('.').length - a.split('.').length | ||
| })[0] | ||
| } | ||
| findOperators(o: any): Array<IOpDef> { | ||
| let ops = Array<IOpDef>() | ||
| if (o instanceof Array) { | ||
| for (let i = 0; i < o.length; i++) { | ||
| ops.push(...this.findOperators(o[i])) | ||
| } | ||
| } else { | ||
| for (let prop in o) { | ||
| console.log(prop + ': ' + o[prop]) | ||
| if (InstanceChecker.isFindOperator(o[prop])) { | ||
| ops.push({ | ||
| prop: prop, | ||
| op: o[prop], | ||
| } as IOpDef) | ||
| } else { | ||
| ops.push(...this.findOperators(o[prop])) | ||
| } | ||
| } | ||
| } | ||
| return ops | ||
| } | ||
| // TODO: refactor this into a factory class | ||
@@ -305,4 +441,4 @@ getOperator(op: FindOperator<any> | undefined, key: string): IComparisonOperator | null { | ||
| if (!this.isNullOrWhiteSpace(this._model)) { | ||
| filterPropertyExpression = `filter[${this._model}]` | ||
| if (this.isToManyFromParent && this.property) { | ||
| filterPropertyExpression = `filter[${this.property}]` | ||
| } | ||
@@ -320,4 +456,4 @@ | ||
| if (this._childQueryBuilder) { | ||
| final += this._childQueryBuilder.build(final) | ||
| if (this.childQueryBuilder) { | ||
| final += this.childQueryBuilder.build(final) | ||
| } | ||
@@ -324,0 +460,0 @@ |
@@ -1,50 +0,8 @@ | ||
| import {suite, test, should, expect, chai} from './utility' | ||
| import {suite, test, should, expect, chai, timeout} from './utility' | ||
| import {QueryBuilder} from '../src' | ||
| import {AndOperator, AnyOperator, EqualsOperator, NotOperator, OrOperator} from '../src/operators/dialect' | ||
| import {GreaterThan} from '../src/operators/GreaterThan' | ||
| import {SparseFieldSet} from '../src' | ||
| import {Any} from '../src/operators/Any' | ||
| import {Contains} from '../src/operators/Contains' | ||
| import {EndsWith} from '../src/operators/EndsWith' | ||
| import {Has} from '../src/operators/Has' | ||
| import {StartsWith} from '../src/operators/StartsWith' | ||
| import {Or} from '../src/operators/Or' | ||
| import {Not} from '../src/operators/Not' | ||
| import {LessThanOrEqual} from '../src/operators/LessThanOrEqual' | ||
| import {LessThan} from '../src/operators/LessThan' | ||
| import {GreaterThanOrEqual} from '../src/operators/GreaterThanOrEqual' | ||
| import {Equals} from '../src/operators/Equals' | ||
| abstract class TestClass { | ||
| property1: string | ||
| property2: boolean | ||
| nested: NestedTestClass | ||
| nestedArray: NestedTestClass[] | ||
| a: string | ||
| c: string | ||
| not1: string | ||
| or1: string | ||
| or3: string | ||
| PropertyAny: string | ||
| prop12: string | ||
| numProp: number | ||
| stageName: string | ||
| lastName: string | ||
| firstName: string | ||
| caeApproved: boolean | ||
| isActive: boolean | ||
| } | ||
| class NestedNestedTestClass { | ||
| property1Nested: string | ||
| property2Nested: boolean | ||
| nested: NestedTestClass | ||
| } | ||
| class NestedTestClass { | ||
| property1Nested: string | ||
| property2Nested: boolean | ||
| nestedAgain: NestedNestedTestClass | ||
| } | ||
| import {GreaterThan, Any, Contains, EndsWith, Has, StartsWith, Or, Not, LessThanOrEqual, LessThan, GreaterThanOrEqual, Equals} from '../src' | ||
| import * as schema from "./Models/schema.json" | ||
| import * as TJS from "typescript-json-schema"; | ||
| import {TestClass} from "./Models/TestClass"; | ||
| import { IBooking } from 'Models/IBooking'; | ||
| should() | ||
@@ -56,9 +14,10 @@ | ||
| // todo: test with modelname | ||
| before() { | ||
| this.sut = new QueryBuilder<TestClass>() | ||
| this.sut = new QueryBuilder<TestClass>("TestClass", schema as TJS.Definition) | ||
| } | ||
| @test 'Can construct'() { | ||
| @timeout(40000) | ||
| @test 'Can construct'(done) { | ||
| expect(this.sut).should.be.not.undefined | ||
| done() | ||
| } | ||
@@ -302,3 +261,3 @@ | ||
| expect(result).to.equal("?filter[nested]=equals(property1Nested,'test')") | ||
| expect(result).to.equal("?filter=equals(nested.property1Nested,'test')") | ||
| } | ||
@@ -362,3 +321,3 @@ | ||
| expect(result).to.equal( | ||
| '?fields[nested]=property2Nested&fields[nested.nestedAgain]=property1Nested&fields=property2', | ||
| '?fields[nested]=property2Nested&fields[nestedAgain]=property1Nested&fields=property2', | ||
| ) | ||
@@ -445,3 +404,3 @@ } | ||
| .build() | ||
| expect(result).to.equal('?page[size]=0&page[number]=10') | ||
| expect(result).to.equal('?page[size]=10,something:20&page[number]=10,something:20') | ||
| } | ||
@@ -465,16 +424,2 @@ | ||
| // @test 'two wheres makes an or - nested - throws'() { | ||
| // expect(() => this.sut.find({ | ||
| // where: [{ | ||
| // nested: { | ||
| // property1Nested: "test" | ||
| // } | ||
| // }, { | ||
| // nested: { | ||
| // property2Nested: false | ||
| // } | ||
| // }] | ||
| // }).build()).to.throw('You can\'t do an implicit OR using nested properties') | ||
| // } | ||
| @test 'ors and and'() { | ||
@@ -508,2 +453,19 @@ let result = this.sut | ||
| @test 'two wheres make an and'() { | ||
| let result = this.sut | ||
| .find({ | ||
| where: | ||
| { | ||
| stageName: StartsWith('Andy'), | ||
| isActive: true, | ||
| } | ||
| }) | ||
| .build() | ||
| expect(result).to.equal( | ||
| "?filter=and(startsWith(stageName,'Andy'),equals(isActive,'true'))", | ||
| ) | ||
| } | ||
| @test 'single where with multiple props generates ands'() { | ||
@@ -523,2 +485,48 @@ let result = this.sut | ||
| @test | ||
| 'regression 20 may 2022'(){ | ||
| let sut = new QueryBuilder<IBooking>("IBooking", schema as TJS.Definition) | ||
| let result = sut | ||
| .find({ | ||
| size: 10, | ||
| number: 1, | ||
| where: { | ||
| approvedDate: Equals(null), | ||
| venue: { | ||
| name: StartsWith('Andy'), | ||
| }, | ||
| }, | ||
| relations: { | ||
| artists: { | ||
| images: true, | ||
| }, | ||
| venue: true | ||
| }, | ||
| }) | ||
| .build(); | ||
| expect(result).to.equal('?filter=and(equals(approvedDate,null),startsWith(venue.name,\'Andy\'))&include=artists.images,venue&page[size]=10&page[number]=1') | ||
| } | ||
| @test | ||
| 'regression 20 may 2022 2 wheres with nesting make an and'(){ | ||
| let sut = new QueryBuilder<IBooking>("IBooking", schema as TJS.Definition) | ||
| let result = sut | ||
| .find({ | ||
| where: { | ||
| approvedDate: Equals(null), | ||
| venue: { | ||
| name: StartsWith("Andy"), | ||
| } | ||
| }, | ||
| }) | ||
| .build(); | ||
| expect(result).to.equal('?filter=and(equals(approvedDate,null),startsWith(venue.name,\'Andy\'))') | ||
| } | ||
| @test | ||
| 'complex query works'() { | ||
@@ -598,5 +606,5 @@ let result = this.sut | ||
| expect(result).to.equal( | ||
| "?sort=property2&sort[nested]=property1Nested&filter=and(contains(a,'lol'),not(equals(not1,'not5')))&include=nested&fields=firstName,lastName&fields[nested]=property2Nested&page[size]=0&page[number]=10&filter[nested]=and(equals(property2Nested,'true'),has(property1Nested,one,two))&filter[nestedAgain]=endsWith(property1Nested,'wot')", | ||
| "?sort=property2&sort[nested]=property1Nested&filter=and(contains(a,'lol'),not(equals(not1,'not5')),has(nested.nestedAgain.property1Nested,one,two),endsWith(nested.nestedAgain.property1Nested,'wot'))&include=nested&fields=firstName,lastName&fields[nested]=property2Nested&page[size]=0&page[number]=10", | ||
| ) | ||
| } | ||
| } |
@@ -1,2 +0,2 @@ | ||
| export { suite, test, params, skip, only } from '@testdeck/mocha' | ||
| export { suite, test, params, skip, only, timeout } from '@testdeck/mocha' | ||
@@ -3,0 +3,0 @@ import * as _chai from 'chai' |
+2
-1
@@ -15,3 +15,4 @@ { | ||
| "strict": false, | ||
| "strictPropertyInitialization": false | ||
| "strictPropertyInitialization": false, | ||
| "resolveJsonModule": true | ||
| }, | ||
@@ -18,0 +19,0 @@ "exclude": ["node_modules"], |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
106364
41.46%102
21.43%2996
51.47%268
10.74%5
25%2
100%2
100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added