Git Product home page Git Product logo

kql's Introduction

Kirby QL

Kirby's Query Language API combines the flexibility of Kirby's data structures, the power of GraphQL and the simplicity of REST.

The Kirby QL API takes POST requests with standard JSON objects and returns highly customized results that fit your application.

Playground

You can play in our KQL sandbox. The sandbox is based on the Kirby starterkit.

โ„น๏ธ Source code of the playground is available on GitHub.

Example

Given a POST request to: /api/query

{
  "query": "page('photography').children",
  "select": {
    "url": true,
    "title": true,
    "text": "page.text.markdown",
    "images": {
      "query": "page.images",
      "select": {
        "url": true
      }
    }
  },
  "pagination": {
    "limit": 10
  }
}
๐Ÿ†— Response
{
  "code": 200,
  "result": {
    "data": [
      {
        "url": "https://example.com/photography/trees",
        "title": "Trees",
        "text": "Lorem <strong>ipsum</strong> โ€ฆ",
        "images": [
          {
            "url": "https://example.com/media/pages/photography/trees/1353177920-1579007734/cheesy-autumn.jpg"
          },
          {
            "url": "https://example.com/media/pages/photography/trees/1940579124-1579007734/last-tree-standing.jpg"
          },
          {
            "url": "https://example.com/media/pages/photography/trees/3506294441-1579007734/monster-trees-in-the-fog.jpg"
          }
        ]
      },
      {
        "url": "https://example.com/photography/sky",
        "title": "Sky",
        "text": "<h1>Dolor sit amet</h1> โ€ฆ",
        "images": [
          {
            "url": "https://example.com/media/pages/photography/sky/183363500-1579007734/blood-moon.jpg"
          },
          {
            "url": "https://example.com/media/pages/photography/sky/3904851178-1579007734/coconut-milkyway.jpg"
          }
        ]
      }
    ],
    "pagination": {
      "page": 1,
      "pages": 1,
      "offset": 0,
      "limit": 10,
      "total": 2
    }
  },
  "status": "ok"
}

Installation

Manual

Download and copy this repository to /site/plugins/kql of your Kirby installation.

Composer

composer require getkirby/kql

Documentation

API Endpoint

KQL adds a new query API endpoint to your Kirby API (i.e. yoursite.com/api/query). This endpoint requires authentication.

You can switch off authentication in your config at your own risk:

return [
  'kql' => [
    'auth' => false
  ]
];

Sending POST Requests

You can use any HTTP request library in your language of choice to make regular POST requests to your /api/query endpoint. In this example, we are using the fetch API and JavaScript to retrieve data from our Kirby installation.

const api = "https://yoursite.com/api/query";
const username = "apiuser";
const password = "strong-secret-api-password";

const headers = {
  Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
  "Content-Type": "application/json",
  Accept: "application/json",
};

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "page('notes').children",
    select: {
      title: true,
      text: "page.text.kirbytext",
      slug: true,
      date: "page.date.toDate('d.m.Y')",
    },
  }),
  headers,
});

console.log(await response.json());

query

With the query, you can fetch data from anywhere in your Kirby site. You can query fields, pages, files, users, languages, roles and more.

Queries Without Selects

When you don't pass the select option, Kirby will try to come up with the most useful result set for you. This is great for simple queries.

Fetching the Site Title
const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "site.title",
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: "Kirby Starterkit",
  status: "ok"
}
Fetching a List of Page IDs
const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "site.children",
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: [
    "photography",
    "notes",
    "about",
    "error",
    "home"
  ],
  status: "ok"
}

Running Field Methods

Queries can even execute field methods.

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "site.title.upper",
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: "KIRBY STARTERKIT",
  status: "ok"
}

select

KQL becomes really powerful by its flexible way to control the result set with the select option.

Select Single Properties and Fields

To include a property or field in your results, list them as an array. Check out our reference for available properties for pages, users, files, etc.

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "site.children",
    select: ["title", "url"],
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: {
    data: [
      {
        title: "Photography",
        url: "/photography"
      },
      {
        title: "Notes",
        url: "/notes"
      },
      {
        title: "About us",
        url: "/about"
      },
      {
        title: "Error",
        url: "/error"
      },
      {
        title: "Home",
        url: "/"
      }
    ],
    pagination: {
      page: 1,
      pages: 1,
      offset: 0,
      limit: 100,
      total: 5
    }
  },
  status: "ok"
}

You can also use the object notation and pass true for each key/property you want to include.

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "site.children",
    select: {
      title: true,
      url: true,
    },
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: {
    data: [
      {
        title: "Photography",
        url: "/photography"
      },
      {
        title: "Notes",
        url: "/notes"
      },
      {
        title: "About us",
        url: "/about"
      },
      {
        title: "Error",
        url: "/error"
      },
      {
        title: "Home",
        url: "/"
      }
    ],
    pagination: { ... }
  },
  status: "ok"
}

