Skip to content

Instantly share code, notes, and snippets.

@holyjak
Last active August 2, 2023 16:45
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save holyjak/207ed33c9ee7e003b9a779fa47c32d91 to your computer and use it in GitHub Desktop.
Save holyjak/207ed33c9ee7e003b9a779fa47c32d91 to your computer and use it in GitHub Desktop.
Fulcro RAD - assorted notes

Unsorted notes on Fulcro RAD.

Reports

Displaying to-one :ref attributes

Tony advises:

There are two primary ways to do this. If it is a true to one relationship, then you can simply make a resolver from person ID to address ID in pathom and then you can just include address things as columns. The other option, which works for any cardinality, is to use the report option ro/column-EQL and write a join that pulls the information you want to format in the column, and then supply a column formatter to do the formatting of the nested data.

Let's assume you have a Person entity with the ref attribute person/home-address with the target address/id. You make a resolver to map :person/id to :address/id (possibly running SQL like select p.addr_id from person p where p.id=?). Then you can ise ro/columns [person/name ... address/city address/country] (assuming these attributes are defined and have :address/id among their ao/identities). Pathom can now navigate from person/id -> address/id -> address/city etc.

In the other case you would extend the address attribute with a join for the properties you want and add a formatter to display them: (defattr home-address :person/home-address :ref {... ro/column-EQL {:person/home-address [:address/city :address/country]}, ro/column-formatter (fn [_report {:address/keys [city country]} _ _] (str city " " country))}).

Displaying to-many :ref attributes in a report column as a value or a sub-report

You have a report for an entity that has a to-many :ref attribute. Normally the value of that will be a vector of idents of the referenced entities. But you want to display something nicer - for example some other prop(s) from the child entity or a complete sub-report with these children. How to do that?

For example let's assume that you have a parent report of all Person which has the to-many ref attribute :person/children (with ao/target :person/id) and want to display also the names of each person's children, i.e. :person/name.

1) As a stringification of some display-friendly child entity props (leveraging a virtual attribute)

The simplest solution is perhaps to create a virtual (not backed by a DB column) attribute with a resolver that returns a string of the concatenated children props that you want to show in the report. Something like:

(defattr children-names :person/children-names :string
  {ao/target     :person/id
   ao/pc-input   #{:person/id}
   ao/pc-output  [:person/children-names]
   ao/pc-resolve (fn [{:keys [parser] :as env} {parent-id :person/id}]
                   ;; the env contains already the instance of the Pathom parser
                   #?(:clj
                      {:person/children-names
                      (-> (parser env [{[:person/id parent-id] [{:person/children [:person/name]}]}])
                          (get [:person/id parent-id])
                          :person/children
                          (->>
                            (map :person/name)
                            (str/join ", ")))}))})

2) As a custom component, e.g. a sub-report (leveraging a custom BodyItem)

