Structr

Expert Topics

Migration Guide

This chapter covers breaking changes and migration steps when upgrading between major Structr versions.

Important: Always create a full backup before upgrading Structr.

Migrating to Structr 7.x

Version 7 changes the signatures of the outbound HTTP functions. All optional arguments moved into a
single trailing options object, so a call no longer has to pass positional nulls to reach the last one,
and an option that is not supported by a function is rejected instead of being silently ignored.

Outbound HTTP Function Signatures

Function Signature
$.GET url [, contentType [, options ]]
$.HEAD url [, options ]
$.POST url, body [, contentType [, options ]]
$.PUT url, body [, contentType [, options ]]
$.PATCH url, body [, contentType [, options ]]
$.DELETE url [, body [, contentType [, options ]]]
$.FETCH url, method [, body [, contentType [, options ]]]
$.POST_multi_part url, partsMap [, options ]
// Old (6.x): charset, then credentials, as positional arguments
$.POST(url, body, 'application/json', 'UTF-8', 'user', 'secret');

// New (7.x): the charset belongs to the content type, everything else is an option
$.POST(url, body, 'application/json; charset=UTF-8', { username: 'user', password: 'secret' });

username, password, preemptive, headers, timeout, redirects and validateCertificates are
accepted by every function. selector, binaryResponse and parseResponse are accepted
only where they mean something; passing one elsewhere is an error naming the function and the key.

Most calls can be rewritten automatically. See application.migration.mode and the migrate maintenance
command, which report every call that still uses the old form and can rewrite the unambiguous ones.

Binary Responses Are Streams

$.POST previously returned the response body as a byte array when the content type was
application/octet-stream. Streaming the response is now requested by the binaryResponse option, the
same key $.GET uses, and the result is an InputStream for both. The stream removes the 2 GB limit
that the array imposed.

// Old (6.x): a byte array
const bytes = $.POST(url, body, 'application/octet-stream').body;

// New (7.x): a stream, which can be passed straight to setContent()
const stream = $.POST(url, body, 'application/octet-stream', { binaryResponse: true }).body;
$.get_or_create('File', { name: 'result.bin' }).setContent(stream);

This change cannot be detected automatically. The call text is unchanged, so the migration report
will not flag it: any code that indexes into the result, measures its length or stores it as a byte array
has to be adjusted by hand.

Failed Requests No Longer Throw

A request that reached the server has always returned its status. A request that did NOT reach it - an
unreachable host, a refused connection, a TLS failure, a timeout - used to throw a FrameworkException
with status 422. It now returns a response with status: 0 and an error field describing the failure.

// Old (6.x): only a try/catch could survive an unreachable service, and StructrScript could not
try { $.GET(url); } catch (e) { /* ... */ }

// New (7.x): check the status, which StructrScript can do as well
let response = $.GET(url);
if (response.status === 0) { $.log('unreachable: ' + response.error); }

Two related corrections come with it. An error response WITHOUT a body, such as a bodyless 404 or a
204, used to be reported as a thrown 422 because the empty body was read unchecked; it now returns the
status the server sent. And a rejected URL used to be re-wrapped as that same 422; its own status now
survives.

Calls that cannot be made at all still throw: a malformed URL, a scheme other than http or https, a URL
without a host, an internal network address, or a URL outside the outgoing whitelist.

Code that relies on a catch to detect an unreachable service will no longer enter it. Check
status === 0 instead.

Binary Request Bodies

A File passed as the request body is now sent as its content rather than as its string representation,
so an upload no longer needs base64 or a multipart envelope. See the Filesystem chapter.

PDF Generation Without wkhtmltopdf

The pdf() function and the PdfServlet no longer call the external wkhtmltopdf binary. The document
is now produced inside the JVM, so nothing has to be installed on the server and PDF generation works
from a cron job or a doPrivileged context as well as from a request.

Three things change for existing applications.

pdf() returns a File, not a string. The old return value was the document’s bytes carried in an
ISO-8859-1 string, which every caller then wrote into a file.

// Old (6.x)
${ set_content(create('File', 'name', 'report.pdf'), pdf('report'), 'ISO-8859-1') }

// New (7.x)
${ pdf('report', 'report.pdf') }

The wkhtmltopdf parameters are gone. The second parameter is now the name of the generated file.
Passing an argument string raises an error rather than being ignored, because a silently dropped
--header-html produces a document that looks right and is missing its header.

A detail object still comes from the path, but parameters are now an argument. The old function
built a URL, so everything travelled in one string. The page path still carries the object the page
renders, which it reads as current, while request parameters are passed as an object instead of a
query string. A query string in the path is refused rather than ignored.

// Old (6.x)
${ pdf(concat('invoice/', order.id, '?lang=de')) }

// New (7.x)
${ pdf(concat('invoice/', order.id), 'invoice.pdf', { lang: 'de' }) }

The page reads those as ${request.lang}, and it sees exactly the parameters passed and no others, so
the same call produces the same document from a page, a cron job or doPrivileged.

Headers, footers and page numbers move into the print stylesheet. They used to be separate Structr
pages fetched over HTTP. They are now page level CSS, and no second page is involved:

@page {
    size: A4;
    margin: 25mm 18mm;
    @top-left     { content: element(docheader); }
    @bottom-right { content: "Page " counter(page) " of " counter(pages); }
}
#docheader { position: running(docheader); }