Using Queries for Properties and Fields

Instead of passing true, you can also pass a string query to specify what you want to return for each key in your select object.

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "site.children",
    select: {
      title: "page.title",
    },
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: {
    data: [
      {
        title: "Photography",
      },
      {
        title: "Notes",
      },
      ...
    ],
    pagination: { ... }
  },
  status: "ok"
}

Executing Field Methods

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "site.children",
    select: {
      title: "page.title.upper",
    },
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: {
    data: [
      {
        title: "PHOTOGRAPHY",
      },
      {
        title: "NOTES",
      },
      ...
    ],
    pagination: { ... }
  },
  status: "ok"
}

Creating Aliases

String queries are a perfect way to create aliases or return variations of the same field or property multiple times.

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "page('notes').children",
    select: {
      title: "page.title",
      upperCaseTitle: "page.title.upper",
      lowerCaseTitle: "page.title.lower",
      guid: "page.id",
      date: "page.date.toDate('d.m.Y')",
      timestamp: "page.date.toTimestamp",
    },
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: {
    data: [
      {
        title: "Explore the universe",
        upperCaseTitle: "EXPLORE THE UNIVERSE",
        lowerCaseTitle: "explore the universe",
        guid: "notes/explore-the-universe",
        date: "21.04.2018",
        timestamp: 1524316200
      },
      { ... },
      { ... },
      ...
    ],
    pagination: { ... }
  },
  status: "ok"
}

Subqueries

With such string queries you can of course also include nested data

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "page('photography').children",
    select: {
      title: "page.title",
      images: "page.images",
    },
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: {
    data: [
      {
        title: "Trees",
        images: [
          "photography/trees/cheesy-autumn.jpg",
          "photography/trees/last-tree-standing.jpg",
          "photography/trees/monster-trees-in-the-fog.jpg",
          "photography/trees/sharewood-forest.jpg",
          "photography/trees/stay-in-the-car.jpg"
        ]
      },
      { ... },
      { ... },
      ...
    ],
    pagination: { ... }
  },
  status: "ok"
}

Subqueries With Selects

You can also pass an object with a query and a select option

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "page('photography').children",
    select: {
      title: "page.title",
      images: {
        query: "page.images",
        select: {
          filename: true,
        },
      },
    },
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: {
    data: [
      {
        title: "Trees",
        images: {
          {
            filename: "cheesy-autumn.jpg"
          },
          {
            filename: "last-tree-standing.jpg"
          },
          {
            filename: "monster-trees-in-the-fog.jpg"
          },
          {
            filename: "sharewood-forest.jpg"
          },
          {
            filename: "stay-in-the-car.jpg"
          }
        }
      },
      { ... },
      { ... },
      ...
    ],
    pagination: { ... }
  },
  status: "ok"
}

Pagination

Whenever you query a collection (pages, files, users, roles, languages) you can limit the resultset and also paginate through entries. You've probably already seen the pagination object in the results above. It is included in all results for collections, even if you didn't specify any pagination settings.

limit

You can specify a custom limit with the limit option. The default limit for collections is 100 entries.

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "page('notes').children",
    pagination: {
      limit: 5,
    },
    select: {
      title: "page.title",
    },
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: {
    data: [
      {
        title: "Across the ocean"
      },
      {
        title: "A night in the forest"
      },
      {
        title: "In the jungle of Sumatra"
      },
      {
        title: "Through the desert"
      },
      {
        title: "Himalaya and back"
      }
    ],
    pagination: {
      page: 1,
      pages: 2,
      offset: 0,
      limit: 5,
      total: 7
    }
  },
  status: "ok"
}

page

You can jump to any page in the resultset with the page option.

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "page('notes').children",
    pagination: {
      page: 2,
      limit: 5,
    },
    select: {
      title: "page.title",
    },
  }),
  headers,
});

console.log(await response.json());
๐Ÿ†— Response
{
  code: 200,
  result: {
    data: [
      {
        title: "Chasing waterfalls"
      },
      {
        title: "Exploring the universe"
      }
    ],
    pagination: {
      page: 2,
      pages: 2,
      offset: 5,
      limit: 5,
      total: 7
    }
  },
  status: "ok"
}

Pagination in Subqueries

Pagination settings also work for subqueries.

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "page('photography').children",
    select: {
      title: "page.title",
      images: {
        query: "page.images",
        pagination: {
          page: 2,
          limit: 5,
        },
        select: {
          filename: true,
        },
      },
    },
  }),
  headers,
});

console.log(await response.json());

Multiple Queries in a Single Call

With the power of selects and subqueries you can basically query the entire site in a single request

