thomasaull

Rest-API site profile

Recommended Posts

I recently started to build Vue SPAs with ProcessWire as the backend, connected with a REST API. Thanks to code and the help of @LostKobrakai (How to use FastRoute with ProcessWire) and @clsource (REST-Helper) I got it up and running pretty quickly and now have put all of it in a site profile for others to use. It includes the REST API with routing for different endpoints, JWT Auth and a simple Vue SPA which shows the process of logging in a user (nevertheless, you don't have to use the Vue part, the API will work on it's own).

Check it out here: https://github.com/thomasaull/RestApiProfile

I'm pretty sure, it's not the perfect or most sophisticsted solution, but it gets the job done for me…

Feedback or Improvements are very welcome :)

  • Like 28
  • Thanks 2

Share this post


Link to post
Share on other sites

Seems great. Awesome Job!.

Thanks for sharing.

I think the JWT Auth may become a module on it´s own.

For folks who don´t know about JWT. It´s a standard for Authentication Using Json.

https://jwt.io/

:)

  • Like 2

Share this post


Link to post
Share on other sites
4 hours ago, thomasaull said:

I recently started to build Vue SPAs with ProcessWire as the backend, connected with a REST API.

Sounds awesome, never done anything like this before, can we see an example application?

Share this post


Link to post
Share on other sites
3 hours ago, SamC said:

Sounds awesome, never done anything like this before, can we see an example application?

Unfortunately, one is not public – the other one not finished (not public either, but I might put in in my portfolio with a screencast or anything)

  • Like 2

Share this post


Link to post
Share on other sites
5 hours ago, clsource said:

I think the JWT Auth may become a module on it´s own.

Are you working on it? ;) I'm not sure if you need a module for that, the only thing which makes sense being managed by the backend is the JWT Secret maybe… But by all means it might make things easy for people who are not familiar with JWT!

Share this post


Link to post
Share on other sites
13 minutes ago, thomasaull said:

Unfortunately, one is not public – the other one not finished (not public either, but I might put in in my portfolio with a screencast or anything)

Yeah that'd be good, thanks.

Share this post


Link to post
Share on other sites

Nice! 

But I wonder if the JWT are really necessary here—or why the implementation?
Doesn't the server session / processwire handle that already?

Share this post


Link to post
Share on other sites
On 4/12/2017 at 6:19 PM, thomasaull said:

Are you working on it? ;) I'm not sure if you need a module for that, the only thing which makes sense being managed by the backend is the JWT Secret maybe… But by all means it might make things easy for people who are not familiar with JWT!

Not really at the moment. Just an idea :)

4 hours ago, blynx said:

Nice! 

But I wonder if the JWT are really necessary here—or why the implementation?
Doesn't the server session / processwire handle that already?

JWT is just a communication system.
If you look at the profile is the Processwire auth system that is used at the end.

Instead of using oAuth or Basic Authentication
JWT can be used for getting user roles and data without requiring to log in the user on every request,
saving server resources :)

 

  • Like 2

Share this post


Link to post
Share on other sites
15 hours ago, blynx said:

Nice! 

But I wonder if the JWT are really necessary here—or why the implementation?
Doesn't the server session / processwire handle that already?

It's not necessary, just one way of authentication. Basic Auth is perfectly fine aswell :)

  • Like 1

Share this post


Link to post
Share on other sites

On a sidenote jwt is perfect when things need to "scale" (i.e. in a microservice architecture). Just share the secret across multiple instances and you don't have to care about syncing sessions and related data. The backend thus can be "stateless" (regarding logins) and just used to deliver data.

 

  • Like 3

Share this post


Link to post
Share on other sites

@thomasaull Thanks for the profile. Can see you've put a lot of time and effort into it.

Have been reading up on PWA's and while Vue.js seems to be the flavour of the month, I really don't like its '/#/' in the URL. Seems Stencil.js is a very similar option and gets around the URL issue. I'm new at this and decided to try your Vue.js option first.

Got through most of it thanks to your docs and then got stumped big time at the login process.

All good to here using postman:

5a83f33c75642_Screenshot2018-02-1419_28_30.png.d5d92b206e3913e06c536cf825dbc5e3.png

From then on, I got completely lost:

5a83f3a3606e0_Screenshot2018-02-1419_30_13.png.c24ffde9e363da253bf0f69f5233548d.png