The renderer implements CSS 2.1 plus paged media. Flexbox, grid, custom properties and JavaScript have
no effect on paper, so a page whose screen layout relies on them needs a print stylesheet. Declarations
the renderer cannot use are written to the server log rather than dropped in silence, so the log tells
you what a document lost.

Images, stylesheets and fonts are read from the Structr filesystem by path, under the permissions of the
user the page is rendered as. External URLs are not fetched unless pdf.resources.external.allowed is
enabled.

Migrating to Structr 6.x

Version 6 introduces several breaking changes that require manual migration from 5.x.

Global Schema Methods

Global schema methods have been simplified. The globalSchemaMethods namespace no longer exists – functions can now be called directly from the root context.

StructrScript / JavaScript:

// Old (5.x)
$.globalSchemaMethods.foo()

// New (6.x)
$.foo()

REST API:

# Old (5.x)
/structr/rest/maintenance/globalSchemaMethods/foo

# New (6.x)
/structr/rest/foo

Action required: Search your codebase for /maintenance/globalSchemaMethods and $.globalSchemaMethods and update all occurrences.

REST API Query Parameter Change

The _loose parameter has been renamed to _inexact.

# Old (5.x)
/structr/rest/foo?_loose=1

# New (6.x)
/structr/rest/foo?_inexact=1

REST API Response Structure

The response body from $.GET and $.POST requests is now accessible via the body property.

// Old (5.x)
JSON.parse($.GET(url))

// New (6.x)
JSON.parse($.GET(url).body)

Schema Inheritance

The extendsClass property on schema nodes has been replaced with inheritedTraits.

// Old (5.x)
eq('Location', get(first(find('SchemaNode', 'name', request.type)), 'extendsClass').name)

// New (6.x)
contains(first(find('SchemaNode', 'name', request.type)).inheritedTraits, 'Location')

JavaScript Function Return Behavior

JavaScript functions now return their result directly by default.

Option 1: Restore old behavior globally:

application.scripting.js.wrapinmainfunction = true

Option 2: Remove unnecessary return statements from functions.

JavaScript Strict Mode

Identifiers must be declared before use. Assigning to undeclared variables throws a ReferenceError.

// ❌ Not allowed
foo = 1;
for (foo of array) {}

// ✅ Correct
let foo = 1;
for (let foo of array) {}

Custom Indices

Custom indices are dropped during the upgrade to 6.0.

Action required: Recreate all custom indices manually after upgrading.

Upload Servlet Changes

Aspect 5.x Behavior 6.x Behavior
Default upload folder Root or configurable /._structr_uploads
Empty folder setting Allowed Enforced non-empty
uploadFolderPath Unrestricted Authenticated users only

Repeaters: No REST Queries

REST queries are no longer allowed for repeaters. Migrate them to function queries or flows.

Migration Checklist for 6.x


Migrating to Structr 4.x

All versions starting with the 4.0 release include breaking changes which require migration of applications built with Structr versions prior to 4.0 (1.x, 2.x and 3.x).

GraalVM Migration

With version 4.0, the required Java Runtime changed from standard JVMs (OpenJDK, Oracle JDK) to GraalVM. GraalVM brings full ECMAScript support, better performance, and polyglot scripting capabilities.

Installing GraalVM

Each Structr version supports the stable GraalVM version current at the time of release. The following example shows installation on Linux:

wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.1.0/graalvm-ce-java11-linux-amd64-22.1.0.tar.gz
tar xvzf graalvm-ce-java11-linux-amd64-22.1.0.tar.gz
sudo mv graalvm-ce-java11-22.1.0 /usr/lib/jvm
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/graalvm-ce-java11-22.1.0/bin/java 2210
sudo update-alternatives --auto java

Migration of Script Expressions

Predicates in find() and search()

All predicates in find() and search() expressions need the $.predicate prefix. The easiest way to migrate is to export the application using deployment export and search all files for these predicates:

$.and
$.or
$.not
$.equals
$.contains
$.empty
$.range
$.within_distance
$.sort
$.page

Examples:

// Old (3.x)
$.find('File', 'size', $.range(null, 100), $.page(1, 10));

// New (4.x+)
$.find('File', 'size', $.predicate.range(null, 100), $.predicate.page(1, 10));
// Old (3.x)
$.find('User', $.sort('createdDate'));

// New (4.x+)
$.find('User', $.predicate.sort('createdDate'));

Some predicates also exist as regular functions ($.sort(), $.empty()) or keywords ($.page). When used outside of find(), they don’t need changes:

// No change needed - sort() used outside find()
$.sort($.find('User'), 'createdDate');

Resource Access Permissions

Resource Permissions (formerly “Resource Access Grants”) have been made more flexible. Rights management now also applies to permission nodes themselves, requiring users to have read access to the permission object to use it.

Manual Migration
  1. Log in as admin
  2. Navigate to Security → Resource Permissions
  3. Enable “Show only used grants”
  4. Migrate permissions:

For many permissions, enable “Show visibility flags in Resource Permissions table” in Dashboard → UI Settings.

Semi-automatic Migration via Deployment

When importing a deployment export from a pre-4.0 version into 4.x+, Structr runs automatic migration using this heuristic:

Scripting Considerations

Date Comparisons

Use the getTime() function when comparing dates to avoid issues with GraalVM ProxyDate entities:

{
    return $.me.createdDate.getTime() <= $.now.getTime();
}
Conditional Chaining Limitation

Conditional chaining on ProxyObjects with function members can cause errors:

