Add support for SEARCH method (#5183)

* Add search method support for advanced get

* Add docs for SEARCH
This commit is contained in:
Rijk van Zanten
2021-04-21 13:35:16 -04:00
committed by GitHub
parent f836c90990
commit b40c62d257
31 changed files with 411 additions and 256 deletions

View File

@@ -90,8 +90,8 @@ export default async function createApp() {
return next();
});
});
app.use(cookieParser())
app.use(cookieParser());
app.use(extractToken);

View File

@@ -6,36 +6,45 @@ import { ForbiddenException, InvalidPayloadException } from '../exceptions';
import useCollection from '../middleware/use-collection';
import { respond } from '../middleware/respond';
import Joi from 'joi';
import { validateBatch } from '../middleware/validate-batch';
const router = express.Router();
router.use(useCollection('directus_activity'));
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new ActivityService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new ActivityService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_activity', req.sanitizedQuery);
let result;
res.locals.payload = {
data: records || null,
meta,
};
if (req.singleton) {
result = await service.readSingleton(req.sanitizedQuery);
} else if (req.body.keys) {
result = await service.readMany(req.body.keys, req.sanitizedQuery);
} else {
result = await service.readByQuery(req.sanitizedQuery);
}
return next();
}),
respond
);
const meta = await metaService.getMetaForQuery('directus_activity', req.sanitizedQuery);
res.locals.payload = {
data: result,
meta,
};
return next();
});
router.search('/', validateBatch('read'), readHandler, respond);
router.get('/', readHandler, respond);
router.get(
'/:pk',

View File

@@ -3,6 +3,8 @@ import asyncHandler from '../utils/async-handler';
import { CollectionsService, MetaService } from '../services';
import { ForbiddenException } from '../exceptions';
import { respond } from '../middleware/respond';
import { validateBatch } from '../middleware/validate-batch';
import { Item } from '../types';
const router = Router();
@@ -29,26 +31,33 @@ router.post(
respond
);
router.get(
'/',
asyncHandler(async (req, res, next) => {
const collectionsService = new CollectionsService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const collectionsService = new CollectionsService({
accountability: req.accountability,
schema: req.schema,
});
const collections = await collectionsService.readByQuery();
const meta = await metaService.getMetaForQuery('directus_collections', {});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
res.locals.payload = { data: collections || null, meta };
return next();
}),
respond
);
let result: Item[] = [];
if (req.body.keys) {
result = await collectionsService.readMany(req.body.keys);
} else {
result = await collectionsService.readByQuery();
}
const meta = await metaService.getMetaForQuery('directus_collections', {});
res.locals.payload = { data: result, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:collection',

View File

@@ -6,12 +6,7 @@ import { File, PrimaryKey } from '../types';
import formatTitle from '@directus/format-title';
import env from '../env';
import Joi from 'joi';
import {
InvalidPayloadException,
ForbiddenException,
FailedValidationException,
ServiceUnavailableException,
} from '../exceptions';
import { InvalidPayloadException, ForbiddenException } from '../exceptions';
import path from 'path';
import useCollection from '../middleware/use-collection';
import { respond } from '../middleware/respond';
@@ -178,27 +173,35 @@ router.post(
respond
);
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new FilesService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new FilesService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_files', req.sanitizedQuery);
let result;
res.locals.payload = { data: records || null, meta };
return next();
}),
respond
);
if (req.singleton) {
result = await service.readSingleton(req.sanitizedQuery);
} else if (req.body.keys) {
result = await service.readMany(req.body.keys, req.sanitizedQuery);
} else {
result = await service.readByQuery(req.sanitizedQuery);
}
const meta = await metaService.getMetaForQuery('directus_files', req.sanitizedQuery);
res.locals.payload = { data: result, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:pk',

View File

@@ -1,11 +1,10 @@
import express from 'express';
import asyncHandler from '../utils/async-handler';
import { FoldersService, MetaService } from '../services';
import { ForbiddenException, InvalidPayloadException, FailedValidationException } from '../exceptions';
import { ForbiddenException } from '../exceptions';
import useCollection from '../middleware/use-collection';
import { respond } from '../middleware/respond';
import { PrimaryKey } from '../types';
import Joi from 'joi';
import { validateBatch } from '../middleware/validate-batch';
const router = express.Router();
@@ -51,26 +50,34 @@ router.post(
respond
);
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new FoldersService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new FoldersService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_files', req.sanitizedQuery);
let result;
res.locals.payload = { data: records || null, meta };
return next();
}),
respond
);
if (req.singleton) {
result = await service.readSingleton(req.sanitizedQuery);
} else if (req.body.keys) {
result = await service.readMany(req.body.keys, req.sanitizedQuery);
} else {
result = await service.readByQuery(req.sanitizedQuery);
}
const meta = await metaService.getMetaForQuery('directus_files', req.sanitizedQuery);
res.locals.payload = { data: result, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:pk',

View File

@@ -1,12 +1,10 @@
import express from 'express';
import express, { RequestHandler } from 'express';
import asyncHandler from '../utils/async-handler';
import collectionExists from '../middleware/collection-exists';
import { ItemsService, MetaService } from '../services';
import { RouteNotFoundException, ForbiddenException, FailedValidationException } from '../exceptions';
import { RouteNotFoundException, ForbiddenException } from '../exceptions';
import { respond } from '../middleware/respond';
import { InvalidPayloadException } from '../exceptions';
import { PrimaryKey } from '../types';
import Joi from 'joi';
import { validateBatch } from '../middleware/validate-batch';
const router = express.Router();
@@ -57,37 +55,41 @@ router.post(
respond
);
router.get(
'/:collection',
collectionExists,
asyncHandler(async (req, res, next) => {
if (req.params.collection.startsWith('directus_')) throw new ForbiddenException();
const readHandler = asyncHandler(async (req, res, next) => {
if (req.params.collection.startsWith('directus_')) throw new ForbiddenException();
const service = new ItemsService(req.collection, {
accountability: req.accountability,
schema: req.schema,
});
const service = new ItemsService(req.collection, {
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const records = req.singleton
? await service.readSingleton(req.sanitizedQuery)
: await service.readByQuery(req.sanitizedQuery);
let result;
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
if (req.singleton) {
result = await service.readSingleton(req.sanitizedQuery);
} else if (req.body.keys) {
result = await service.readMany(req.body.keys, req.sanitizedQuery);
} else {
result = await service.readByQuery(req.sanitizedQuery);
}
res.locals.payload = {
meta: meta,
data: records || null,
};
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
return next();
}),
respond
);
res.locals.payload = {
meta: meta,
data: result,
};
return next();
});
router.search('/:collection', collectionExists, validateBatch('read'), readHandler, respond);
router.get('/:collection', collectionExists, readHandler, respond);
router.get(
'/:collection/:pk',

View File

@@ -49,27 +49,35 @@ router.post(
respond
);
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new PermissionsService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new PermissionsService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const item = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_permissions', req.sanitizedQuery);
let result;
res.locals.payload = { data: item || null, meta };
return next();
}),
respond
);
if (req.singleton) {
result = await service.readSingleton(req.sanitizedQuery);
} else if (req.body.keys) {
result = await service.readMany(req.body.keys, req.sanitizedQuery);
} else {
result = await service.readByQuery(req.sanitizedQuery);
}
const meta = await metaService.getMetaForQuery('directus_permissions', req.sanitizedQuery);
res.locals.payload = { data: result, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:pk',

View File

@@ -50,26 +50,34 @@ router.post(
respond
);
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new PresetsService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new PresetsService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_presets', req.sanitizedQuery);
let result;
res.locals.payload = { data: records || null, meta };
return next();
}),
respond
);
if (req.singleton) {
result = await service.readSingleton(req.sanitizedQuery);
} else if (req.body.keys) {
result = await service.readMany(req.body.keys, req.sanitizedQuery);
} else {
result = await service.readByQuery(req.sanitizedQuery);
}
const meta = await metaService.getMetaForQuery('directus_presets', req.sanitizedQuery);
res.locals.payload = { data: result, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:pk',

View File

@@ -50,27 +50,26 @@ router.post(
respond
);
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new RelationsService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new RelationsService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
res.locals.payload = { data: records || null, meta };
return next();
}),
respond
);
res.locals.payload = { data: records || null, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:pk',

View File

@@ -3,31 +3,31 @@ import asyncHandler from '../utils/async-handler';
import { RevisionsService, MetaService } from '../services';
import useCollection from '../middleware/use-collection';
import { respond } from '../middleware/respond';
import { validateBatch } from '../middleware/validate-batch';
const router = express.Router();
router.use(useCollection('directus_revisions'));
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new RevisionsService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new RevisionsService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_revisions', req.sanitizedQuery);
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_revisions', req.sanitizedQuery);
res.locals.payload = { data: records || null, meta };
return next();
}),
respond
);
res.locals.payload = { data: records || null, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:pk',

View File

@@ -50,26 +50,25 @@ router.post(
respond
);
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new RolesService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new RolesService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_roles', req.sanitizedQuery);
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_roles', req.sanitizedQuery);
res.locals.payload = { data: records || null, meta };
return next();
}),
respond
);
res.locals.payload = { data: records || null, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:pk',

View File

@@ -51,26 +51,25 @@ router.post(
respond
);
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new UsersService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new UsersService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const item = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_users', req.sanitizedQuery);
const item = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_users', req.sanitizedQuery);
res.locals.payload = { data: item || null, meta };
return next();
}),
respond
);
res.locals.payload = { data: item || null, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/me',

View File

@@ -50,26 +50,25 @@ router.post(
respond
);
router.get(
'/',
asyncHandler(async (req, res, next) => {
const service = new WebhooksService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const readHandler = asyncHandler(async (req, res, next) => {
const service = new WebhooksService({
accountability: req.accountability,
schema: req.schema,
});
const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
});
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
res.locals.payload = { data: records || null, meta };
return next();
}),
respond
);
res.locals.payload = { data: records || null, meta };
return next();
});
router.get('/', validateBatch('read'), readHandler, respond);
router.search('/', validateBatch('read'), readHandler, respond);
router.get(
'/:pk',

View File

@@ -2,25 +2,36 @@ import { RequestHandler } from 'express';
import asyncHandler from '../utils/async-handler';
import Joi from 'joi';
import { FailedValidationException, InvalidPayloadException } from '../exceptions';
import { sanitizeQuery } from '../utils/sanitize-query';
export const validateBatch = (scope: 'update' | 'delete'): RequestHandler =>
export const validateBatch = (scope: 'read' | 'update' | 'delete'): RequestHandler =>
asyncHandler(async (req, res, next) => {
if (req.method.toLowerCase() === 'get') {
req.body = {};
return next();
}
if (!req.body) throw new InvalidPayloadException('Payload in body is required');
let batchSchema = Joi.object()
.keys({
keys: Joi.array().items(Joi.alternatives(Joi.string(), Joi.number())),
query: Joi.object().unknown(),
})
.xor('query', 'keys');
// Every cRUD action has either keys or query
let batchSchema = Joi.object().keys({
keys: Joi.array().items(Joi.alternatives(Joi.string(), Joi.number())),
query: Joi.object().unknown(),
});
// In reads, you can't combine the two, and 1 of the two at least is required
if (scope !== 'read') {
batchSchema = batchSchema.xor('query', 'keys');
}
// In updates, we add a required `data` that holds the update payload
if (scope === 'update') {
batchSchema = batchSchema.keys({
data: Joi.object().unknown().required(),
});
}
// Accept an array of primary keys as batch payload for deletes
// In deletes, we want to keep supporting an array of just primary keys
if (scope === 'delete' && Array.isArray(req.body)) {
return next();
}
@@ -31,5 +42,10 @@ export const validateBatch = (scope: 'update' | 'delete'): RequestHandler =>
throw new FailedValidationException(error.details[0]);
}
// In reads, the query in the body should override the query params for searching
if (scope === 'read' && req.body.query) {
req.sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
}
return next();
});

View File

@@ -307,10 +307,14 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
const queryWithKeys = {
...query,
filter: {
...(query.filter || {}),
[primaryKeyField]: {
_in: keys,
},
_and: [
query.filter || {},
{
[primaryKeyField]: {
_in: keys,
},
},
],
},
};

View File

@@ -3,6 +3,14 @@ import url from 'url';
export function getCacheKey(req: Request) {
const path = url.parse(req.originalUrl).pathname;
const key = `${req.accountability?.user || 'null'}-${path}-${JSON.stringify(req.sanitizedQuery)}`;
let key: string;
if (path?.includes('/graphql')) {
key = `${req.accountability?.user || 'null'}-${path}-${JSON.stringify(req.params.query)}`;
} else {
key = `${req.accountability?.user || 'null'}-${path}-${JSON.stringify(req.sanitizedQuery)}`;
}
return key;
}

View File

@@ -3,7 +3,7 @@ import logger from '../logger';
import { parseFilter } from '../utils/parse-filter';
import { flatten, set, merge, get } from 'lodash';
export function sanitizeQuery(rawQuery: Record<string, any>, accountability: Accountability | null) {
export function sanitizeQuery(rawQuery: Record<string, any>, accountability?: Accountability | null) {
const query: Query = {};
if (rawQuery.limit !== undefined) {
@@ -129,7 +129,7 @@ function sanitizeMeta(rawMeta: any) {
return [rawMeta];
}
function sanitizeDeep(deep: Record<string, any>, accountability: Accountability | null) {
function sanitizeDeep(deep: Record<string, any>, accountability?: Accountability | null) {
const result: Record<string, any> = {};
if (typeof deep === 'string') {

View File

@@ -107,6 +107,10 @@ pre
pre,
pre[class*="language-"]
margin-top 0
div[class*="language"] + p
text-align right
margin-top -11px
font-size 14px
@media (min-width: 1000px)
.two-up

View File

@@ -145,3 +145,47 @@ foreign key. This means that your data will never suddenly disappear, but it als
orphaned items.
:::
## SEARCH HTTP Method
When using the REST API to read multiple items by (very) advanced filters, you might run into the issue where the URL
simply can't hold enough data to include the full query structure. In those cases, you can use the SEARCH http method as
a drop-in replacement for GET, where you're allowed to put the query into the request body as follows:
**Before:**
```
GET /items/articles?filter[title][_eq]=Hello World
```
**After:**
```json
SEARCH /items/articles
{
"query": {
"filter": {
"title": {
"_eq": "Hello World"
}
}
}
}
```
There's a lot of discussion around whether or not to put a body in a GET request, to use POSTs to create search queries,
or to rely on a different method altogether. As of right now, we've chosen
[to align with IETF's _HTTP SEARCH Method_ specification](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/).
While we recognize this is still a draft spec, the SEARCH method has been used extensively before in the WebDAV world
([spec](https://tools.ietf.org/html/rfc5323)), and compared to the other available options, it feels like the "cleanest"
and most correct to handle this moving forward. As with everything else, if you have any ideas, opinions, or concerns,
[we'd love to hear your thoughts](http://github.com/directus/directus/discussions/new).
Useful reading:
- [_HTTP SEARCH Method_ (IETF, 2021)](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/)
- [_Defining a new HTTP method: HTTP SEARCH_ (Tim Perry, 2021)](https://httptoolkit.tech/blog/http-search-method/)
- [_HTTP GET with request body_ (StackOverflow, 2009 and ongoing)](https://stackoverflow.com/questions/978061/http-get-with-request-body)
- [_Elastic Search GET body usage_ (elastic, n.d.)](https://www.elastic.co/guide/en/elasticsearch/guide/current/_empty_search.html)
- [_Dropbox starts using POST, and why this is poor API design._ (Evert Pot, 2015)](https://evertpot.com/dropbox-post-api/)

View File

@@ -72,7 +72,8 @@ will be an empty array.
#### Singleton
If your collection is a singleton, this endpoint will return the item.
If your collection is a singleton, this endpoint will return the item. If the item doesn't exist in the database, the
default values will be returned.
</div>
<div class="right">
@@ -81,8 +82,11 @@ If your collection is a singleton, this endpoint will return the item.
```
GET /items/:collection
SEARCH /items/:collection
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
##### Example
```

View File

@@ -103,8 +103,11 @@ available, data will be an empty array.
```
GET /activity
SEARCH /activity
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -152,8 +152,11 @@ An array of [collection objects](#the-collection-object).
```
GET /collections
SEARCH /collections
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -150,8 +150,11 @@ will be an empty array.
```
GET /files
SEARCH /files
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -77,8 +77,11 @@ data will be an empty array.
```
GET /folders
SEARCH /folders
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -112,8 +112,11 @@ available, data will be an empty array.
```
GET /permissions
SEARCH /permissions
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -126,8 +126,11 @@ data will be an empty array.
```
GET /presets
SEARCH /presets
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -102,8 +102,11 @@ available, data will be an empty array.
```
GET /relations
SEARCH /relations
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -102,8 +102,11 @@ available, data will be an empty array.
```
GET /revisions
SEARCH /revisions
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -107,8 +107,11 @@ will be an empty array.
```
GET /roles
SEARCH /roles
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -121,8 +121,11 @@ will be an empty array.
```
GET /users
SEARCH /users
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql

View File

@@ -97,8 +97,11 @@ available, data will be an empty array.
```
GET /webhooks
SEARCH /webhooks
```
[Learn more about SEARCH ->](/reference/api/introduction/#search-http-method)
### GraphQL
```graphql