const response = await fetch(api, {
  method: "post",
  body: JSON.stringify({
    query: "site",
    select: {
      title: "site.title",
      url: "site.url",
      notes: {
        query: "page('notes').children.listed",
        select: {
          title: true,
          url: true,
          date: "page.date.toDate('d.m.Y')",
          text: "page.text.kirbytext",
        },
      },
      photography: {
        query: "page('photography').children.listed",
        select: {
          title: true,
          images: {
            query: "page.images",
            select: {
              url: true,
              alt: true,
              caption: "file.caption.kirbytext",
            },
          },
        },
      },
      about: {
        text: "page.text.kirbytext",
      },
    },
  }),
  headers,
});

console.log(await response.json());

Allowing Methods

KQL is very strict with allowed methods by default. Custom page methods, file methods or model methods are not allowed to make sure you don't miss an important security issue by accident. You can allow additional methods though.

Allow List

The most straight forward way is to define allowed methods in your config.

return [
  'kql' => [
    'methods' => [
      'allowed' => [
        'MyCustomPage::cover'
      ]
    ]
  ]
];

DocBlock Comment

You can also add a comment to your methods' doc blocks to allow them:

class MyCustomPage extends Page
{
  /**
   * @kql-allowed
   */
  public function cover()
  {
    return $this->images()->findBy('name', 'cover') ?? $this->image();
  }
}

This works for model methods as well as for custom page methods, file methods or other methods defined in plugins.

Kirby::plugin('your-name/your-plugin', [
  'pageMethods' => [
    /**
     * @kql-allowed
     */
    'cover' => function () {
      return $this->images()->findBy('name', 'cover') ?? $this->image();
    }
  ]
]);

Blocking Methods

You can block individual class methods that would normally be accessible by listing them in your config:

return [
  'kql' => [
    'methods' => [
      'blocked' => [
        'Kirby\Cms\Page::url'
      ]
    ]
  ]
];

Blocking Classes

Sometimes you might want to reduce access to various parts of the system. This can be done by blocking individual methods (see above) or by blocking entire classes.

return [
  'kql' => [
    'classes' => [
      'blocked' => [
        'Kirby\Cms\User'
      ]
    ]
  ]
];

Now, access to any user is blocked.

Custom Classes and Interceptors

If you want to add support for a custom class or a class in Kirby's source that is not supported yet, you can list your own interceptors in your config

return [
  'kql' => [
    'interceptors' => [
      'Kirby\Cms\System' => 'SystemInterceptor'
    ]
  ]
];

You can put the class for such a custom interceptor in a plugin for example.

class SystemInterceptor extends Kirby\Kql\Interceptors\Interceptor
{
  public const CLASS_ALIAS = 'system';

  protected $toArray = [
    'isInstallable',
  ];

  public function allowedMethods(): array
  {
    return [
      'isInstallable',
    ];
  }
}

Interceptor classes are pretty straight forward. With the CLASS_ALIAS you can give objects with that class a short name for KQL queries. The $toArray property lists all methods that should be rendered if you don't run a subquery. I.e. in this case kirby.system would render an array with the isInstallable value.

The allowedMethods method must return an array of all methods that can be access for this object. In addition to that you can also create your own custom methods in an interceptor that will then become available in KQL.

class SystemInterceptor extends Kirby\Kql\Interceptors\Interceptor
{
  ...

  public function isReady()
  {
    return 'yes it is!';
  }
}

This custom method can now be used with kirby.system.isReady in KQL and will return yes it is!

Unintercepted Classes

If you want to fully allow access to an entire class without putting an interceptor in between, you can add the class to the allow list in your config:

return [
  'kql' => [
    'classes' => [
      'allowed' => [
        'Kirby\Cms\System'
      ]
    ]
  ]
];

This will introduce full access to all public class methods. This can be very risky though and you should avoid this if possible.

No Mutations

KQL only offers access to data in your site. It does not support any mutations. All destructive methods are blocked and cannot be accessed in queries.

Plugins

What's Kirby?

  • getkirby.com โ€“ Get to know the CMS.
  • Try it โ€“ Take a test ride with our online demo. Or download one of our kits to get started.
  • Documentation โ€“ Read the official guide, reference and cookbook recipes.
  • Issues โ€“ Report bugs and other problems.
  • Feedback โ€“ You have an idea for Kirby? Share it.
  • Forum โ€“ Whenever you get stuck, don't hesitate to reach out for questions and support.
  • Discord โ€“ Hang out and meet the community.
  • Mastodon โ€“ Spread the word.
  • Instagram โ€“ Share your creations: #madewithkirby.

License

MIT License ยฉ 2020-2023 Bastian Allgeier

kql's People

Contributors

badmuts avatar bastianallgeier avatar benwest avatar distantnative avatar johannschopplich avatar lukasbestle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kql's Issues

Using "when()" in a query