{
    const obj = {
        method1: () => "works"
    };

    // Works
    obj.method1?.();

    // Works, call doesn't get executed
    obj.method2?.();

    const proxyObject = $.retrieve('passedObject');

    // Does NOT work - throws unsupported message exception
    proxyObject.myMethod?.();
}

REST Request Parameters

Starting with 4.0, REST request parameters must be prefixed with underscore to prevent name collisions with property names:

# Old
/structr/rest/Project?page=1&pageSize=10&sort=name

# New
/structr/rest/Project?_page=1&_pageSize=10&_sort=name

Full list of affected parameters:

Parameter Parameter Parameter
page pageSize sort
order loose locale
latlon location state
house country postalCode
city street distance
outputNestingDepth debugLoggingEnabled forceResultCount
disableSoftLimit parallelizeJsonOutput batchSize

Legacy mode can be enabled with application.legacy.requestparameters.enabled = true but is discouraged for new projects.

Neo4j Upgrade

Neo4j 4.x is recommended for Structr 4.x, though Neo4j 3.5 is still supported. If upgrading Neo4j, consult the Neo4j changelog.

Cypher Parameter Syntax

The old parameter syntax {param} was deprecated in Neo4j 3.0 and removed in Neo4j 4.0. Use $param instead. For compatibility, you can prefix queries with CYPHER 3.5.

Database Name Configuration

If migrating from Neo4j versions prior to 4, the default database may be named graph.db instead of neo4j. Configure the database name in structr.conf:

YOUR_DB_NAME.database.connection.url = bolt://localhost:7687
YOUR_DB_NAME.database.connection.name = YOUR_DB_NAME
YOUR_DB_NAME.database.connection.password = your_neo4j_password
YOUR_DB_NAME.database.connection.databasename = graph.db
YOUR_DB_NAME.database.driver = org.structr.bolt.BoltDatabaseService

Migration Checklist for 4.x

Creating Widgets

This chapter covers how to write your own Widgets: static building blocks, page templates, data-driven components and render templates. It builds on the Widgets & Components chapter, which explains how Widgets and Shared Components are used, and on the template expressions described in the Dynamic Content chapter.

The widget system consists of three subsystems that are separated from each other by design. The component type system controls where a Widget can be inserted and how Widgets nest. The replacement mechanism controls what happens when you replace an element with a Widget or wrap it in one. Data-driven components render their content from a data source and a ComponentConfiguration, using a small set of rendering functions. A good way to learn all three is to import the default widget set and read the source of its Widgets alongside this chapter.

How a Widget Becomes Page Elements

A Widget is an object in the database with an HTML source code field. When you insert it, Structr parses the source and creates the corresponding elements in the page tree. In the simplest case the source is plain HTML, and the resulting elements are independent copies with no connection to the Widget. Inserting the same Widget twice creates two separate sets of elements.

The source can define Shared Components with <structr:shared-template> tags. Structr creates each Shared Component only once. When the Widget is inserted again, Structr finds the existing Shared Component by name and references it instead of creating a duplicate. This is the mechanism behind widget libraries, where every Widget produces the same Shared Components on every page.

The source can contain template variables in square brackets, such as [variableName]. Structr treats a square bracket expression as a variable only when a key with the same name exists in the Widget configuration. All other square bracket expressions are left unchanged. When a Widget has at least one recognized variable, the Insert Widget dialog asks for the values before insertion.

The source can also contain deployment annotations, the HTML comments that the application export writes in front of elements to preserve Structr attributes. Set processDeploymentInfo to true in the Widget configuration to have them interpreted on insertion. This is useful when you build the Widget source from an exported page.

The Widget Editor

The plus button in the Widgets flyout creates a new Widget and opens the editor. The editor has five tabs: Source, Configuration, Description, Options and Help.

Source

The Source tab holds the HTML source of the Widget, including Structr expressions. The easiest way to obtain it is to build the elements in a page and export their markup with the _edit=1 URL parameter, which renders the page with all expressions and Structr attributes intact instead of evaluating them:

  1. Build the Widget in a page called myWidgetPage
  2. Open http://localhost:8082/myWidgetPage?_edit=1
  3. View and copy the source code of the page
  4. Paste it into the Source tab
Setting Structr Attributes in the Source

Widget source is plain HTML, but Structr elements have attributes that do not exist in HTML, such as componentType or contentType. To set them during insertion, use HTML attributes with the prefix data-structr-meta-. Take the Structr attribute name in camelCase, convert it to kebab-case and add the prefix. Structr strips the prefix on import, converts the name back to camelCase and sets the attribute on the element.

HTML attribute Structr attribute
data-structr-meta-name name
data-structr-meta-component-type componentType
data-structr-meta-content-type contentType
data-structr-meta-dimensions dimensions
data-structr-meta-show-conditions showConditions
data-structr-meta-hide-conditions hideConditions
data-structr-meta-item-type itemType
data-structr-meta-repeater-type repeaterType
data-structr-meta-data-key dataKey
data-structr-meta-function-query functionQuery
data-structr-meta-triggered-actions The event action mapping of the element, as a JSON object

The mechanism works for any Structr attribute, not only the ones listed here.

Configuration

The Configuration tab defines the template variables of the Widget as a JSON object. Each key is a variable name that matches a square bracket expression in the source, and its value describes the form field that the Insert Widget dialog shows for it. The configuration must be valid JSON.

