Modules

From SocialStack
Revision as of 12:20, 1 December 2022 by Luke (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Modules in SocialStack are the main way to create small, portable, easy to test units of code. The best way to create a new one is via the generate command, but you can also install public ones. Importantly a module should be self-contained as much as possible with the smallest dependency footprint.

Making C# code modular

As C# is a statically typed language, it is inherently more rigid than languages like Javascript on the frontend. However, partial classes are very useful for this end goal of making extensible units of code. For this reason, every class outputted by the generate command is partial.

Why are partial classes useful? Let's take the User type for example. Modules want to extend this one all the time - a 2 factor authentication plugin may want to store some data in the user objects, modules for adding names and addresses and so on. Because the installed User class is partial, you can add fields to it without editing the original file. By adding fields to the User type, any User reference you end up with - either via the API, admin panel, events etc - will then have that field on it. Because the database storage mechanism is also generic, it will get automatically stored too. Therefore, adding a field to the User type is as simple as adding a file like this:

namespace Api.Users;

public partial class User{
    public string MyCustomField;
}

Secondly, we also have a very fast and flexible events system inside the API. When a user object is created, an event is triggered (specifically that is Events.User.BeforeCreate and AfterCreate), which then any module can subscribe to. Just like with the content type itself, common events are fully automatic but you can also create your own.

Event listeners should be added once at startup. This means you'll either put them in service constructors, or in special EventListener classes which are constructed as a singleton for you.

[EventListener]
public class MyEventListener {
    
    public MyEventListener(){

        Events.User.BeforeCreate.AddEventListener(async (Context context, User user) => {
            if(user == null){
                return null;
            }
            
            // can await something here if needed!
            // Because we're in a beforeCreate, we can edit the object including populating custom fields or validating them.
            // If we have a validation failure, throw a PublicException.
            // throw new PublicException("Incorrect date of birth", "dob/invalid");
            return user;
        });
    }
}

Publishing a module

Modules can be shared to the world via the github repository. You'll need to create a new module entry on socialstack cloud first which should be the desired installable name and the path within the "Core" repository. Adding a new entry is currently non-public because of this socialstack cloud dependency but will be fully public in the future. You can however edit the socialstack command line tools to point at something other than socialstack cloud to have a completely custom repository/ a superset of installable modules.

Note that it's important to add the module entry first - that's because the commit handler specifically checks if committed changes are within known module paths and if so, then generates the new installable zip. If you create the module entry afterwards, just make a commit to any of the files in your modules path and it will then update for you.

No modules are silly modules! If you'd like to share your code between projects you're always invited to publish it.

Installing a published module

The socialstack command line tools have an install command that let you install a module from the github repository. Typically use the path to the module that you want, like so:

socialstack install Api/Pages
socialstack install UI/Alert
socialstack install Defaults

"Defaults" is the package which gets installed when a new project is generated.

Adding dependencies

Any published module can contain a package.json file which lists other modules to install when this module is being installed. It's based on the node.js package.json file and is structured like so:

{
    "dependencies": [
        "UI/Alert",
        "nuget:a-nuget-package"
    ]
}

You can either use the name of another module, an npm package or a nuget package. Installing npm packages this way is currently experimental.

Editing changes

When installing a module its files are copied into your project in a directory called ThirdParty. You can edit the files in there, but it's generally recommended to avoid doing so unless necessary. This is because when you want to upgrade your project, the changes will be something you may end up needing to merge.

Typically when making changes to a module that originated from the public repository, it's best to make the changes generic such that they can be contributed (by simply committing them to the github repo, or make a pull request there). For example if you're editing a UI module it's best to expose a React prop instead of adding your new project specific value in the source. If you're editing an API module, e.g. add a feature flag as a config option.

If you're making substantial changes - e.g. use of the thirdparty module was more just as a structure guideline rather than using it for its functionality - then it's best to either name it something new, or make it firstparty in your project by simply moving the directory.

Importing and referencing modules

Socialstack's module compiler tries to do as much of the plumbing for you here as possible. As a result, a module simply needs to exist to be compiled in the C# project, and it simply needs to exist in the UI tree to be importable on the frontend. A module located at:

UI/Source/ThirdParty/Alert

Can be imported like so:

import Alert from 'UI/Alert';

This is because it actively ignores the word "ThirdParty" as well as the root "Source" directory. The same is true for the Admin directory too:

Admin/Source/ThirdParty/Templates

import Templates from 'Admin/Templates';

The only difference is the admin modules are only available specifically on the pages of the admin panel to help minimise bloat.

Two modules with the same name

Because it ignores the word ThirdParty, it certainly can arise where two modules have the same apparent name. In the API directory this won't compile because C# requires unique class names. In UI directories though, the code will always favour the firstparty module. For example, let's say you make a copy of the Alert module but keep the original still in the project:

UI/Source/Alert and UI/Source/ThirdParty/Alert

If you then attempt to import it:

import Alert from 'UI/Alert';

The first party one will "win" every time. This is resolved on a file by file basis such that files in the ThirdParty copy but not in the first party one will still be usable.