I wondered if it is possible to use the more complex parts of the Kirby API in a query.

For example, I want to get all the pages with all children, and if children have "date" property, I want to sort them by date.

For example, that's the working query, with sorting by date:

{
  "pages": {
    "query": "site.children",
    "select": {
      "id": true,
      "status": true,
      "template": true,
      "unlisted": true,
      "slug": true,
      "date": true,
      "title": true,
      "headline": true,
      "intro": true,
      "hero_image": true,
      "items": "page.items.toStructure",
      "children": {
        "query": "page.children.sortBy('date', 'desc')",
        "select": {
          "id": true,
          "status": true,
          "template": true,
          "unlisted": true,
          "slug": true,
          "date": true,
          "title": true,
          "headline": true,
          "intro": true,
          "hero_image": true,
          "body": "page.body.kirbyText",
          "items": "page.items.toStructure",
          "children": {
            "query": "page.children.sortBy('date', 'desc')",
            "select": {
              "id": true,
              "status": true,
              "template": true,
              "unlisted": true,
              "slug": true,
              "date": true,
              "title": true,
              "headline": true,
              "intro": true,
              "hero_image": true,
              "items": "page.items.toStructure",
              "children": true
            }
          }
        }
      }
    }
  }
}

Now I wonder If there is a way to extend this to only sort by "date" when the child indeed is one of those blueprint types that defines a date.

I can't really wrap my head around how to use the when() in kql (if that is even possible), as it features function definitions.

RFC: KQL vs GraphQL

Iโ€™ve been casually following the development this plugin and I think itโ€™s pretty neat. I like seeing innovation in this area. Iโ€™m creating this issue to hopefully prompt some discussion about adopting an existing standard vs creating a new one.


GraphQL has one major advantage over KQL: itโ€™s a standard thatโ€™s been adopted all over the place. This has resulted in a battle-tested design thatโ€™s got a ton of tooling built around it.

Design

The GraphQL spec describes a ton of functionality but Iโ€™ve highlighted a few of my favourite features:

  • Predictability: Data is always a known shape.
  • Nullability: Developers can reliably know whether or not a field is potentially null, which leads to much more resiliently-built websites.
  • Standard error handling: Resolution failures bubble up to the nearest nullable field. If something in a field resolver goes catastrophically wrong, the damage is contained in a standard way.
  • Partial responses/response with errors: If one or more fields fail to resolve, a partial response is sent with an accompanying error array that shows the fields failed to resolve and the specific error per-field. Errors can be handled differently on a per-request basis.

Tooling

The GraphQL community has built all sorts of nifty things around the GraphQL standard. There are IDE/text editor integrations that provide autocomplete as youโ€™re writing queries. Queries are syntax highlighted just about everywhere. There are GraphQL server and client implementations for just about every web-friendly language.

One of my recent favourite community-built tools is graphql-codegen. Hereโ€™s how I use it in my personal projects:

  • I generate TypeScript types for API calls. GraphQL types and enums translate directly to TypeScript interfaces and enums. Nullability transfers over in the types so when Iโ€™m digging through a query response object, I know whether or not Iโ€™m accessing something safely.

  • I generate async fetch functions for each GraphQL query:

    This query:

    query exampleQuery($limit: Int = 10, $offset: Int = 0) {
      page(id: "photography") {
        children(limit: $limit, offset: $offset) {
          title
          text
          images {
            url
            srcset
          }
        }
      }
    }

    becomes this fetch function:

    const exampleQueryText = `...`;
    
    export const exampleQuery = (params: ExampleQueryParams, options?: ApolloQueryOptions): ExampleQueryResult => {
      return apolloClient.query<ExampleQueryParams, ExampleQueryResult>(exampleQueryText, params, options);
    }

Sorry for this lengthy issue. I donโ€™t want to discourage you in the further development of this plugin, but I did want to provide some perspective from the GraphQL side of things. Let me know if you have any questions.

I know KQL has the potential to be quite a bit more powerful than GraphQL but that power comes at the expense of being able to take advantage of a wealth of tools that are available today. For the headless Kirby use case I think thatโ€™s a pretty big deal.

Custom Page Model

Hello

I installed this plugin recently, but I'm having trouble reading some data of pages with a custom page model.
I get the following error when I call the API.

Query
{ "query": "page('nieuws').children", "select": { "url": true, "title": true, "text": "page.text.kirbytext" } }

Response error
Kirby\\Cms\\Page::__construct(): Argument #1 ($props) must be of type array, NewsPage given, called in /home/<user>/domains/<domain>/public_html/site/plugins/kql/src/Kql/Interceptor.php on line 254

On the forum there was a topic about this problem with a solution, but it's not working for me.
https://forum.getkirby.com/t/custom-pagemodels-in-kql/28195