{
    "configSwitch": {
        "position": 2,
        "default": "This is the default text"
    },
    "selectArray": {
        "position": 3,
        "type": "select",
        "options": [
            "choice_one",
            "choice_two",
            "choice_three"
        ],
        "default": "choice_two"
    },
    "selectObject": {
        "position": 1,
        "type": "select",
        "options": {
            "choice_one": "First choice",
            "choice_two": "Second choice",
            "choice_three": "Third choice"
        },
        "default": "choice_two"
    },
    "processDeploymentInfo": true
}

Two kinds of top-level keys are reserved. processDeploymentInfo is the boolean described above. Keys that begin with an underscore are meta entries that are not shown as form fields. The process module uses _autoVisibilityMapping in this way to bind a Widget to a process step on insertion.

Each variable supports these attributes:

Attribute Applies to Description
title all The title displayed in the dialog. If omitted, the variable name is used.
placeholder input, textarea The placeholder text displayed when the field is empty. If omitted, the title is used.
default all The default value. For input and textarea it is prefilled, for select it is preselected.
position all A number for sorting the fields. Variables without a position appear after those with a position, in natural key order.
help all Help text displayed when hovering over the information icon.
comment page, boolean Help text for the typed fields that share their rendering with the render template settings.
type all The form field type. See the table below. Defaults to input.
options select An array of strings or an object with value-label pairs. Arrays render as simple options. Objects use the key as value and the object value as displayed text.
dynamicOptionsFunction select A function body that populates the options. The function receives a callback parameter that must be called with the options. If present, options is ignored.
rows textarea The number of rows. Defaults to 5.

The type attribute selects the form field:

Type Form field
input A text input. This is the default.
textarea A multi-line text input.
select A dropdown with the given options.
boolean A checkbox.
page A dropdown listing the pages of the application.
schema-type A dropdown listing the custom types of the application.
schema-property A dropdown listing the properties of the type selected in the schema-type field of the same dialog.
schema-method A dropdown listing the methods of the type selected in the schema-type field of the same dialog.
datasource The data source menu. Structr replaces the variable with the resolved data source name. See Authoring Data-Driven Components below.
process A dropdown listing the BPMN processes of the application. Requires the process module.
user-task A dropdown listing the user tasks of the process selected in the process field of the same dialog. Requires the process module.

The types schema-property, schema-method and user-task read their options from a sibling field, so they only work together with a schema-type or process field in the same configuration. The Action Button of the default set combines schema-type and schema-method to let the user pick the method the button calls.

Description

The Description tab holds text that is shown at the top of the Insert Widget dialog and on the tile in the Create Page dialog for page templates. It can contain HTML and typically explains what the Widget does and what the configuration fields mean.

Options

The Options tab holds the settings that control where and how the Widget is offered:

Setting Description
Selectors CSS selectors that decide under which elements the Widget is suggested. [type='container'] suggests the Widget inside any element whose component type is container. Several selectors are combined with commas. For render templates, the selectors hold the compatible data types instead.
Is Page Template Shows the Widget in the Create Page dialog.
Is Render Template Marks the Widget as a render template for fields of data-driven components.
Is Exclusive In Parent Allows only one instance per parent element. Once an element created by the Widget exists under a parent, the Widget is no longer suggested there.
Short Description A one-line description of the Widget.
Component Type The component type that Structr assigns to the root element created by the Widget.
Dimensions The dimensions value that Structr assigns to the root element created by the Widget.

The flyout context menu entry “Advanced” opens the full properties dialog of the Widget, which additionally holds the paths of the thumbnail and the icon shown in the flyout and in the Create Page dialog.

The Component Type System

The component type system controls where Widgets can be inserted and how they nest. It uses two attributes on the page elements. componentType gives an element a role in the nesting hierarchy, and dimensions describes the shape of data the element is designed to display.

Component Types and Selectors

The values of componentType are defined freely by the author of a widget set. Structr does not impose a fixed set. The selectors of a Widget are standard CSS selectors that Structr matches against a synthetic element built from the prospective parent: its tag name, CSS classes, id and name, plus the component type of the parent as the attribute type. If the parent is a linked copy of a Shared Component, the component type of the Shared Component is used. [type='container'] therefore suggests a Widget inside every element whose component type is container, and table suggests it inside every table element.

The default widget set uses these component types:

componentType Description
canvas The main content area of a page. Accepts the top-level components.
container A structural container for layout and grouping.
form A container with submit semantics. Accepts inputs directly.
content A leaf element that displays content. Cannot have children.
input A leaf element for user input. Cannot have children.
action A leaf element that triggers an action, such as a button.
table, list, gallery Data-driven components that render a collection. Inserted into a canvas.
table-cell The cells of a data-driven table. Accept input, action and text widgets, so a table can be edited in place.

There are two ways to assign a component type to the elements a Widget creates. The Component Type and Dimensions settings in the Options tab are applied to the root element on insertion, or to the Shared Component behind it. Alternatively, data-structr-meta-component-type on a <structr:shared-template> or any other element in the source sets the type for that element. The second way is needed when a Widget defines several elements with different roles, for example a card whose content area is a container while the card itself is not.

Dimensions

The dimensions attribute counts the axes along which the content of a component repeats:

dimensions Meaning Content structure Examples in the default set
0 A single object Fixed layout with named areas, no repetition Card, Edit Form
1 A list A repeating sequence of identical areas List, Gallery, Accordion
2 A table Areas arranged in rows and columns Table

For data-driven components, the value also constrains the data source. A component with dimensions 0 expects a single object, dimensions 1 a collection, and dimensions 2 a collection with several properties per item. Structr checks this whenever the configuration of a component changes, see Data Source Compatibility below. Leaf elements and forms have no dimensions value, and a missing value means that the data source is not constrained.

