Difference between revisions of "Includes"

From SocialStack
 
(15 intermediate revisions by 2 users not shown)
Line 1: Line 1:
Includes are a mechanism in SocialStack which allow you to request additional content when obtaining an entity (or a list of entity) from the API. For example, you have a loop of things and would like to also display the name of the user who created each one. They are not case sensitive.
Includes are a mechanism in SocialStack which allow you to request additional content when obtaining an entity (or a list of entities) from the API. For example, you have a loop of things and would like to also display the name of the user who created each one. Includes are not case sensitive.


=Frontend Usage=
=Frontend Usage=
Line 45: Line 45:
<syntaxhighlight lang="js" line="1">
<syntaxhighlight lang="js" line="1">
     webRequest('page/1', null, {includes: ['creatorUser']});
     webRequest('page/1', null, {includes: ['creatorUser']});
</syntaxhighlight>
In some rare cases you may need a [[forms|<Form>]] response to include something custom. By default edit and create include * with their response, but they don't know about any nested includes that you may need. Form has a requestOpts prop which gives you the ability to set raw values on the webRequest that it makes, allowing us to essentially do the same as above but with a form instead:
<syntaxhighlight lang="js" line="1">
    <Form action='page' requestOpts={{includes: ['*', 'creatorUser.tags']}}>
    ..
</syntaxhighlight>
</syntaxhighlight>


=API Usage=
=API Usage=


The frontend is actually just adding any includes on as a GET parameter, like so:
The frontend is actually just adding any includes on as a GET parameter, like so. If you have more than one, they are comma separated:


https://www.example.com/v1/page/1?includes=creatorUser
https://www.example.com/v1/page/1?includes=creatorUser
Line 61: Line 68:
</syntaxhighlight>
</syntaxhighlight>


Nesting has no depth limit - you can use 3+ layers deep if you need to. Because you're spelling out each depth, there is no risk of recursion or cyclical behaviour either - you can include the creatorUser of a creatorUser, even if they are the same object. (And yes, Users do have a creatorUser as well - it can be used to identify if a user account was created by an admin for example).


=Show me everything=
 
=Wildcards=


You can wildcard include to show all available current level includes, and this is available nested as well:
You can wildcard include to show all available current level includes, and this is available nested as well:
Line 71: Line 80:


Note that the wildcard is only supported as the last field (*.creatorUser is not available for example, as not everything necessarily supports the named field you want).
Note that the wildcard is only supported as the last field (*.creatorUser is not available for example, as not everything necessarily supports the named field you want).
Wildcards also do not show explicit includes (ones where you must explicitly list the field name).


=Making something includable=
=Making something includable=
Line 80: Line 91:
To make a single object, you'll need to create a "virtual field" on your C# type:
To make a single object, you'll need to create a "virtual field" on your C# type:


<syntaxhighlight lang="js" line="1">
<syntaxhighlight lang="csharp" line="1">
     [HasVirtualField("OtherTypeName", typeof(OtherTypeName), "OtherTypeNameId")]
     [HasVirtualField("OtherTypeName", typeof(OtherTypeName), "OtherTypeNameId")]
     public partial class MyCustomType : VersionedContent<uint>
     public partial class MyCustomType : VersionedContent<uint>
Line 96: Line 107:
For example, this is how creatorUser is actually defined:
For example, this is how creatorUser is actually defined:


<syntaxhighlight lang="csharp" line="1">
[HasVirtualField("CreatorUser", typeof(User), "UserId")]
[HasVirtualField("CreatorUser", typeof(User), "UserId")]
</syntaxhighlight>
"Create an includable field called CreatorUser, which will be a User type, and the ID of the user to include comes from the UserId field".
=== On the admin panel ===
A virtual field declared in this way will cause a convenient dropdown selector to appear for the ID field on the admin panel (UI/ContentSelect). Sometimes though your included type may have thousands or millions of rows, so a dropdown is not appropriate and instead a search first mechanism is required. You can turn on search mode like so:
<syntaxhighlight lang="csharp" line="1">
[Data("titleField", "Name")] /* The field name to use as the title in the results list (or in the dropdown) */
[Data("search", "true")] /* Use search mode instead of a dropdown of all */
public uint OtherTypeNameId;


"Create an includable field called CreatorUser, which will be a User type, and the ID of the user to include comes from the UserId column".
</syntaxhighlight>