Did all that then used postman to try to get the JWT-token and log in. Nope... every response was "user is not logged in"

After many & varied attempts, I finally realised that the first conditional in /site/templates/api/Auth.php says if the user isn't log in, throw a WireException... OK but I'm not logged in. I'm trying to log in...

Commented out the WireException then my postman GET request gave me my JWT token.

Tried to login from my site but still no access???

 

Doesn't seem to matter whether I have $authActive set to true or false.

What am I doing wrong?

At what point, if ever, does $authActive get set back to true?

Any and all help, especially if they involve more detailed help docs (examples are great!!!!), much appreciated.

Thanks

psy

 

 

Share this post


Link to post
Share on other sites

Got a bit further... this time the error reporting in Router.php pointed to Tracy Debugger which is not included in the profile:

  public static function handle($class, $method, $vars)
  {
//      $authActive = true;
      $authActive = false;
//     if(!$authActive) \TD::fireLog('WARNING: Auth disabled!');

Another thing I did not know how to fix was that the Vue build files only worked when uploaded to the same domain as PW. When I uploaded the final build static folder and index.html to another domain, it failed with:

Screenshot-2018-02-15-08_32_03.jpg.ddc7c69e2e2de27741a43ce7eef5f4f7.jpg

 

Does this mean I can only have one Vue SPA per PW instance? Would be good to have the ability for one PW instance to serve multiple Vue sites.

What's your recommendation for configuring, eg install PW in a subfolder and have the Vue build files in the root?

TIA

psy

 

Share this post


Link to post
Share on other sites

Still moving along slowly

Auth.php line 37 changed:

  public static function login($data) {
//      ApiHelper::checkRequiredParameters($data, ['username', 'password']);
      ApiHelper::checkAndSanitizeRequiredParameters($data, ['username', 'password']);

Next... in ApiHelper.php - again Tracy Debugger not installed/included in default profile:

      // Sanitize Data
      if (!$sanitizer) {
//        \TD::fireLog('WARNING: No Sanitizer specified for: ' . $name . ' Applying default sanitizer: text');
        $data->$name = wire('sanitizer')->text($data->$name);
      }

That then forced the next bit in ApiHelper.php to fail too, ie:

     $data->$name = wire('sanitizer')->$sanitizer($data->$name);

as $sanitizer is empty.

Maybe not me after all.

@thomasaull suspect you have the client file versions out of synch in the profile on github... what with TD calls and incorrect function names, etc. Please confirm.

Share this post


Link to post
Share on other sites

Sorry for the hazzle @psy. I‘m on the road atm, how important is this? I can try to get you some assistance maybe later today

Share this post


Link to post
Share on other sites

Hi @thomasaull keen to get it resolved but not urgent in the next few hours. When you get it right is best :) 

Just grateful for the API and want to get it working. Have lots of applications for a fully functional PW REST API that handles logins and sanitizer.

Happy to help you quality control/troubleshoot. Maybe we can take this conversation offline until the profile is ready?

Keep me posted.

Thx

 

Share this post


Link to post
Share on other sites

Ok so apparently my flight got wifi, so I'm able to answer now :)

Vue Router is also capable of suppressing the /#/ in URLs, you just need to add the HTML5 History Mode: https://router.vuejs.org/en/essentials/history-mode.html

Did you actually check and run the Vue Login example? Is everything working as supposed there?

The login does happen in the login function in Auth.php, the other function just makes sure the user is actually logged in and therefore allowed to query the API. 

$authActive never gets changed after the initial declaration, it's basically just a switch to make the API publicy available or not. 

To get rid of the errors by TracyDebugger you can just delete these lines, they are just in there for debugging. 

You can totally have multiple Vue SPAs querying the same API, although I have never tested this with this profile. There might be CORS related issues that needs to be solved first. For starters, I'd try to have it all under the same domain.

The checkAndSanitize works with. name|sanitizer pairs, check this line: https://github.com/thomasaull/RestApiProfile/blob/master/templates/api/Test.php#L17 (message is the parameter you're sending, text is the sanitizer which should be applied)

I think I forgot to change the function call in Auth.php, try: ApiHelper::checkAndSanitizeRequiredParameters($data, ['username|text', 'password|text']);

Try to change this line, remove all TracyDebugger statements and run the Vue example. I guess it should work then! I'm not with my computer until end of next week, so not really a big help I'm afraid 🙈

Share this post


Link to post
Share on other sites

Hi @thomasaull

In answer to your questions:

- Vue Router - thanks for the tip. Hadn't got that far yet

- Have run the installed-by-profile Vue Login example countless times - that's where I'm stuck. On the times I do get logged in, I'm only ever logged in as 'guest'

- OK I think about $authActive - but in the destructions you ask the dev to to change $authActive to false

- Yep, figured that but then in ApiHelper.php, $sanitizer is null so everything falls in a heap

- OK

- Ahhh yes, the line is just username, password - not username|text, password|text

Will try tomorrow. It's late here. Thanks for the help and safe travels

Cheers, psy

 

Share this post


Link to post
Share on other sites

Yay!!! Got it working successfully with a few tweaks to @thomasaull's templates.

In Auth.php:

  public static function login($data) {
//      ApiHelper::checkRequiredParameters($data, ['username', 'password']);

//    $data->username = wire('sanitizer')->selectorValue($data->username);
//    $data->password = wire('sanitizer')->string($data->password);

// All the below work as expected including logging an incorrect Sanitizer method
//    $data =   ApiHelper::checkAndSanitizeRequiredParameters($data, ['username', 'password']);
//    $data =   ApiHelper::checkAndSanitizeRequiredParameters($data, ['username|selectorValuexxx', 'password|string']);
      $data =   ApiHelper::checkAndSanitizeRequiredParameters($data, ['username|selectorValue', 'password|string']);

    $user = wire('users')->get($data->username);
...

The changes to ApiHelper::checkAndSanitizeRequiredParameters($data, $params) were a bit more intense:

  public static function checkAndSanitizeRequiredParameters($data, $params) {
    foreach ($params as $param) {
      // Split param: Format is name|sanitizer
      $name = explode('|', $param)[0];

        // Check if Param exists
        if (!isset($data->$name)) throw new \Exception('Required parameter "' . $param .'" missing!', 400);

        $sanitizer = stripos($param,'|') !== false ? explode('|', $param)[1] : 'text';

        // Check the sanitizer type is valid (Array copied from Sanitizer method testAll)
        $sanitizers = array(
            'alpha',
            'alphanumeric',
            'array',
            'bool',
            'date',
            'digits',
            'email',
            'emailHeader',
            'entities',
            'entities1',
            'entitiesMarkdown',
            'fieldName',
            'filename',
            'float',
            'int',
            'intArray',
            'intSigned',
            'intUnsigned',
            'markupToLine',
            'markupToText',
            'minArray',
            'name',
            'names',
            'pageName',
            'pageNameTranslate',
            'pageNameUTF8',
            'pagePathName',
            'pagePathNameUTF8',
            'path',
            'purify',
            'removeNewlines',
            'selectorField',
            'selectorValue',
            'string',
            'templateName',
            'text',
            'textarea',
            'unentities',
            'url',
            'varName',
        );
        if (!in_array($sanitizer, $sanitizers))
            throw new WireException(_("Invalid sanitizer type: $sanitizer for param $name"));

      $data->$name = wire('sanitizer')->$sanitizer($data->$name);
    }
    return $data;
  }

 

Share this post


Link to post
Share on other sites

@psy I (hopefully) resolved all issues with an update to the site profile. Can you give it a try and check if everything is working for you now? https://github.com/thomasaull/RestApiProfile/releases/tag/0.1.1

I didn't want to copy all the sanitizers in my site profile since it might break, when there will be more sanitizers added in the future. Instead I'm checking with method_exists if the specified sanitizer exists.

Thanks for all your input and work :)

  • Like 1

Share this post


Link to post
Share on other sites

@thomasaull Works great!

You say tomato and I say tomato :) 