Exclusive Widgets

A Widget with “Is Exclusive In Parent” is suggested only while no element created by it exists under the prospective parent. Structr recognizes such elements by name, so the root element of an exclusive Widget must carry the Widget’s name, either as the name of its Shared Component or via data-structr-meta-name. Use this for elements that make sense only once per container, such as a page heading or a pagination bar.

Replacing and Wrapping

The context menu offers two operations that exchange an existing element for a Widget. “Replace Element With” removes the selected element and inserts the Widget in its place. “Wrap Element In” inserts the Widget in place of the selected element and moves the element into one of the Widget’s content areas. For both, Structr suggests only Widgets whose selectors match the parent of the selected element, using the same matching as for insertion. For wrapping, the Widget must additionally contain an element whose component type matches the selectors of the Widget that created the selected element, so that the element has a valid place to go.

When an element is replaced, Structr transfers matching content areas and repeaters from the old element to the new Widget. Content areas are matched by itemType, repeaters by repeaterType. Both attributes are set freely by the widget author, matching is by string equality, and each value may occur only once in a Widget. The Card with heading of the default set marks its heading with data-structr-meta-item-type="label" and its body with data-structr-meta-item-type="content", so replacing one card with another keeps heading and body in place.

itemType and repeaterType belong exclusively to the replacement mechanism. They are distinct from componentType and from the ComponentConfiguration described below. Data-driven components are not subject to content transfer. When one data-driven component is replaced with another, only the data source binding is carried over, since there is no manually placed content to preserve.

Building a Widget Library

A widget library is a set of Widgets designed to work together through the component type system. Each Widget defines one or more Shared Components with componentType, dimensions and selectors that enforce the nesting rules. When a Widget from the library is inserted, Structr creates its Shared Components if they do not exist yet, or reuses them if they do. A library can contain static Widgets and data-driven components.

Defining Widgets

Use <structr:shared-template> tags for the Shared Component definitions and <structr:template> tags to reference them. Container Widgets call render(children) to mark the content area where child elements go. Leaf Widgets omit the call. The following source defines four Shared Components and assembles them into a page:

<!-- Shared Component definitions -->

<structr:shared-template name="Main Page Template"
    data-structr-meta-content-type="text/html">
    <html>
        <head>
            <title>${page.name}</title>
        </head>
        <body>
            ${render(children)}
        </body>
    </html>
</structr:shared-template>

<structr:shared-template name="Main Content"
    data-structr-meta-component-type="canvas"
    data-structr-meta-content-type="text/html">
    <main>${render(children)}</main>
</structr:shared-template>

<structr:shared-template name="Panel"
    data-structr-meta-component-type="container"
    data-structr-meta-dimensions="0"
    data-structr-meta-content-type="text/html">
    <div class="panel panel-content">
        ${render(children)}
    </div>
</structr:shared-template>

<structr:shared-template name="Paragraph"
    data-structr-meta-component-type="content"
    data-structr-meta-content-type="text/html">
    <p>Text</p>
</structr:shared-template>

<!-- References: assemble the page structure -->

<structr:template src="Main Page Template">
    <structr:template src="Main Content">
        <structr:template src="Panel">
            <structr:template src="Paragraph"></structr:template>
        </structr:template>
    </structr:template>
</structr:template>

The definitions create the Shared Components, the references assemble them: Main Page Template contains Main Content, which contains a Panel with a Paragraph. Inserting the Widget again reuses the existing Shared Components. With “Is Page Template” set, this Widget appears in the Create Page dialog, and every page created from it shares the same Main Page Template.

Multi-Area Widgets

Some container Widgets provide more than one content area. The Card with heading of the default set renders the card frame in its Shared Component and places two elements inside it through the referencing <structr:template>: a heading with data-structr-meta-item-type="label" and a container with data-structr-meta-item-type="content" and data-structr-meta-component-type="container". The itemType values name the areas for the replacement mechanism, and the component type of the content area decides which Widgets are suggested inside it. The Widget owns its internal structure and exposes the areas for the user to fill.

Organizing the Library

Use the treePath attribute to group Widgets into categories in the flyout and in the Suggested Widgets submenu. The path is slash-separated, must begin with a slash, and may contain spaces. Widgets without a path appear under “Uncategorized”. The default set uses these categories:

/Layout
/Components
/Component Parts
/Form Elements
/Text
/Actions
/Render Templates
/Page Templates

Render templates and page templates get categories of their own because they are not inserted directly.

Using the Library

Once the Widgets carry component types, dimensions and selectors, the library integrates into the page building workflow. A page created from the page template starts with a canvas that accepts containers and components. Right-clicking the canvas suggests exactly those, right-clicking inside a container suggests content, inputs and actions. The component type system guides the user through a valid page structure without knowledge of the rules.

Authoring Data-Driven Components

A Template element becomes a data-driven component when a ComponentConfiguration is attached to it. This happens when the Widget source contains a <structr:template> with a config attribute. The attribute holds the initial values of the configuration as a JSON-like object. The Table of the default set uses:

<structr:template src="Table" config="{ dataSource: '[dataSource]', reload: 'partial' }"></structr:template>

The keys correspond to the properties of the ComponentConfiguration: dataSource, displayMode (output or input), role (none, controller or subscriber), selectionChannel, reload (partial, page or others), labels, pageSize, columns and transform. The Edit Form presets displayMode: 'input', role: 'subscriber', labels: true so that the user only has to pick the channel.