==Lists==
==Lists==
Line 104: Line 129:
To include a list of things on a type (for example, a list of answers on a poll), you'll need to mark the C# type as listable:
To include a list of things on a type (for example, a list of answers on a poll), you'll need to mark the C# type as listable:


<syntaxhighlight lang="js" line="1">
<syntaxhighlight lang="csharp" line="1">
     [ListAs("MyCustomTypes")]
     [ListAs("MyCustomTypes")]
    [HasVirtualField("OtherTypeName", typeof(OtherTypeName), "OtherTypeNameId")]
     public partial class MyCustomType : VersionedContent<uint>
     public partial class MyCustomType : VersionedContent<uint>
     {
     {
         public uint OtherTypeNameId;
         public uint OtherTypeNameId;
       
        public string Name;
     }
     }
</syntaxhighlight>
</syntaxhighlight>


You can use multiple ListAs declarations. Users is a good example of a use case for this - there is a list of permitted users on other types, but also you might want a list of members etc.
You can use multiple ListAs declarations. Users is a good example of a use case for this - there is a list of permitted users on other types, but also you might want a list of members etc.


==On: Includes without the entity==
=== On the admin panel ===
 
By default a ListAs field will appear as a content selector on the admin panel. This content selector uses a searchbar to find the content you're after and it will default to using the [[Content_Type_Meta_Fields|meta title field]] of the thing being marked with ListAs. Meta title fields take a guess at an appropriate field to use if they are not explicitly marked and it will ultimately default to searching by a field called "Name" otherwise.
 
For example, if you have the MyCustomType above, it has a field called Name so the search bar that appears will use that. If it did not have a field called Name, it will also attempt to use fields like "Title" and similar if they exist, based on the rules of the [[Content_Type_Meta_Fields|meta title field]]. If you want some other field to be searched by, mark it as the meta title field instead:
 
<syntaxhighlight lang="csharp" line="1">
    [Meta("title")]
    public string Registration;
</syntaxhighlight>
 
With that, the admin UI will then search by this field called Registration.
 
===Explicit Lists===
 
There is a special consideration for listing though: by default, your ListAs field will be available to get included on anything. This is great for generic things like tags or categories, but the chances are you probably have something specific in mind. You can just ignore it of course, but the field will list on your admin panel and cause some unwanted clutter.
 
So, to clamp down where your ListAs field actually appears, use the explicit and implicit modes:
 
<syntaxhighlight lang="csharp" line="1">
    [ListAs("MyCustomTypes", Explicit=true)]
</syntaxhighlight>
 
Explicit means the field name must be explicitly used - it won't appear when the wildcard is used. You can, for example, do this for your includes array:
 
<syntaxhighlight lang="js" line="1">
['*', 'MyCustomTypes']
</syntaxhighlight>
 
The wildcard is what the admin panel uses, so an explicit include will no longer appear there. So, we now have an includable field that exists but appears nowhere by default. We can then make it appear on specific types with ImplicitFor:
 
<syntaxhighlight lang="csharp" line="1">
    [ListAs("MyCustomTypes", Explicit=true)]
    [ImplicitFor("MyCustomTypes", typeof(ParentTypeToListOn))]
</syntaxhighlight>
 
You can list as many ImplicitFor attributes as you need. The above means the following:
 
* This type will be includable with the name 'MyCustomTypes'
* This 'MyCustomTypes' include will not appear on wildcard inclusions
* But it will appear on wildcard inclusions specifically for the type called 'ParentTypeToListOn'
 
=On: Includes without the entity=


There are situations where you'd like a list of includable things but without the thing they're included on. For example, if you want a paginated list of members. Maybe it's also live. Ideally we'd want to make use of Loop for this as it has those features, and you can do so using the On filter:
There are situations where you'd like a list of includable things but without the thing they're included on. For example, if you want a paginated list of members. Maybe it's also live. Ideally we'd want to make use of Loop for this as it has those features, and you can do so using the On filter:
Line 126: Line 197:
The above loop will have the same results as having included UserPermits on MyCustomType with ID #1. This allows full access to Loop features on these specialised lists of things.
The above loop will have the same results as having included UserPermits on MyCustomType with ID #1. This allows full access to Loop features on these specialised lists of things.


==Permissions==
=Permissions=


Includes are a powerful way to get simple secondary and tertiary permissions - those are permissions where being able to see something relies on being able to see something else. For example, lets say you're permitted to see messages in a chat if you are a member of the chat.
Includes are a powerful way to get simple secondary and tertiary permissions - those are permissions where being able to see something relies on being able to see something else. For example, lets say you're permitted to see messages in a chat if you are a member of the chat.


We can structure this such that the user has a userPermit in the chat, with chat list/load access restricted by the custom rule <code>HasUserPermit()</code>, and we can restrict message list/load with the custom rule <code>IsIncluded()</code>. This means a message can only be displayed if you arrived at it via an include, and you can only load the chat (and thus include its messages) if you have a permit. The above section about "On" is particularly useful if you want, for example, a live loop of messages which are include restricted.
We can structure this such that the user has a userPermit in the chat, with chat list/load access restricted by the custom rule <code>HasUserPermit()</code>, and we can restrict message list/load with the custom rule <code>IsIncluded()</code>. This means a message can only be displayed if you arrived at it via an include, and you can only load the chat (and thus include its messages) if you have a permit. Because includes can be deeply nested, so too can these permissions. The above section about "On" is particularly useful if you want, for example, a live loop of messages which are include restricted.


One important aspect of this is being able to create a new message with the connection to the parent chat immediately. To allow for this, "On" is supported during create calls as well.
One important aspect of this is being able to create a new message with the connection to the parent chat immediately. To allow for this, "On" is supported during create calls as well.
Line 153: Line 224:
     );
     );
</syntaxhighlight>
</syntaxhighlight>
= Functional Includes =
It can be useful to run some code on a particular object whilst its JSON is generating. Whilst you can do this with the List and Load event handlers, it is generally recommended to use a functional include instead as it has less overhead. Their downside is they are global: they operate on any type. If you would like the primary URL of a piece of content, it is available as a functional include called "primaryUrl". In other words, including primaryUrl on a piece of content will provide a field called primaryUrl which would contain the page URL that content of that type is the primary content on.
As with all includes, functional includes can also be nested. You can, for example include "tags,tags.primaryUrl" - to get a list of tags on this object, plus a primaryUrl for each tag - assuming some sort of /tags/{tag.slug} page exists.
See also: PrimaryUrlValueGenerator for an example in your code.
= Dynamic Includes =
Dynamic includes are a mechanism where you can include something of a database specified type. This is useful in, for example, featured content scenario's where the thing being featured could be a product, a business or really any other kind of content. In other words: the database is specifying what the content type is, and also the ID of the content too.
To use dynamic includes, declare the field name containing the type as well as the field name containing the ID. Note that the content type field must be a string, and the ID field must be a ulong.
<syntaxhighlight lang="csharp">
// Including "item" when obtaining this featured content would also obtain the referenced type
// provided permission checks pass too of course.
[HasVirtualField("Item", "ContentType", "ContentId")]
public partial class FeaturedContent : VersionedContent<uint>
{
/// <summary>
/// The content type name, e.g. "Product".
/// </summary>
public string ContentType;
/// <summary>
/// The content ID. e.g. A product ID.
/// </summary>
public ulong ContentId;
}
</syntaxhighlight>
Dynamic includes should however be considered a feature of last resort. If you know that something will be one of two types, then it is suggested to include lists of both of those types instead. As anything can be included this way, the UI is less structured as it doesn't necessarily know for certain what it will have to display.
Globally available includes can be nested on dynamic includes too. For example, "item,item.tags" will obtain the dynamic item plus any tags on it. As functional includes such as primaryUrl are global, they are are available as well. If you use a wildcard, it will use all available implicit global includes.

Latest revision as of 12:44, 15 June 2023

Includes are a mechanism in SocialStack which allow you to request additional content when obtaining an entity (or a list of entities) from the API. For example, you have a loop of things and would like to also display the name of the user who created each one. Includes are not case sensitive.

Frontend Usage

Typically includes are used from either Loop or Content:

    import Loop from 'UI/Loop';
    
    ...
    
    <Loop over='page/list' includes={['creatorUser']}>
        {page => {
            return <div>
                The page "{page.title}" was created by {page.creatorUser.username}
            </div>;
        }}
    </Loop>
    import Content from 'UI/Content';
    
    ...
    
    <Content type='page' id={1} includes={['creatorUser']}>
        {page => {
            if(!page){
                // Just an example. The page might also not exist. See <Content> usage for more info.
                return 'Loading..';
            }
            
            return <div>
                The page "{page.title}" was created by {page.creatorUser.username}
            </div>;
        }}
    </Content>

If you aren't in the React render scope, you can specify includes via a webRequest as well:


    webRequest('page/1', null, {includes: ['creatorUser']});

In some rare cases you may need a <Form> response to include something custom. By default edit and create include * with their response, but they don't know about any nested includes that you may need. Form has a requestOpts prop which gives you the ability to set raw values on the webRequest that it makes, allowing us to essentially do the same as above but with a form instead:

    <Form action='page' requestOpts={{includes: ['*', 'creatorUser.tags']}}>
    ..

API Usage

The frontend is actually just adding any includes on as a GET parameter, like so. If you have more than one, they are comma separated:

https://www.example.com/v1/page/1?includes=creatorUser

Nested Includes

You will often have situations where you want to include things on the included objects as well. This is done with a dot:

    ['creatorUser', 'creatorUser.tags']

Nesting has no depth limit - you can use 3+ layers deep if you need to. Because you're spelling out each depth, there is no risk of recursion or cyclical behaviour either - you can include the creatorUser of a creatorUser, even if they are the same object. (And yes, Users do have a creatorUser as well - it can be used to identify if a user account was created by an admin for example).


Wildcards

You can wildcard include to show all available current level includes, and this is available nested as well:

    ['*', 'creatorUser.*']

Note that the wildcard is only supported as the last field (*.creatorUser is not available for example, as not everything necessarily supports the named field you want).

Wildcards also do not show explicit includes (ones where you must explicitly list the field name).

Making something includable

Includable things are either singular objects (like the creator user - a piece of content only ever has 1 of those), or a list of things.

Single objects

To make a single object, you'll need to create a "virtual field" on your C# type:

    [HasVirtualField("OtherTypeName", typeof(OtherTypeName), "OtherTypeNameId")]
    public partial class MyCustomType : VersionedContent<uint>
    {
        public uint OtherTypeNameId;
    }

You can declare as many virtual fields as you need, but essentially you define:

  • The virtual fields name (this is the text which goes inside the includes array)
  • The type of thing that will be included. This must always be a Content type.
  • The name of the field where the ID for your included thing will come from.

For example, this is how creatorUser is actually defined:

[HasVirtualField("CreatorUser", typeof(User), "UserId")]

"Create an includable field called CreatorUser, which will be a User type, and the ID of the user to include comes from the UserId field".

On the admin panel

A virtual field declared in this way will cause a convenient dropdown selector to appear for the ID field on the admin panel (UI/ContentSelect). Sometimes though your included type may have thousands or millions of rows, so a dropdown is not appropriate and instead a search first mechanism is required. You can turn on search mode like so:

[Data("titleField", "Name")] /* The field name to use as the title in the results list (or in the dropdown) */
[Data("search", "true")] /* Use search mode instead of a dropdown of all */
public uint OtherTypeNameId;

Lists

To include a list of things on a type (for example, a list of answers on a poll), you'll need to mark the C# type as listable:

    [ListAs("MyCustomTypes")]
    [HasVirtualField("OtherTypeName", typeof(OtherTypeName), "OtherTypeNameId")]
    public partial class MyCustomType : VersionedContent<uint>
    {
        public uint OtherTypeNameId;
        
        public string Name;
    }


You can use multiple ListAs declarations. Users is a good example of a use case for this - there is a list of permitted users on other types, but also you might want a list of members etc.

On the admin panel

By default a ListAs field will appear as a content selector on the admin panel. This content selector uses a searchbar to find the content you're after and it will default to using the meta title field of the thing being marked with ListAs. Meta title fields take a guess at an appropriate field to use if they are not explicitly marked and it will ultimately default to searching by a field called "Name" otherwise.

For example, if you have the MyCustomType above, it has a field called Name so the search bar that appears will use that. If it did not have a field called Name, it will also attempt to use fields like "Title" and similar if they exist, based on the rules of the meta title field. If you want some other field to be searched by, mark it as the meta title field instead:

    [Meta("title")]
    public string Registration;

With that, the admin UI will then search by this field called Registration.

Explicit Lists

There is a special consideration for listing though: by default, your ListAs field will be available to get included on anything. This is great for generic things like tags or categories, but the chances are you probably have something specific in mind. You can just ignore it of course, but the field will list on your admin panel and cause some unwanted clutter.

So, to clamp down where your ListAs field actually appears, use the explicit and implicit modes:

    [ListAs("MyCustomTypes", Explicit=true)]