One thing about putting the classes in templates rather than a module is that they can be modified to suit. I tweaked the new ApiHelper to make it more ProcessWire-ish for my own benefit - still does the same thing as your version.

    public static function checkAndSanitizeRequiredParameters($data, $params) {
        $sanitizer = wire('sanitizer');
        foreach ($params as $param) {
            // Split param: Format is name|sanitizer method
            $name = explode('|', $param)[0];
            // Check if Param exists
            if (!isset($data->$name)) throw new WireException(_("Required parameter: '$param' missing!"), 400);
            // Sanitize Data
            $parms = explode('|', $param);
            $sanMethod = 'text';
            if (isset($parms[1]))  $sanMethod = $parms[1];
            if(!method_exists($sanitizer, $sanMethod)) throw new WireException(_("Sanitizer: '$sanMethod' is invalid for param '$name'"), 400);
            $data->$name = $sanitizer->$sanMethod($data->$name);
        }
        return $data;
    }

Thanks for the update. :) 

 

  • Like 1

Share this post


Link to post
Share on other sites

Hi @thomasaull,
Thanks for this great site profile !

After building for production I'm having a 404 error when trying to reach a page from its url.
It works with the /login though but I guess this is because this page doesn't exist as a processwire page.

This problem is explained here https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations
but I'm not sure how to implement the Apache solution to make it work with processwire. 
 