The [dataSource] variable is filled from the Insert Widget dialog. The Widget declares it in its configuration with the type datasource:

{
    "dataSource": {
        "title": "Data Source",
        "type": "datasource"
    }
}

The dialog then shows the data source menu. When the user picks an existing type or channel, the variable is replaced with the data source name, for example node:Project or channel:current. When the user creates a new type, Structr creates the schema type and its properties, fills the field set of the component with the new attributes and, if requested, generates example data. When the user creates a script-based or query-based data source, Structr creates the empty node and binds the component to it by id.

Keywords in Component Templates

Inside the Shared Component or template of a data-driven component, these keywords are available:

component refers to the closest data-driven component above the current element and exposes its state and helper methods: pagination(), page(), pageCount, filterControls() and isEditable, which is true in input mode and serves as a show condition for a save button. From outside the component, getComponent('Name') returns the component with the given name.

dataSource refers to the data source of the component. dataSource.selectedValue is the record that a subscriber received via its channel and is only available when the data source is a channel. The Edit Form uses it as the id expression of its update action and in empty(dataSource.selectedValue) to hide the save button while nothing is selected. dataSource.currentValue is the record of the current iteration inside renderEach(). The Delete Button uses dataSource.currentValue.id in a table row. dataSource.dataType returns the name of the type the data source delivers.

adapter refers to the data adapter of the component. adapter.dataType returns the type name and is used by the default forms in their button labels.

Rendering Functions

Three functions render the configured fields. Each takes a tag specification as its first argument, a small CSS selector consisting of a tag name with optional classes and an id, such as th.sw-table-header. The rendered wrapper receives the col-span-N class for the width of the field, or col-span-6 when no width is set, so the six-column grid works inside every component.

renderEach(tags) iterates over the paginated, filtered and sorted collection of the data source and renders the configured fields for each record. The argument names two tags separated by a space, the first for each record and the second for each field. The Table uses renderEach('tr td.sw-table-cell'), the List uses renderEach('li.sw-list-item'), the Accordion uses renderEach('details summary'). Each record wrapper receives the attributes that make the record selectable when the component is a controller.

renderFields(tag [, slot]) renders the configured fields for a single record, applying each field’s render template in output mode or its edit template in input mode. The Edit Form calls renderFields('div') inside a grid grid-cols-6 container so that the field widths take effect.

renderLabels(tag [, slot]) renders the labels of the configured fields without values. The Table calls renderLabels('th.sw-table-header') in its header row. The rendered labels carry the attributes that make a column sortable. Clicking a label sorts by the field’s sort key and toggles the direction, and the active label receives the CSS class ascending or descending.

Slots

A slot is a named position in a component template. A render template can assign its fields to slots, and renderFields('div', 'media') then renders only the fields of the media slot. renderLabels() accepts the slot argument in the same way. Slots are rarely needed. Without a slot argument all configured fields are rendered in order, which works for tables, lists and forms. Slots become useful when a template has fixed structural positions, for example a card where image, title and action must appear in specific places regardless of field order.

Pagination and Filtering

The component method pagination() returns the HTML attributes that wire a button to the pagination state. Its first argument names the kind of button: prev, next, first and last lead to the neighbouring pages and to the first and last page, window takes an offset relative to the current page as second argument and produces the numbered buttons, and ellipsis takes low or high and marks the placeholder buttons that appear when the first or last page is far away. page() takes the same arguments and returns the page number the button leads to. pageCount returns the total number of pages. The pagination bar of the Table reads:

<div class="flex flex-row p-4 gap-4 justify-between">
    <button ${component.pagination('prev')}>Prev</button>
    <div class="flex gap-4">
        <button ${component.pagination('first')}>1</button>
        <button ${component.pagination('ellipsis', 'low')}>…</button>
        <button ${component.pagination('window', -2)}>${component.page('window', -2)}</button>
        <button ${component.pagination('window', -1)}>${component.page('window', -1)}</button>
        <button ${component.pagination('window',  0)}>${component.page('window',  0)}</button>
        <button ${component.pagination('window',  1)}>${component.page('window',  1)}</button>
        <button ${component.pagination('window',  2)}>${component.page('window',  2)}</button>
        <button ${component.pagination('ellipsis', 'high')}>…</button>
        <button ${component.pagination('last')}>${component.pageCount}</button>
    </div>
    <button ${component.pagination('next')}>Next</button>
</div>

Buttons that lead outside the valid range receive the hidden attribute, buttons without a valid target are disabled, and the button for the current page carries data-current-page. The same markup is shipped as the standalone “Pagination” Widget in the Component Parts category, which can be inserted into any container inside a component. The number of numbered buttons is controlled by the paginationWindowSize property of the ComponentConfiguration, default 5, which has no field in the dialog and is set in the Advanced properties.

filterControls() returns the attributes that wire a text input to the filter state of the component:

<input type="text" placeholder="Filter.." ${component.filterControls()} />

Typing performs a contains-search across all fields with “Include in filter” enabled, resets the pagination to the first page and stores the term in the URL. Escape clears the filter.

Component State and Reload Cascades

A data-driven component holds two kinds of state. Design-time configuration, that is data source, page size, reload behaviour, display mode and field configuration, lives in the ComponentConfiguration and does not change during user interaction. Runtime state, that is the selected record, current page, sort key and filter text, lives in URL parameters and changes as the user interacts. Because runtime state is in the URL, it survives navigation and refresh.