Explicit means the field name must be explicitly used - it won't appear when the wildcard is used. You can, for example, do this for your includes array:

['*', 'MyCustomTypes']

The wildcard is what the admin panel uses, so an explicit include will no longer appear there. So, we now have an includable field that exists but appears nowhere by default. We can then make it appear on specific types with ImplicitFor:

    [ListAs("MyCustomTypes", Explicit=true)]
    [ImplicitFor("MyCustomTypes", typeof(ParentTypeToListOn))]

You can list as many ImplicitFor attributes as you need. The above means the following:

  • This type will be includable with the name 'MyCustomTypes'
  • This 'MyCustomTypes' include will not appear on wildcard inclusions
  • But it will appear on wildcard inclusions specifically for the type called 'ParentTypeToListOn'

On: Includes without the entity

There are situations where you'd like a list of includable things but without the thing they're included on. For example, if you want a paginated list of members. Maybe it's also live. Ideally we'd want to make use of Loop for this as it has those features, and you can do so using the On filter:

    <Loop over='user' filter={{On: {type: 'MyCustomType', id: 1, map: 'UserPermits'}}}>
        ...
    </Loop>

The above loop will have the same results as having included UserPermits on MyCustomType with ID #1. This allows full access to Loop features on these specialised lists of things.

Permissions

Includes are a powerful way to get simple secondary and tertiary permissions - those are permissions where being able to see something relies on being able to see something else. For example, lets say you're permitted to see messages in a chat if you are a member of the chat.

We can structure this such that the user has a userPermit in the chat, with chat list/load access restricted by the custom rule HasUserPermit(), and we can restrict message list/load with the custom rule IsIncluded(). This means a message can only be displayed if you arrived at it via an include, and you can only load the chat (and thus include its messages) if you have a permit. Because includes can be deeply nested, so too can these permissions. The above section about "On" is particularly useful if you want, for example, a live loop of messages which are include restricted.

One important aspect of this is being able to create a new message with the connection to the parent chat immediately. To allow for this, "On" is supported during create calls as well.


    import webRequest from 'UI/Functions/WebRequest';
    
    ...
    
    // also from <Form>'s onValues
    webRequest(
        "chatmessage",
        {
            text: 'Hello! This is a chat message',
            on: {
                type: 'Chat',
                id: 1,
                map: 'Messages' // If ChatMessage only has one ListAs, i.e. ListAs("Messages"), declaring map here is optional
            }
        }
    );

Functional Includes

It can be useful to run some code on a particular object whilst its JSON is generating. Whilst you can do this with the List and Load event handlers, it is generally recommended to use a functional include instead as it has less overhead. Their downside is they are global: they operate on any type. If you would like the primary URL of a piece of content, it is available as a functional include called "primaryUrl". In other words, including primaryUrl on a piece of content will provide a field called primaryUrl which would contain the page URL that content of that type is the primary content on.

As with all includes, functional includes can also be nested. You can, for example include "tags,tags.primaryUrl" - to get a list of tags on this object, plus a primaryUrl for each tag - assuming some sort of /tags/{tag.slug} page exists.

See also: PrimaryUrlValueGenerator for an example in your code.

Dynamic Includes

Dynamic includes are a mechanism where you can include something of a database specified type. This is useful in, for example, featured content scenario's where the thing being featured could be a product, a business or really any other kind of content. In other words: the database is specifying what the content type is, and also the ID of the content too.

To use dynamic includes, declare the field name containing the type as well as the field name containing the ID. Note that the content type field must be a string, and the ID field must be a ulong.

// Including "item" when obtaining this featured content would also obtain the referenced type
// provided permission checks pass too of course.
[HasVirtualField("Item", "ContentType", "ContentId")]
public partial class FeaturedContent : VersionedContent<uint>
{
	/// <summary>
	/// The content type name, e.g. "Product".
	/// </summary>
	public string ContentType;

	/// <summary>
	/// The content ID. e.g. A product ID.
	/// </summary>
	public ulong ContentId;
}

Dynamic includes should however be considered a feature of last resort. If you know that something will be one of two types, then it is suggested to include lists of both of those types instead. As anything can be included this way, the UI is less structured as it doesn't necessarily know for certain what it will have to display.

Globally available includes can be nested on dynamic includes too. For example, "item,item.tags" will obtain the dynamic item plus any tags on it. As functional includes such as primaryUrl are global, they are are available as well. If you use a wildcard, it will use all available implicit global includes.