Any ideas ?

Thanks!

Share this post


Link to post
Share on other sites

If it works for the login, it should work with every other route aswell. Did you create the route in /router/index.js? And are you trying to use HTML5 History Mode?

Share this post


Link to post
Share on other sites

Yes, i'm using history mode.
Also I'm using props.

{ 
  path: '/:section/',
  name: 'Section',
  component: Section,
  props: true
}

where :section is the name of the page.

It's working fine in dev environnement but not in production.

I think it's related to that issue :
https://router.vuejs.org/guide/essentials/history-mode.html

Quote

Here comes a problem, though: Since our app is a single page client side app, without a proper server configuration, the users will get a 404 error if they access http://oursite.com/user/id directly in their browser. Now that's ugly.


it's trying to reach the processwire page instead of redirecting to my app template (home.php)

Share this post


Link to post
Share on other sites

Ok, I figured out 

I just needed to use the home.php template for every other templates (in templates/template/files/ > Alternate template settings)

 

Thank you for your reply !

Share this post


Link to post
Share on other sites
Posted (edited)

Glad you figured it out! I think I ran into the same issue on another project earlier and I solived it like you by using "Alternate Template Filename". Since the frontend of a Vue App would be completely decoupled from ProcessWire anyway, another possible solution would be to set every page to "hidden" except the home page (didn't try that though). Edit: An that probably wouldn't allow deep linking on initial page load

Edited by thomasaull

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By EyeDentify
      Hello Fellow PW Fans and Gurus.

      I have run into a problem where i have created a template without a template file associated with it.
      Now i would like to HAVE a template file associated with it, not an alternate but as the main template file.

      So i thought, that's easy, I upload the template file and change it in template settings but I only get the Alternate template file setting?

      Ok so maybe i can change it via the API , so i wrote some code for this.
      <?PHP function changeTemplate($pages = null, $templates = null) { /* get the pages to change template on */ $logItems = $pages->findMany('template=tmp_log_item'); /* get the template object for our desired template */ $Template = $templates->get('tmp_new_log_item'); foreach($logItems AS $key => $logData) { /* use template object $Template to set template */ $logData->template = $Template; /* save page item */ $logData->save(); } } /* do the magic change */ changeTemplate(); ?> Needless to say, PW was not happy about this, It throw up an error message:
      Error: Exception: Invalid value sent to Page::setTemplate (in /home/virtual/mydomain/public_html/wire/core/Page.php line 1782)
      Now from what i understand i gave $logData->template the wrong type of value.
      So what should the value be to correct this?
      When I read the docs at: https://processwire.com/api/ref/page/
      the value can be "string" or "Template".

      So i gave it a string and it went haywire.

      How should I approach this cause I realy dont want to manually change template file on över 50 plus pages

      Maybe I should use the Template class to create the value that the API wanted, but I am unsure how to use it.

      Thank you for any info you can give me on this.

      If you want more info to help me, ask and I will try my best to give it to you.
      Updated and Solved
      I solved the problem and have updated the code to reflect this.

      Essentialy i used the get() method of $templates to get the template object for my template i wanted to use using the template name.
      I gave the Object to the $logData->template property and all went well.
      We learn something new every day.
      One Note:
      The template file you want to use must have been uploaded and added as a template in the Template section in the Admin for this code to work as expected.
    • By chcs
      After running this code in my module's __install() the template_id is not set. Any ideas why?  (The export contents from the template created and the one that I built with Admin are the same. Both exports have the template_id is set to mm_state. When I go to edit the field in Admin, the template is not showing on the Input tab and it doesn't show the template title on a page using the template that uses this field. Using processwire 3.0.98)

      // mm_state
      $field = $this->fields->get('mm_state');
      if (!@$field->id) $field = new Field();
      $field->type = $this->modules->get("FieldtypePage");
      $field->name = 'mm_state';
      $field->label = 'State';
      $field->derefAsPage = 2;
      $field->inputfield = "InputfieldSelect";
      $this->fields->save($field);
      $field->labelFieldName = 'title';
      $field->template_id = 'mm_state';
      $field->columnWidth = 100;
      $field->required = true;
      $this->fields->save($field);
    • By mattcohen
      Hi, I am wondering if someone can help me out with getting the count of the comments of a page? 
      I am using Ryan's comments module as a reviews system for my site and would like to display the count of the comments per page, as below 
       

       
      The shops part that you see above is something similar to this 
      <?php $set_page = $page->title; $count = $pages->count("template=prices, title=$set_page"); echo $count; ?>  
    • By louisstephens
      So I am making a front-end dashboard for an internal project, and was curious if it is possible to match 2 pages together based on their title and retrieve the data.
      To make it more clear:
      I have two pages "Section A"  and "Section B" that each has child pages with names (the specific person) with subpages for their services offered. 
      Section A - John Doe (name=john-doe) - Service 1 - Service 2 - Service 3 - Jane Doe (name=jane-doe) - Service 1 - Service 2 - Service 3 Section B - John Doe (name=john-doe) - Service 4 - Service 5 - Service 6 - Jane Doe (name=jane-doe) - Service 4 - Service 5 - Service 6 What I would like to achieve on the front-end (if it is even possibile), is an output like:
      John Doe - Service 1 - Service 2 - Service 3 - Service 4 - Service 5 - Service 6 Jane Doe - Service 1 - Service 2 - Service 3 - Service 4 - Service 5 - Service 6 Thinking aloud: use a find to get all pages using the template "service user" and then foreaching through their children (and possibly getting a few field values as well) to output in the front end. The service templates used in the sections vary slightly in which fields they use.
      <?php $findUsers = $pages->find("template=service-a (and or) service-b,(some how match based on names)"); //if no match exists, output what is available ?> <ul> <?php foreach ($findUsers as $groupedUser): ?> <li><?php echo $groupedUser->title; ?></li> <li><?php echo $groupedUser->service_a_location; ?></li> <li><?php echo $groupedUser->service_a_body; ?></li> <li><?php echo $groupedUser->service_b_location; ?></li> <li><?php echo $groupedUser->service_b_phone; ?></li> <?php endforeach; ?> </ul> Sorry, it is just me thinking aloud, but I was not even sure if it is possible to group them together like that. I was going to have a page for each "section" only displaying that data, but I thought it would be nice to display them all together as an overview for the user.
    • By Noel Boss
      Page Query Boss
      Build complex nested queries containing multiple fields and pages and return an array or JSON. This is useful to fetch data for SPA and PWA.
      You can use the Module to transform a ProcessWire Page or PageArray – even RepeaterMatrixPageArrays – into an array or JSON. Queries can be nested and contain closures as callback functions. Some field-types are transformed automatically, like Pageimages or MapMarker.
      Installation
      Via ProcessWire Backend
      It is recommended to install the Module via the ProcessWire admin "Modules" > "Site" > "Add New" > "Add Module from Directory" using the PageQueryBoss class name.
      Manually
      Download the files from Github or the ProcessWire repository: https://modules.processwire.com/modules/page-query-builder/
      Copy all of the files for this module into /site/modules/PageQueryBoss/ Go to “Modules > Refresh” in your admin, and then click “install” for the this module. Module Methods
      There are two main methods:
      Return query as JSON
      $page->pageQueryJson($query); Return query as Array
      $page->pageQueryArray($query); Building the query
      The query can contain key and value pairs, or only keys. It can be nested and 
      contain closures for dynamic values. To illustrate a short example:
      // simple query: $query = [ 'height', 'floors', ]; $pages->find('template=skyscraper')->pageQueryJson($query); Queries can be nested, contain page names, template names or contain functions and ProcessWire selectors:
      // simple query: $query = [ 'height', 'floors', 'images', // < some fileds contain default sub-queries to return data 'files' => [ // but you can also overrdide these defaults: 'filename' 'ext', 'url', ], // Assuming there are child pages with the architec template, or a // field name with a page relation to architects 'architect' => [ // sub-query 'name', 'email' ], // queries can contain closure functions that return dynamic content 'querytime' => function($parent){ return "Query for $parent->title was built ".time(); } ]; $pages->find('template=skyscraper')->pageQueryJson($query); Keys:
      A single fieldname; height or floors or architects 
      The Module can handle the following fields:
      Strings, Dates, Integer… any default one-dimensional value Page references Pageimages Pagefiles PageArray MapMarker FieldtypeFunctional A template name; skyscraper or city
      Name of a child page (page.child.name=pagename); my-page-name A ProcessWire selector; template=building, floors>=25
      A new name for the returned index passed by a # delimiter:
      // the field skyscraper will be renamed to "building": $query = ["skyscraper`#building`"]  
      Key value pars:
      Any of the keys above (1-5) with an new nested sub-query array:
      $query = [ 'skyscraper' => [ 'height', 'floors' ], 'architect' => [ 'title', 'email' ], ]  
      A named key and a closure function to process and return a query. The closure gets the parent object as argument:
      $query = [ 'architecs' => function($parent) { $architects = $parent->find('template=architect'); return $architects->arrayQuery(['name', 'email']); // or return $architects->explode('name, email'); } ] Real life example:
      $query = [ 'title', 'subtitle', // naming the key invitation 'template=Invitation, limit=1#invitation' => [ 'title', 'subtitle', 'body', ], // returns global speakers and local ones... 'speakers' => function($page){ $speakers = $page->speaker_relation; $speakers = $speakers->prepend(wire('pages')->find('template=Speaker, global=1, sort=-id')); // build a query of the speakers with return $speakers->arrayQuery([ 'title#name', // rename title field to name 'subtitle#ministry', // rename subtitle field to ministry 'links' => [ 'linklabel#label', // rename linklabel field to minlabelistry 'link' ], ]); }, 'Program' => [ // Child Pages with template=Program 'title', 'summary', 'start' => function($parent){ // calculate the startdate from timetables return $parent->children->first->date; }, 'end' => function($parent){ // calculate the endate from timetables return $parent->children->last->date; }, 'Timetable' => [ 'date', // date 'timetable#entry'=> [ 'time#start', // time 'time_until#end', // time 'subtitle#description', // entry title ], ], ], // ProcessWire selector, selecting children > name result "location" 'template=Location, limit=1#location' => [ 'title#city', // summary title field to city 'body', 'country', 'venue', 'summary#address', // rename summary field to address 'link#tickets', // rename ticket link 'map', // Mapmarker field, automatically transformed 'images', 'infos#categories' => [ // repeater matrix! > rename to categories 'title#name', // rename title field to name 'entries' => [ // nested repeater matrix! 'title', 'body' ] ], ], ]; if ($input->urlSegment1 === 'json') { header('Content-type: application/json'); echo $page->pageQueryJson($query); exit(); } Module default settings
      The modules settings are public. They can be directly modified, for example:
      $modules->get('PageQueryBoss')->debug = true; $modules->get('PageQueryBoss')->defaults = []; // reset all defaults Default queries for fields:
      Some field-types or templates come with default selectors, like Pageimages etc. These are the default queries:
      // Access and modify default queries: $modules->get('PageQueryBoss')->defaults['queries'] … public $defaults = [ 'queries' => [ 'Pageimages' => [ 'basename', 'url', 'httpUrl', 'description', 'ext', 'focus', ], 'Pagefiles' => [ 'basename', 'url', 'httpUrl', 'description', 'ext', 'filesize', 'filesizeStr', 'hash', ], 'MapMarker' => [ 'lat', 'lng', 'zoom', 'address', ], 'User' => [ 'name', 'email', ], ], ]; These defaults will only be used if there is no nested sub-query for the respective type. If you query a field with complex data and do not provide a sub-query, it will be transformed accordingly:
      $page->pageQueryArry(['images']); // returns something like this 'images' => [ 'basename', 'url', 'httpUrl', 'description', 'ext', 'focus'=> [ 'top', 'left', 'zoom', 'default', 'str', ] ]; You can always provide your own sub-query, so the defaults will not be used:
      $page->pageQueryArry([ 'images' => [ 'filename', 'description' ], ]); Overriding default queries:
      You can also override the defaults, for example
      $modules->get('PageQueryBoss')->defaults['queries']['Pageimages'] = [ 'basename', 'url', 'description', ]; Index of nested elements
      The index for nested elements can be adjusted. This is also done with defaults. There are 3 possibilities:
      Nested by name (default) Nested by ID Nested by numerical index Named index (default):
      This is the default setting. If you have a field that contains sub-items, the name will be the key in the results:
      // example $pagesByName = [ 'page-1-name' => [ 'title' => "Page one title", 'name' => 'page-1-name', ], 'page-2-name' => [ 'title' => "Page two title", 'name' => 'page-2-name', ] ] ID based index:
      If an object is listed in $defaults['index-id'] the id will be the key in the results. Currently, no items are listed as defaults for id-based index:
      // Set pages to get ID based index: $modules->get('PageQueryBoss')->defaults['index-id']['Page']; // Example return array: $pagesById = [ 123 => [ 'title' => "Page one title", 'name' => 123, ], 124 => [ 'title' => "Page two title", 'name' => 124, ] ] Number based index
      By default, a couple of fields are transformed automatically to contain numbered indexes:
      // objects or template names that should use numerical indexes for children instead of names $defaults['index-n'] => [ 'Pageimage', 'Pagefile', 'RepeaterMatrixPage', ]; // example $images = [ 0 => [ 'filename' => "image1.jpg", ], 1 => [ 'filename' => "image2.jpg", ] ] Tipp: When you remove the key 'Pageimage' from $defaults['index-n'], the index will again be name-based.
       
      Help-fill closures & tipps:
      These are few helpfill closure functions you might want to use or could help as a
      starting point for your own (let me know if you have your own):

      Get an overview of languages:
          $query = ['languages' => function($page){         $ar = [];         $l=0;         foreach (wire('languages') as $language) {             // build the json url with segment 1             $ar[$l]['url']= $page->localHttpUrl($language).wire('input')->urlSegment1;             $ar[$l]['name'] = $language->name == 'default' ? 'en' : $language->name;             $ar[$l]['title'] = $language->getLanguageValue($language, 'title');             $ar[$l]['active'] = $language->id == wire('user')->language->id;             $l++;         }         return $ar;     }]; Get county info from ContinentsAndCountries Module
      Using the [ContinentsAndCountries Module](https://modules.processwire.com/modules/continents-and-countries/) you can extract iso
      code and names for countries:
          $query = ['country' => function($page){         $c = wire('modules')->get('ContinentsAndCountries')->findBy('countries', array('name', 'iso', 'code'),['code' =>$page->country]);         return count($c) ? (array) $c[count($c)-1] : null;     }]; Custom strings from a RepeaterTable for interface
      Using a RepeaterMatrix you can create template string for your frontend. This is
      usefull for buttons, labels etc. The following code uses a repeater with the
      name `strings` has a `key` and a `body` field, the returned array contains the `key` field as,
      you guess, keys and the `body` field as values:
          // build custom translations     $query = ['strings' => function($page){         return array_column($page->get('strings')->each(['key', 'body']), 'body', 'key');     }]; Multilanguage with default language fallback
      Using the following setup you can handle multilanguage and return your default
      language if the requested language does not exist. The url is composed like so:
      `page/path/{language}/{content-type}` for example: `api/icf/zurich/conference/2019/de/json`
       
          // get contenttype and language (or default language if not exists)     $lang = wire('languages')->get($input->urlSegment1);     if(!$lang instanceof Nullpage){         $user->language = $lang;     } else {         $lang = $user->language;     }     // contenttype segment 2 or 1 if language not present     $contenttype = $input->urlSegment2 ? $input->urlSegment2 : $input->urlSegment1;     if ($contenttype === 'json') {         header('Content-type: application/json');         echo $page->pageQueryJson($query);         exit();     } Debug
      The module respects wire('config')->debug. It integrates with TracyDebug. You can override it like so:
      // turns on debug output no mather what: $modules->get('PageQueryBoss')->debug = true; Todos
      Make defaults configurable via Backend. How could that be done in style with the default queries?
      Module in alpha Stage: Subject to change
      This module is in alpha stage … Query behaviour (especially selecting child-templates, renaming, naming etc)  could change