It crashes the entire website when I use this. Is there a way to disable the interceptors or add the PageModels without adding the Kirby\Cms namespace?

TypeScript types

I've been making some moves on TypeScript typings for KQL queries. It's early days, but I am able to derive response types from queries pretty well already.

I'm testing and developing it in my current project, and I'm keen for anyone else to give it a try or contribute (In particular, the models.ts file is incomplete and needs filling out with the types of all Kirby's built-in stuff).

https://github.com/benwest/kql-ts

import { createClient } from "./kql/client";
import { KQLQueryData } from "./kql/query";

const query = {
  query: "site",
  select: {
    title: true,
    logo: {
      query: "site.logo.toFile",
      select: {
        srcset: true,
        width: true,
        height: true,
        placeholder: "file.resize(5).url",
      },
    },
    pages: {
      query: "site.children",
      select: {
        title: true,
        tags: `page.tags.split(',')`,
      },
    },
  },
} as const; // <- important

type Content = KQLQueryData<typeof query>;
/*   ^^^^^^^
{
  readonly title: string;
  readonly logo: {
      readonly srcset: string;
      readonly width: number;
      readonly height: number;
      readonly placeholder: string;
  } | null;
  readonly pages: {
      readonly title: string;
      readonly tags: string[];
  }[];
} */

const kql = createClient({
  user: KQL_USER,
  password: KQL_PASSWORD,
  url: KQL_URL,
});

const response = await kql(query);
if (response.status === "ok") {
  const data = response.result; // strongly typed!
}

[3.8.0-rc.2] Trying to include `uuid` in response returns 403

Note: I initially mentioned this issue here in Discord.

Example response object:

{
	"status": "error",
	"message": "The method \"Kirby\\Cms\\Page::uuid()\" is not allowed in the API context",
	"code": 403,
	"exception": "Kirby\\Exception\\PermissionException",
	"key": "error.permission",
	"file": "\/site\/plugins\/kql\/src\/Kql\/Interceptors\/Interceptor.php",
	"line": 43,
	"details": [],
	"route": "query"
}

Allow empty field values for certain methods

{
    "status": "error",
    "exception": "TypeError",
    "message": "Argument 1 passed to Kirby\\Text\\Markdown::parse() must be of the type string, null given, called in โ€ฆ/components.php on line 113",
    "file": "",
    "line": 64,
    "code": 500,
    "route": "query"
}

The error is caused by an empty field within a structure entry (caption for an image). It'd be great if the plugin would check for a valid value before a respective method is applied. In this case it is the markdown method. The field in question is not filled in for every structure entry, thus the error.

How to select URL of optional file easily?

I have a page model where an optional file can be added. I would like to select the file's URL (among other fields of the page) using KQL. The easiest way to get a file's URL seems to be like this:

{
    "query": "page('some-page')",
    "select": {
        "someFileUrl": "page.someFile.toFile.url"
    }
}

However, when the file (someFile) is empty/null, I get an error response:

{
    "code": 403,
    "details": [],
    "exception": "Kirby\\Exception\\PermissionException",
    "file": "/site/plugins/kql/src/Kql/Interceptors/Interceptor.php",
    "key": "error.permission",
    "line": 43,
    "message": "The method \"Kirby\\Cms\\Field::url()\" is not allowed in the API context",
    "route": "query",
    "status": "error"
}

I had expected/hoped that in case of an empty file, the URL would fallback to a null response, instead of the above error.

After some trial-and-error, I got this alternative approach working:

{
    "query": "page('some-page')",
    "select": {
        "someFile": {
            "query": "page.someFile.toFile",
            "select": ["url"]
        }
    }
}

This returns { "someFile": null } if the file is empty, and { "someFile": { "url": "..." } } otherwise. The downside is that my query is a lot more verbose, and also the output is slightly more complex (a nested object instead of the URL at the root level).

It's not a big problem of course, but is there another way to get my desired output using KQL? If not, perhaps this could be something to improve?

adding custom interceptors

this is currently not easily possible. interceptors need the base class to be in kirby namespace. thats not practicall for businuess logic.

kql needs a hook/closure to add stuff like this...

        $className   = get_class($object);
        $interceptor = str_replace('Kirby\\', 'Kirby\\Kql\\Interceptors\\', $className);
       // add other base namespaces...
        $interceptor = str_replace('Mynamespace\\', 'Kirby\\Kql\\Interceptors\\', $interceptor);

Support for "blocks" function of Kirby Editor

Describe the bug
I can't clearly tell whether this is a kql plugin issue or a editor plugin issue.

When querying an editor field via kql plugin, the page.field.blocks only returns the block ids.

To Reproduce
Steps to reproduce the behavior:

  1. Install the kql plugin
  2. Install editor plugin
  3. Create a blueprint using the editor field, e.g. on the site.yml, named my_editor_field
  4. Query the API with an editor block
  5. You get the JSON result
  6. Now query the API but use the blocks function as described in the editor plugin wiki
  7. API responds with an array of block ids.

Here's a random example. The my_editor_field_1 would output json. The my_editor_field_2 would output an array of block ideas.

Example query

{
  "query": "site",
  "select": {
    "my_editor_field_1": "page.my_editor_field",
    "my_editor_field_2": "page.my_editor_field.blocks"
  }
}

Expected behavior
When using the blocks function in the field query of kql, I would expect it to return the editors content as HTML the same way this function works outside of KQL.

Support for getting draft entries

We're needing a way to include draft content in query results for a custom headless preview setup.

Perhaps this is already supported?

Srcset with configs presets not working

Hey guys,

I would like to query the srcset of an image file with a preset defined in the config. Unfortunately this is not working. When defining explicit size values it is working. Is this a bug or do I do something wrong.

images: {
  query: "structureItem.image.toFiles",
  select: {
    url: "file.url",
    thumbs: "file.srcset([300, 800, 1024])",
    thumbsNotWorking1: "file.srcset()",
    thumbsNotWorking2: "file.srcset('default')"
  }
}

I defined the preset like this:

'thumbs' => [
  'srcsets' => [
      'default' => [300, 800, 1024]
  ]
]

Thanks for your help.

Best regards
Chris

Add queries to get translations

As previously asked in the Kirby Forum, whether there is a way to get the translations via kql, it turned out, it is currently not possible.

Would be very cool if this could be added. Sadly, I just don't have a lot of knowledge with PHP so that I could easily contribute to that.

CORS issue with Kirby QL

Description

After Updating Kirby 3.9.7 to Kirby 4.0.0-beta-2 , the request query status responds with error code 401 with message "Unauthenticated".

Expected behavior
The request should be successful and return status code 200 OK

Screenshots
Here is my config.php and .htaccess files to document accessibility. It works fine with Kirby 3.9.7

Screenshot 2023-10-24 at 11 44 52 Screenshot 2023-10-24 at 11 45 26

To reproduce

  1. Replace Kirby folder on the server
  2. Go to browser and reach the website url
  3. Go to dev tools and check console message
    Result : Error message
    " POST https://my-website.com/api/query 403 (forbidden) "

Your setup

Kirby Version
Update from Kirby 3.9.7 to Kirby 4.0.0-beta-2

Your system (please complete the following information)

  • Device: Mac Pro
  • OS: macOS Monterey
  • Browser: Chrome/ Safari / Firefox

Additional context
using Kirby QL and React

How to query custom site methods?

Hi there โ€“ I've created a custom site method that I would like to query but am have trouble doing so. The custom site method works from the Kirby instance.

Here's my method:

<?php


Kirby::plugin('lettau/custom-site-methods', [
    'siteMethods' => [
        'venues' => function () {
            /**
             * @kql-allowed
             */
            //  custom site method goes here
            $site = kirby()->site();
            $array = [];
            $reviews = $site->find('reviews')->children()->listed()->flip();

            // exhibitions yaml
            foreach ($reviews as $review) {
                $exhibitions = $review->exhibitions()->yaml();
                $venues = A::pluck($exhibitions, 'venue');
                $list = implode(', ', $venues);

                array_push($array, $list);
            }

            // venues field
            foreach ($reviews as $review) {
                $venues = $review->venue();
                array_push($array, $venues);
            }

            $array = implode(', ', $array);
            $array = explode(', ', $array);
            $array = array_unique($array);
            $array = array_filter($array);
            sort($array);

            return $array;
        }
    ]
]);

I have tried to allowed methods in the config as follows:

return[
  'kql' => [
    'methods' => [
      'allowed' => [
        'siteMethods::venues'
      ],
    ]
  ],
]

I make the following query from KQL:

 {
    query: `site.venues`,
  }

But nothing is returned:

{
    "code": 200,
    "result": "",
    "status": "ok"
}

how to select and transform data from structures

i tried field.toArray and field.toStructure combined with arrayItem and structureItem like in query language but that did not work. best i could find out was to use field.yaml or a custom pagemethod.

i would love to see support for arrayItem and structureItem in kql since that would be most consistent imho.

[Headless] Question - Is this csrf-token protected?

Hello,

I would like to use the query language extension to effectively use Kirby as a headless REST CMS (seperated React frontend). The only problem I see is, that this query language is using POST Requests, and afaik kirby uses csrf protection and there seems to be no (safe) way to get the csrf-token on the frontend.

But is it even csrf protected?

PS: CSRF Protection would still be applied, but instead of Kirby I would integrate it into my backend API which would fetch the kirby cms data which is protected via basic auth so that only the backend api has access to kirby.

Thanks in advance!
-eder

Kirby 4 compatibility

I tried KQL with Kirby 4 (alpha 5), but the output is always an error: Access to the class \"Kirby\Content\Field\" is not supported.
The same queries work as expected on Kirby 3.9.5.

โ€”

Example of POST request:

{
  "query": "page('home')",
  "select": {
    "title": "page.title"
  }
}

โŒ Response:

{
  "status":"error",
  "message":"Access to the class \"Kirby\\Content\\Field\" is not supported",
  "code":403,
  "exception":"Kirby\\Exception\\PermissionException",
  "key":"error.permission",
  "file":"Interceptor.php",
  "line":269,
  "details":[],
  "route":"query"
}

(Thanks so much for this essential plugin)

CI

  • Unit tests
  • Code quality

add an optional cache per query

implemented via usual plugin cache. config via each query...

{
    "query": "page('photography').children",
    "cache": {
       "key": "myquery-{{ page.id }}",
       "expire": 60
    },
    "select": {
...

Question - Get related children

I have a product that can have one case. A case can have multiple products.

Pulling the case inside a product is working fine, but now I need to query all products for a case.

This is my product.yml blueprint:

    fields:
      case:
        label: Case
        type: pages
        query: site.find('cases')
        multiple: false
        translate: false

Inside my case.yml I have not specified any relation.

How would I do this? As I have no relation specified in a case I am not sure what the best (kirby) way is to accomplish this. I found this: https://getkirby.com/docs/cookbook/content/filtering#filtering-by-the-pages-files-or-users-field

$page->children()->filter(function($child) use($page) {
    return $child->related()->toPages()->has($page);
});

but as I am not sure how I would write this in dot-notation or KQL.

Support for different selects based on object type (Block type/Page template/User role)

The issue

I want to access different properties based on block types. For an image, I might want to generate a srcset and get the alt text, but other blocks don't have an image, so the query would fail.

The previous solution would be to create a Block model with Kirby, with a toArray function. This would result in my "queries" being in two different locations - half of it in my frontend, and everything related to a specific Block type etc. would be in my backend.

Solution

I experimented with that idea here.

In short, I added a models property that allows to specify different selects based on the object type, similar to models in core.

const { result } = await fetchKql({
  query: 'page("home")',
  select: {
    title: true,
    blocks: {
      query: 'page.blocks.toBlocks',
      // needs select as potential fallback, empty array will return null if no matches
      // if not specified, query will not respect 'models' and return all fields
      select: ['type'],
      // 'models' as directory for different 'selects' based on class/block type
      models: {
        // 'select' for 'image' block type
        image: {
          type: true,
          image: 'block.image.toFile?.resize(200)',
          title: true
        },
        // 'select' for 'text' block type
        text: ['type', 'heading', 'text']
      }
    }
  }
})

This is a quick-and-dirty proof-of-concept, but it works for what I've tested it. Feel free to take any code from that repository for adding this feature.

[Docs] BasicAuth login only possible via email (instead of username)

Hi, just made my first steps connecting to the Kirby KQL endpoint.

The docs example states that we can connect via username and password (https://github.com/getkirby/kql#sending-post-requests).

const auth = {
  username: "apiuser",
  password: "strong-secret-api-password"
};

When I try to connect via username I get the following response:

image

When I use the emailaddress of the API user, everything work's fine:

image

(ThunderClient plugin VSCode)

Allow shorthand method *.kt

Version: 1.0

page.text.kt results in The method \"Kirby\\Cms\\Field::kt()\" is not allowed in the API context

should be allowed as page.text.kirbytext is allowed

thanks @bnomei who figured it out in discord

Pagination not working with "select"

I am trying to use the pagination option to limit results.

I am able to do this provided that I do not use the "select" query.

Whenever I use the select query, the pagination option are ignored and revert to default 100 limit.

I have used various queries, including this one below from the Pagination instructions. I get the same results: always a reversion to the default 100 limit.

Any ideas what is going wrong?

query: "page('notes').children", pagination: { limit: 5, }, select: { title: "page.title" } }, { auth });

Query for Previous / Next Sibling Entry

I'm trying to query for the next/prev sibling entry on a page like so:

const response = await $axios.post('api/query', {
    query: "page('projects/" + params.slug + "')",
        select: {
            title: true,
            nextProject: {
                query: 'page.next'
            }
        }
    )}
)}