When several components on a page share channels, a selection must trigger reloads in dependency order, otherwise a subscriber could render before its upstream channel has settled. Structr computes a dependency graph from the data source and channel relationships of all components on the page at render time and writes the reload order as an attribute on the active element. The frontend script reads it, resets the affected URL parameters in order and triggers the partial reloads. The data-channel attribute on a component’s root marks it as a listener for the named channels, and a component with reload behaviour others omits its own name so that it is not reloaded by its own actions.

Data Source Compatibility

When the data source, the transform or the expected type of a component changes, Structr checks the combination against the dimensions of the component and rejects it with an error if it does not fit. A transform can only be applied to a data source that delivers a single object, because a property cannot be navigated on a collection. A collection cannot be bound to a component with dimensions 0. When the transform points to a collection property, the result counts as a collection. Components without a dimensions value accept any data source. The check runs on every modification of the ComponentConfiguration, including deployment import, so a widget set with wrong dimensions fails early.

Data Source Names

The dataSource property holds a name that Structr resolves at render time. node:<Type> delivers all instances of a type, node:<uuid> a user-defined script-based or query-based data source by id, channel:<name> the record selected on a channel, parent the record that a surrounding repeater or component provides under the data key parent, and root-folders the root folders of the file system. The data source menu in the dialogs produces exactly these names.

Authoring Render Templates

A render template is a Widget with “Is Render Template” set. Structr instantiates it on demand: the first time a component needs the template, Structr expands the Widget source into a Shared Component in the hidden document, and reuses that Shared Component afterwards. Users never insert render templates.

Structr finds the instantiated Shared Component by the name of the render template, so the root element of the source must carry data-structr-meta-name with the Widget’s name. Without it, Structr creates a new instance on every render.

The selectors of a render template hold the data types it can display, for example date, string, boolean, node, enum or custom. The template dropdown of a field offers only templates whose selectors contain the field’s data type.

Inside the template, value holds the field value and field gives access to the field metadata: field.label, field.propertyName, field.required, field.options for enum and relationship fields, field.multiple, field.rows and field.config for the parameters described next. The textfield template of the default set renders:

<input class="sw-input" name="${field.propertyName}" placeholder="${field.label}"
    required="${is(field.required, 'required')}" type="text" value="${value}"
    data-structr-meta-name="textfield" />

A render template can declare parameters in its Configuration tab. The format differs from the template variables of other Widgets: each parameter has a label, an optional type (page or boolean, otherwise a text input), optional options, an optional comment and an optional defaultValue. The parameters appear in the Render Template Settings of every field that uses the template, their values are stored per field and read via field.config. The formatted-date template declares:

{
    "dateFormat": {
        "label": "Date Format (ISO 8601)",
        "defaultValue": "dd.MM.yyyy"
    }
}

and renders ${dateFormat(value, field.config.dateFormat)}. The related-link template declares a detailPage parameter of type page and a labelProperty parameter and renders a link to the detail page of the related record with the chosen property as link text.

Process-Bound Widgets

With the process module installed, a Widget can bind the component it creates to a user task of a BPMN process. The configuration uses the process and user-task field types so that the user picks the task on insertion, and a _autoVisibilityMapping meta entry whose values reference those fields in square brackets, for example "boundProcess": "[process]". On insertion, Structr resolves the references and creates a visibility mapping that shows the component only while the task is active. The binding mode of the ComponentConfiguration is then processBound, the data source is fixed to the current channel, and the subject type of the process appears as the read-only Expected Type. The Process Subject Form of the default set is an example. Processes and their pages are covered in the BPMN Process Control chapter.

Prefetching

Structr’s Neo4j Bolt driver includes an automatic prefetching system that optimizes graph traversals by reducing the number of individual database queries. When your application accesses relationships on many nodes of the same type, prefetching detects this pattern and loads the relevant subgraph in a single bulk query, caching the results in memory for the duration of the transaction.

This is similar in concept to the N+1 query problem in relational databases, where iterating over a list of objects and accessing a related object on each one results in one additional query per item. Prefetching solves this by recognizing repetitive query patterns at runtime and replacing them with a single, broader query.

How It Works

Prefetching operates at three levels: automatic detection and learning, manual triggering via scripting, and self-optimization through query combining.

Automatic Prefetching

During a transaction, Structr monitors every relationship query that passes through the Bolt driver. Each query is recorded in a histogram that tracks the Cypher statement pattern, the source node type, the relationship type, the direction, and a counter of how often it has been executed.

When the counter for a given pattern exceeds a configurable threshold (default: 100 executions within a single transaction), the system checks whether activating prefetching for that pattern is safe. It runs a count() query to estimate the result size. If the result count is below the maximum allowed (default: 50,000), the pattern is registered for prefetching. If it exceeds the limit, the pattern is blacklisted instead, preventing it from being considered again.

The learned patterns are associated with a “prefetch hint” - an identifier set by higher-level application code that describes the context of the current operation, such as rendering a specific page or processing a REST request. On subsequent transactions with the same hint, all learned patterns are executed immediately at the start, populating the cache before application code begins accessing relationships.

Patterns that take longer than the maximum allowed duration (default: 1,000 ms) are also blacklisted automatically.

Manual Prefetching

You can trigger prefetching explicitly from StructrScript or JavaScript using the prefetch() function. This is useful when you know in advance which part of the graph your code will traverse.

StructrScript:

${prefetch('(:Customer)-[]->(:Task)', merge('HAS_TASK', 'ASSIGNED_TO'))}

JavaScript:

$.prefetch('(:Customer)-[]->(:Task)', ['HAS_TASK', 'ASSIGNED_TO']);

