import { belongsTo , Entity , Filter , hasMany , model , property , } from '@loopback/repository' ; import { expect } from '@loopback/testlab' ; import Ajv from 'ajv' ; import { JsonSchema } from '../..' ; import { AnyScopeFilterSchema , getFieldsJsonSchemaFor , getFilterJsonSchemaFor , getScopeFilterJsonSchemaFor , getWhereJsonSchemaFor , } from '../../filter-json-schema' ; describe ( 'getFilterJsonSchemaFor' , ( ) => { let ajv ; let customerFilterSchema ; let dynamicCustomerFilterSchema ; let customerFilterExcludingWhereSchema ; let customerFilterExcludingIncludeSchema ; let orderFilterSchema ; beforeEach ( ( ) => { ajv = new Ajv ( ) ; customerFilterSchema = getFilterJsonSchemaFor ( Customer ) ; dynamicCustomerFilterSchema = getFilterJsonSchemaFor ( DynamicCustomer ) ; customerFilterExcludingWhereSchema = getFilterJsonSchemaFor ( Customer , { exclude : [ 'where' ] , } ) ; customerFilterExcludingIncludeSchema = getFilterJsonSchemaFor ( Customer , { exclude : [ 'include' ] , } ) ; orderFilterSchema = getFilterJsonSchemaFor ( Order ) ; } ) ; it ( 'produces a valid schema' , ( ) => { const isValid = ajv . validateSchema ( customerFilterSchema ) ; const SUCCESS_MSG = 'Filter schema is a valid JSON Schema' ; const result = isValid ? SUCCESS_MSG : ajv . errorsText ( ajv . errors ! ) ; expect ( result ) . to . equal ( SUCCESS_MSG ) ; } ) ; it ( 'allows an empty filter' , ( ) => { expectSchemaToAllowFilter ( customerFilterSchema , { } ) ; } ) ; it ( 'allows a string-based order' , ( ) => { expectSchemaToAllowFilter ( customerFilterSchema , { order : 'id DESC' } ) ; } ) ; it ( 'allows a array-based order' , ( ) => { expectSchemaToAllowFilter ( customerFilterSchema , { order : [ 'id DESC' ] } ) ; } ) ; it ( 'allows all top-level filter properties' , ( ) => { const filter = { where : { id : 1 } , fields : { id : true , name : true } , include : [ { relation : 'orders' } ] , offset : 0 , limit : 10 , order : [ 'id DESC' ] , skip : 0 , } ; expectSchemaToAllowFilter ( customerFilterSchema , filter ) ; } ) ; it ( 'disallows "where"' , ( ) => { const filter = { where : { name : 'John' } } ; ajv . validate ( customerFilterExcludingWhereSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'additionalProperties' , instancePath : '' , schemaPath : '#/additionalProperties' , params : { additionalProperty : 'where' } , message : 'must NOT have additional properties' , } , ] ) ; } ) ; it ( 'disallows "include"' , ( ) => { const filter = { include : 'orders' } ; ajv . validate ( customerFilterExcludingIncludeSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'additionalProperties' , instancePath : '' , schemaPath : '#/additionalProperties' , params : { additionalProperty : 'include' } , message : 'must NOT have additional properties' , } , ] ) ; } ) ; it ( 'describes "where" as an object' , ( ) => { const filter = { where : 'invalid-where' } ; ajv . validate ( customerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'type' , instancePath : '/where' , message : 'must be object' , } , ] ) ; } ) ; it ( 'describes "fields" as an object' , ( ) => { const filter = { fields : 'invalid-fields' } ; ajv . validate ( customerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'type' , instancePath : '/fields' , message : 'must be object' , } , ] ) ; } ) ; it ( 'allows free-form properties in "fields" for non-strict models"' , ( ) => { const filter = { fields : [ 'test' , 'id' ] } ; ajv . validate ( dynamicCustomerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . be . empty ( ) ; } ) ; it ( 'allows only defined properties in "fields" for strict models"' , ( ) => { const filter = { fields : [ 'test' ] } ; ajv . validate ( customerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'enum' , instancePath : '/fields/0' , params : { allowedValues : [ 'id' , 'name' ] } , message : 'must be equal to one of the allowed values' , } , ] ) ; } ) ; it ( 'rejects "fields" with duplicated items for strict models' , ( ) => { const filter = { fields : [ 'id' , 'id' ] } ; ajv . validate ( customerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'uniqueItems' , instancePath : '/fields' , message : 'must NOT have duplicate items (items ## 1 and 0 are identical)' , } , ] ) ; } ) ; it ( 'rejects "fields" with duplicated items for non-strict models' , ( ) => { const filter = { fields : [ 'test' , 'test' ] } ; ajv . validate ( dynamicCustomerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'uniqueItems' , instancePath : '/fields' , message : 'must NOT have duplicate items (items ## 1 and 0 are identical)' , } , ] ) ; } ) ; it ( 'describes "include" as an array for models with relations' , ( ) => { const filter = { include : 'invalid-include' } ; ajv . validate ( customerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'type' , instancePath : '/include' , message : 'must be array' , } , ] ) ; } ) ; it ( 'leaves out "include" for models with no relations' , ( ) => { const filterProperties = Object . keys ( orderFilterSchema . properties ?? { } ) ; expect ( filterProperties ) . to . not . containEql ( 'include' ) ; } ) ; it ( 'describes "offset" as an integer' , ( ) => { const filter = { offset : 'invalid-offset' } ; ajv . validate ( customerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'type' , instancePath : '/offset' , message : 'must be integer' , } , ] ) ; } ) ; it ( 'describes "limit" as an integer' , ( ) => { const filter = { limit : 'invalid-limit' } ; ajv . validate ( customerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'type' , instancePath : '/limit' , message : 'must be integer' , } , ] ) ; } ) ; it ( 'describes "skip" as an integer' , ( ) => { const filter = { skip : 'invalid-skip' } ; ajv . validate ( customerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'type' , instancePath : '/skip' , message : 'must be integer' , } , ] ) ; } ) ; it ( 'describes "order" as a string or array' , ( ) => { const filter = { order : { invalidOrder : '' } } ; ajv . validate ( customerFilterSchema , filter ) ; expect ( ajv . errors ?? [ ] ) . to . containDeep ( [ { keyword : 'type' , instancePath : '/order' , message : 'must be string' , } , { keyword : 'type' , instancePath : '/order' , message : 'must be array' , } , { keyword : 'oneOf' , instancePath : '/order' , params : { passingSchemas : null } , message : 'must match exactly one schema in oneOf' , } , ] ) ; } ) ; it ( 'returns "title" when no options were provided' , ( ) => { expect ( orderFilterSchema . title ) . to . equal ( 'Order.Filter' ) ; } ) ; it ( 'returns "include.title" when no options were provided' , ( ) => { expect ( customerFilterSchema . properties ) . to . have . propertyByPath ( 'include' , 'title' ) . to . equal ( 'Customer.IncludeFilter' ) ; } ) ; it ( 'returns "include.items.title" when no options were provided' , ( ) => { expect ( customerFilterSchema . properties ) . to . have . propertyByPath ( 'include' , 'items' , 'anyOf' , '0' , 'title' ) . to . equal ( 'Customer.IncludeFilter.Items' ) ; } ) ; it ( 'returns "scope.title" when no options were provided' , ( ) => { expect ( customerFilterSchema . properties ) . to . have . propertyByPath ( 'include' , 'items' , 'anyOf' , '0' , 'properties' , 'scope' , 'title' ) . to . equal ( 'Customer.ScopeFilter' ) ; } ) ; function expectSchemaToAllowFilter < T > ( schema , value ) { const isValid = ajv . validate ( schema , value ) ; const SUCCESS_MSG = 'Filter instance is valid according to Filter schema' ; const result = isValid ? SUCCESS_MSG : ajv . errorsText ( ajv . errors ! ) ; expect ( result ) . to . equal ( SUCCESS_MSG ) ; } } ) ; describe ( 'getFilterJsonSchemaFor - excluding where' , ( ) => { let customerFilterSchema ; it ( 'excludes "where" using string[]' , ( ) => { customerFilterSchema = getFilterJsonSchemaFor ( Customer , { exclude : [ 'where' ] , } ) ; expect ( customerFilterSchema . properties ) . to . not . have . property ( 'where' ) ; } ) ; it ( 'excludes "where" using string' , ( ) => { customerFilterSchema = getFilterJsonSchemaFor ( Customer , { exclude : 'where' , } ) ; expect ( customerFilterSchema . properties ) . to . not . have . property ( 'where' ) ; } ) ; } ) ; describe ( 'getFilterJsonSchemaFor - excluding include' , ( ) => { let customerFilterSchema ; it ( 'excludes "include" using string[]' , ( ) => { customerFilterSchema = getFilterJsonSchemaFor ( Customer , { exclude : [ 'include' ] , } ) ; expect ( customerFilterSchema . properties ) . to . not . have . property ( 'include' ) ; } ) ; it ( 'excludes "include" using string' , ( ) => { customerFilterSchema = getFilterJsonSchemaFor ( Customer , { exclude : 'include' , } ) ; expect ( customerFilterSchema . properties ) . to . not . have . property ( 'include' ) ; } ) ; } ) ; describe ( 'getFilterJsonSchemaForOptionsSetTitle' , ( ) => { let customerFilterSchema ; beforeEach ( ( ) => { customerFilterSchema = getFilterJsonSchemaFor ( Customer , { setTitle : true } ) ; } ) ; it ( 'returns "title" when a single option "setTitle" is set' , ( ) => { expect ( customerFilterSchema . title ) . to . equal ( 'Customer.Filter' ) ; } ) ; it ( 'returns "include.title" when a single option "setTitle" is set' , ( ) => { expect ( customerFilterSchema . properties ) . to . have . propertyByPath ( 'include' , 'title' ) . to . equal ( 'Customer.IncludeFilter' ) ; } ) ; it ( 'returns "include.items.title" when a single option "setTitle" is set' , ( ) => { expect ( customerFilterSchema . properties ) . to . have . propertyByPath ( 'include' , 'items' , 'anyOf' , '0' , 'title' ) . to . equal ( 'Customer.IncludeFilter.Items' ) ; } ) ; it ( 'returns "scope.title" when a single option "setTitle" is set' , ( ) => { expect ( customerFilterSchema . properties ) . to . have . propertyByPath ( 'include' , 'items' , 'anyOf' , '0' , 'properties' , 'scope' , 'title' ) . to . equal ( 'Customer.ScopeFilter' ) ; } ) ; } ) ; describe ( 'getFilterJsonSchemaForOptionsUnsetTitle' , ( ) => { let customerFilterSchema ; beforeEach ( ( ) => { customerFilterSchema = getFilterJsonSchemaFor ( Customer , { setTitle : false } ) ; } ) ; it ( 'no title when a single option "setTitle" is false' , ( ) => { expect ( customerFilterSchema ) . to . not . have . property ( 'title' ) ; } ) ; it ( 'no title on include when single option "setTitle" is false' , ( ) => { expect ( customerFilterSchema . properties ) . property ( 'include' ) . to . not . have . property ( 'title' ) ; } ) ; it ( 'no title on include.items when single option "setTitle" is false' , ( ) => { expect ( customerFilterSchema . properties ) . propertyByPath ( 'include' , 'items' , 'anyOf' , '0' ) . to . not . have . property ( 'title' ) ; } ) ; it ( 'no title on scope when single option "setTitle" is false' , ( ) => { expect ( customerFilterSchema . properties ) . propertyByPath ( 'include' , 'items' , 'anyOf' , '0' , 'properties' , 'scope' ) . to . not . have . property ( 'title' ) ; } ) ; } ) ; describe ( 'getScopeFilterJsonSchemaFor - nested inclusion' , ( ) => { let todoListScopeSchema ; @ model ( ) class Todo extends Entity { @ property ( { type : 'number' , id : true , generated : false , } ) id : number ; @ belongsTo ( ( ) => TodoList ) todoListId : number ; } @ model ( ) class TodoList extends Entity { @ property ( { type : 'number' , id : true , generated : false , } ) id : number ; @ hasMany ( ( ) => Todo ) todos : Todo [ ] ; } beforeEach ( ( ) => { todoListScopeSchema = getScopeFilterJsonSchemaFor ( TodoList , { setTitle : false , } ) ; } ) ; it ( 'does not have constraint for scope filter' , ( ) => { expect ( todoListScopeSchema . properties ) . propertyByPath ( 'include' ) . to . containEql ( { ... AnyScopeFilterSchema , } ) ; } ) ; } ) ; describe ( 'getWhereJsonSchemaFor' , ( ) => { let ajv ; let customerWhereSchema ; beforeEach ( ( ) => { ajv = new Ajv ( ) ; customerWhereSchema = getWhereJsonSchemaFor ( Customer ) ; } ) ; it ( 'produces a valid schema' , ( ) => { const isValid = ajv . validateSchema ( customerWhereSchema ) ; const SUCCESS_MSG = 'Where schema is a valid JSON Schema' ; const result = isValid ? SUCCESS_MSG : ajv . errorsText ( ajv . errors ! ) ; expect ( result ) . to . equal ( SUCCESS_MSG ) ; } ) ; it ( 'returns "title" when no options were provided' , ( ) => { expect ( customerWhereSchema . title ) . to . equal ( 'Customer.WhereFilter' ) ; } ) ; } ) ; describe ( 'getWhereJsonSchemaForOptions' , ( ) => { let customerWhereSchema ; it ( 'returns "title" when a single option "setTitle" is set' , ( ) => { customerWhereSchema = getWhereJsonSchemaFor ( Customer , { setTitle : true , } ) ; expect ( customerWhereSchema . title ) . to . equal ( 'Customer.WhereFilter' ) ; } ) ; it ( 'leaves out "title" when a single option "setTitle" is false' , ( ) => { customerWhereSchema = getWhereJsonSchemaFor ( Customer , { setTitle : false , } ) ; expect ( customerWhereSchema ) . to . not . have . property ( 'title' ) ; } ) ; } ) ; describe ( 'getFieldsJsonSchemaFor' , ( ) => { let customerFieldsSchema ; it ( 'returns "title" when no options were provided' , ( ) => { customerFieldsSchema = getFieldsJsonSchemaFor ( Customer ) ; expect ( customerFieldsSchema . title ) . to . equal ( 'Customer.Fields' ) ; } ) ; it ( 'returns "title" when a single option "setTitle" is set' , ( ) => { customerFieldsSchema = getFieldsJsonSchemaFor ( Customer , { setTitle : true , } ) ; expect ( customerFieldsSchema . title ) . to . equal ( 'Customer.Fields' ) ; } ) ; it ( 'leaves out "title" when a single option "setTitle" is false' , ( ) => { customerFieldsSchema = getFieldsJsonSchemaFor ( Customer , { setTitle : false , } ) ; expect ( customerFieldsSchema ) . to . not . have . property ( 'title' ) ; } ) ; } ) ; describe ( 'single option setTitle override original value' , ( ) => { let customerFieldsSchema ; it ( 'returns builtin "title" when no options were provided' , ( ) => { customerFieldsSchema = { title : 'Test Title' , ... getFieldsJsonSchemaFor ( Customer ) , } ; expect ( customerFieldsSchema . title ) . to . equal ( 'Customer.Fields' ) ; } ) ; it ( 'returns builtin "title" when a single option "setTitle" is set' , ( ) => { customerFieldsSchema = { title : 'Test Title' , ... getFieldsJsonSchemaFor ( Customer , { setTitle : true , } ) , } ; expect ( customerFieldsSchema . title ) . to . equal ( 'Customer.Fields' ) ; } ) ; it ( 'returns original "title" when a single option "setTitle" is false' , ( ) => { customerFieldsSchema = { title : 'Test Title' , ... getFieldsJsonSchemaFor ( Customer , { setTitle : false , } ) , } ; expect ( customerFieldsSchema . title ) . to . equal ( 'Test Title' ) ; } ) ; } ) ; @ model ( ) class Order extends Entity { @ property ( { id : true } ) id : number ; @ property ( ) customerId : number ; } @ model ( ) class Customer extends Entity { @ property ( { id : true } ) id : number ; @ property ( ) name : string ; @ hasMany ( ( ) => Order ) orders ? : Order [ ] ; } @ model ( { settings : { strict : false } , } ) class DynamicCustomer extends Entity { [ key ] : any ; }