It basically works an returns the whole page as an object:

entries:  {
    children: [],     
    content: {
        title: 'test',
       // all the other fields
    }
}  

However, when I try to specify the query, for example like below, I get an error.

const response = await $axios.post('api/query', {
    query: "page('projects/" + params.slug + "')",
        select: {
            title: true,
            nextProject: {
                query: 'page.next',
                select: {
                    title: true,
                    previewImage: {
                        query: 'page.previewImage.toFile'
                    }
                }
            }
        }
    )}
)}

I'm sure this is only a question of writing the query correctly, but since there is no documentation regarding this, I'm currently lost and appreciate all tipps & helps.

Access file inside Blocks field

Similar to issue #8 I want to access the files that are in my blocks field.
This is my query:

    {
      query: "site.children",
      select: {
        title: true,
        url: true,
        slug: true,
        allarticles: {
          query: "page.allarticles.toBlocks",
          select: {
            text: true,
            image: {
              query: "block.image.toFile",
            },
          },
        },
      },
    }, 

The text works fine, but the image reproduces a null.
I want to access the file because I want to have an srcset there.

Note: There are solutions with custom hooks written in the forum, but it would be nice to have it inside the query.

calling kirby and custom site methods

calling kirby.site and custom method does not seem to work. examples:
kirby.site.mailjetContactslists

but does
kirby.site.url

i guess that is intended. but why can any custom page method/field be called but not on site-object?

Block/Layout fields return type confusion

I'm seeing block and layout fields returned as a string of the raw object. I'd have expected either a string of HTML or the raw object.

What should the return type be for block and layout fields?

[Docs] Provide basic info about how to create a read-only api user

I tried out KQL with astro (https://astro.build/) recently, it took me some time to figure out how to create an read-only api user for authentication with the API.

Just as improvement suggestions for the README of this plugin, feel free to just close if not relevant ;-)

I just read pages via API by now, I guess files.read should also be enabled? I created the following user role:


Create an api user role in site/blueprints/users/api.yml and add a new user in the panel with that role:

title: Api
description: Api users, read-only
permissions:
  access:
    panel: true
    site: false
    languages: false
    system: false
    users: false
  files:
    create: false
    changeName: false
    delete: false
    read: false
    replace: false
    update: false
  languages:
    create: false
    delete: false
  pages:
    changeSlug: false
    changeStatus: false
    changeTemplate: false
    changeTitle: false
    create: false
    delete: false
    duplicate: false
    preview: false
    read: true
    sort: false
    update: false
  site:
    changeTitle: false
    update: false
  user:
    changeEmail: false
    changeLanguage: false
    changeName: false
    changePassword: false
    changeRole: false
    delete: false
    update: false
  users:
    changeEmail: false
    changeLanguage: false
    changeName: false
    changePassword: false
    changeRole: false
    create: false
    delete: false
    update: false

Return null or undefined for missing values

Now when I select a value that does not exist on an entry I get an empty string.
It would be nice to have the option to get null or undefined here.
I think this would make it easier to check for a null value than for an empty string.

Maybe there already is a way to do this via field methods that I'm not aware off?

Getting html content from editor blocks

From what I can tell (based on info here), this should work (but doesn't):

{
	"query": "page('events').children",
	"select": {
		"content": "page.content.blocks"
	}
}

(that returns an empty string)

Am I doing something obviously wrong here?

If only passing page.content then the raw data is returned (as expected), but I'd prefer to avoid having to parse it into HTML if possible.

How to query content from an editor field and get usable HTML?

Get full object of nested objects (related pages)

First of all thank you for the awesome plugin!

I have products that can have 1 related category. What I am trying to accomplish is to return all products including the categories headline.

I got this query:

 {
            query: "site",
            select: {
              title: "site.title",
              url: "site.url",
              products: {
                query: "page('products').children.listed",
                select: {
                  id: true,
                  title: true,
                  category: true
                }
              },
            }
          }

Which works fine, but it is only returning the category ID for the category value.

If I change category: true to categoryHeadline: 'page.category.headline' I get the following error:

 data: {
      status: 'error',
      message: 'The method "Kirby\\Cms\\Field::headline()" is not allowed in the API context',
      code: 403,
      exception: 'Kirby\\Exception\\PermissionException',
      key: 'error.permission',
      file: '/site/plugins/kql-main/src/Kql/Interceptors/Interceptor.php',
      line: 38,
      details: [],
      route: 'query'
    }

I am very new to KQL, is there something wrong with my approach? Or is pulling full related objects not possible with kql?

option to autoregister page/site/file-methods

instead of having to create and manually manage interceptors (or disable them altogether) maybe just add an option to automatically register any page, site and file methods that kirby knows about based on its config and used plugins.

why? i use custom methods a lot and kql is pretty hard to use then.

Plugin folder includes .git folder when installed via composer

I noticed, in comparison to other plugins, that the kql plugin folder contains the repositories .git folder after installation via composer. Now, I think, composer doesn't have a dedicated way of excluding files, but there is probably a way to remove the git folder when installed via composer.

I'm facing deployment issues with the .git folder due to the different permissions on this folder which I can resolve forcefully removing it before deployment. But still, I am curious if it could be excluded.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.