The first argument is a Cypher path pattern describing the subgraph to load. The second argument is a list of relationship type keys that should be considered prefetched after the query completes. This tells the caching layer that it does not need to make additional database queries for these relationship types on the affected nodes.

Manual Prefetching with prefetch2()

The prefetch2() function provides more control over prefetching by allowing you to write a custom Cypher query that returns explicit node and relationship collections, and by separating outgoing and incoming relationship keys.

JavaScript:

$.prefetch2(
    'MATCH (n:Customer)-[r:HAS_TASK]->(m:Task) RETURN collect(n) + collect(m) AS nodes, collect(r) AS rels',
    ['Customer/all/OUTGOING/HAS_TASK'],
    ['Task/all/INCOMING/HAS_TASK']
);

StructrScript:

${prefetch2(
    'MATCH (n:Customer)-[r:HAS_TASK]->(m:Task) RETURN collect(n) + collect(m) AS nodes, collect(r) AS rels',
    merge('Customer/all/OUTGOING/HAS_TASK'),
    merge('Task/all/INCOMING/HAS_TASK')
)}

The arguments are:

  1. A Cypher query that returns two collections: nodes (all nodes in the subgraph) and rels (all relationships). Unlike prefetch(), which expects a path pattern, this gives you full control over the query structure.
  2. A list of outgoing relationship keys that should be marked as prefetched on the source nodes.
  3. A list of incoming relationship keys that should be marked as prefetched on the target nodes.
  4. An optional ID parameter that is passed to the Cypher query as $id. This allows you to scope the prefetch to a specific starting node.

The key format for relationship keys follows the pattern Type/all/DIRECTION/RELATIONSHIP_TYPE, for example Customer/all/OUTGOING/HAS_TASK.

The difference between the two functions is that prefetch() works with path patterns and treats all relationship keys symmetrically (both outgoing and incoming get the same keys), while prefetch2() lets you write arbitrary Cypher queries and assign different keys to outgoing and incoming sides. Use prefetch2() when you need fine-grained control over what gets cached, or when your subgraph structure does not fit a simple path pattern.

Query Optimization

When a transaction closes, Structr analyzes the collected prefetching patterns and attempts to combine them to reduce the number of queries on future transactions.

Combining by type and direction. If multiple patterns share the same source node type and direction but differ in relationship type, they are merged into a single pattern using Neo4j’s pipe syntax. For example, two separate patterns like (n:Task)-[r:HAS_SUBTASK]-> and (n:Task)-[r:HAS_ASSIGNEE]-> would be merged into (n:Task)-[r:HAS_SUBTASK|HAS_ASSIGNEE]->.

Combining by inheritance. If two patterns differ only in their source node type and those types share a common base type in the schema, the patterns are merged using the common ancestor. This further reduces the number of prefetch queries needed.

Caching Mechanism

When a prefetch query executes, it runs a MATCH p = (pattern) RETURN p query against Neo4j that returns complete paths. The driver iterates over each path segment and populates two layers of cache:

The transaction-level cache stores NodeWrapper and RelationshipWrapper objects keyed by their Neo4j internal IDs. Any subsequent access to these entities within the same transaction returns the cached wrapper without hitting the database.

Each NodeWrapper maintains its own relationship cache organized as a tree structure. Prefetched relationships are inserted into this cache, and the node records which relationship keys have been prefetched. When application code later calls getRelationships() on a node, the system checks this cache first. If data is present, it is returned directly. If a key is marked as prefetched but no relationships exist for it, an empty result is returned immediately. This is an important optimization, because it avoids a database round-trip to confirm that no relationships exist.

Any write operation (creating or deleting a node or relationship) within the transaction invalidates the prefetch caches, forcing subsequent reads to go to the database again. This ensures data consistency within the transaction.

Configuration

Prefetching behavior is controlled by four settings in the “Prefetching” section of the database configuration.

Setting Key Default Description
Prefetching Threshold database.prefetching.threshold 100 The number of times an identical query pattern must execute within a single transaction before automatic prefetching is activated for that pattern.
Max Duration database.prefetching.maxduration 1000 Maximum time in milliseconds that a prefetch query may take. Patterns that exceed this duration are blacklisted and will not be prefetched again.
Max Count database.prefetching.maxcount 50,000 Maximum number of results a prefetch pattern may return. Patterns exceeding this count are blacklisted to avoid loading excessively large subgraphs into memory.
Cost Ratio database.prefetching.costratio 100 How many entities a prefetch query may load per lookup that it saves in the transaction. A prefetch that loads more than that is blacklisted for the request it was learned for, because loading the graph costs more than the lookups it replaces.

These settings can be adjusted in the Configuration Interface or directly in structr.conf.

Debugging

To monitor prefetching activity, enable Cypher debug logging by setting log.cypher.debug to true in your configuration. When enabled, the system logs:

This information helps you understand whether prefetching is working effectively for your application’s access patterns and whether any of the thresholds need adjustment.

When Prefetching Helps

Prefetching is most effective in scenarios where your application iterates over a collection of nodes and accesses the same type of relationship on each one. Typical examples include rendering a list view where each item displays related data, processing a batch of records that each reference linked entities, or traversing a tree structure where every node has children of the same type.

If your application’s access patterns are highly varied – accessing different relationship types on different node types in an unpredictable order – the automatic detection may not activate because no single pattern reaches the threshold. In these cases, manual prefetching with the prefetch() function gives you explicit control.

Related Topics