You can also display the children entities in a sub report. For that, you want to take full control of the row component by providing your own ro/BodyItem. This must both specify the complete row query (so you can include {:person/children (comp/get-query YourChildComponent)) - and RAD will thus ignore :row-query-inclusion and will not add form links for you - and render the body, where it can delegate to rad.report/render-row. Inside, you can display the children entities in whatever way you want, e.g. leveraging existing report functionality.

Look at what defsc-report is doing and how it is generating the default *-Row BodyItem component.

Tony, 1/2021:

Customize the row [via ro/BodyItem SomeComponent, see this example of custom BodyItem] but you may have to switch render styles (the default is table, and list is supported). If you use table, then your element (because of DOM, not Fulcro) must be a table row. If you use list it is more general. NOTE: This is all dependent on the internals of the particular render plugin you use. Write the render body of the defsc-report yourself. All the logic is there, there are helpers in report.cljc for triggering all the logic. Render it exactly the way you want. For a special repeated pattern, it might be work making your own render plugin that you can then reuse across your app.

3. Leveraging row query inclusions with a custom column renderer

Do not include the person/children attribute in the report (as it would override the row query inclusion we are about to add). Add something like ro/row-query-inclusion [{:person/children (comp/get-query YourChildComponent)].

Now you have the data but no column to display them. One solution is to specify a custom ::rad.report/row-style ::my-style and having registered a component for it under :com.fulcrologic.rad/controls ::rad.report/row-style->row-layout in all-control. The component would be like the default TableRowLayout but also display the children.

Example of customizing UISM

From Tony:

(defn load-statistics [env]
  (let [{:project/keys [id]} (report/current-control-parameters env)]
    (uism/load env :local/stats TheStats {:params {:project/id id}
                                          :target (conj (uism/actor->ident env :actor/report) :local/stats)})))

;; Use the normal report state machine, but change the handler that post-processes running the report,
;; and adds an additional load that gets the stats
(defstatemachine report-with-stats
  (let [base-machine report/report-machine
        handler-path [::uism/states :state/loading ::uism/events :event/loaded ::uism/handler]
        base-handler (get-in base-machine handler-path)]
    (assoc-in base-machine handler-path
      (fn [env]
        (-> env
          (base-handler)
          (load-statistics))))))
          
;; and then I just added :local/stats to the ro/query inclusion after setting the machine:
;; ...
ro/machine             report-with-stats
ro/query-inclusions    [{:local/stats (comp/get-query TheStats)}]

And now those stats load when the report finishes loading. Just scan through the state machine definition. UISM is order-independent within a handler, so it's pretty easy just to call the existing handler and then patch in your logic.

Incrementally loaded report

See https://github.com/fulcrologic/fulcro-rad/blob/develop/src/main/com/fulcrologic/rad/state_machines/incrementally_loaded_report.cljc and its options. Also see the similar, newer Server-side pagination UISM below.

Server-side pagination

See https://github.com/fulcrologic/fulcro-rad/blob/develop/src/main/com/fulcrologic/rad/state_machines/server_paginated_report.cljc (use it instead of the default report uism) and its server-paginated-report-options

in order to do server side pagination you have to make it possible to say, in a database-specific manner: “OFFSET 100 LIMIT 10”. It depends on the DB tech, in SQL this is making sure you’re using an index with OFFSET/LIMIT, and in Datomic this is index-pull => this is why Fulcro RAD has no default, db-agnostic solution for this

Consider: You want 5 cols in a report. Which ones are sortable? For every sortable column, is there a secondary sort order? For EACH of those combinations, you have to create an index, and you have to communicate between the client and server as to which of them you decided to support. Do you auto-generate a cross product of every possible column combination as indexes in advance?

what you end up wanting on top of all of this is filtering, which means it gets more expensive. In Datomic index-pull works fine, but if you want to filter on top of that it is a post-step and it makes finding the total potentially expensive. The next-offset is also about filtering. If you asked for next page of size 10, and you get that, the filtering might have caused the index scan to walk 150 things, so your next offset actually grows by 150 instead of just 10. So, in that state machine the assumption is we know we have a lot of results, and we can get a page of them with relative ease, but we need the client to track where we last left off scanning for efficiency (beyond just a page number…we need the real index offset) in SQL this is kind of hidden for you, but unless you’re really careful you can really kill your database with a combo of pagination and filtering done on the server.

Wisdom from the Slack

Reports and including sub-entities

Tony, 3/2022, answering a question about RAD reports querying for an entity and a sub-entity:

Crossing relations in a report is not magically solved by any technology…there are too many different things you might want to be doing at a given time. [..] every time you cross a reference then there can be any number of things you might want to do: Filter, Sort, Aggregate and to make matters worse, these decisions compound for every additional reference you cross. Throw in some polymophism (that edge, say :feed/items, can point to an image or a comment) and you now have a need to parameterize the decisions around the parameters of handing the edge. So this is why RAD doesn’t really even try to make a stab at handling edges in reports “for you”. The sheer number and complexity of options would be very hard to get right, and just as hard to understand/use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment