The initial target of this project is to build a headless CMS in and for Elixir that can be used either as an embeddable library in your application or as a stand-alone service.
Why headless? Long term there may well be a case for building out the website generation part and integrating that more. But there is a lot of bang for the buck in terms of time and a very wide use-case for headless CMS:es. Most Elixir enthusiasts are developers. Targeting headless first means we constrain the scope significantly.
We are targeting Phoenix and Ecto which shouldn't be particularly controversial. We are likely not committing to a single database but would at least want to support Postgres and Sqlite which likely means MySQL will work fine too.
Look at almost any popular CMS and they will have runtime (not compile-time) editing of "content types" such as "pages" or "posts". You create them, you can manage what fields they have, all without redeploying code. This presents a bit of a challenge with how Ecto does compile time schema generation. Fortunately Ecto supports schemaless everything (queries, likely using dynamic fragments a lot, sometimes raw queries and schemaless changeset). All the tools around Repo, Query and Migrator can be called at runtime with some alternative data structures. This library should make some opinionated decisions around building "entities", our conception of content types (they aren't just for content O_o). The library should make working with Ecto in this manner as convenient as possible.
This is the necessary foundation for enabling a runtime CMS system on top of Ecto while retaining most of the niceties of Ecto and making programming against it familiar to anyone familiar with Ecto.
It needs to provide:
Picking up where the Ecto Entity library leaves off this should provide the best practices for working with entities. It should provide a library API that helps you deal with persisting definitions and running migrations. Much like Ecto is useful without SQL, Ecto Entity may be useful without this Storage system. But as with Ecto SQL, for the 90% use case you want both.
The thing you think about when using a CMS. This library should provide a UI that maps to the underlying Ecto Entity and Ecto Entity Storage libraries. It implements a number of Routes much like Live Dashboard or Oban Web and you decide your need for auth, the path, etc. via the Phoenix router and Plug pipelines.
I don't know how feasible building an Absinthe GraphQL API is at runtime or whether we can nicely model these dynamic entities.
A REST API plus webhooks would be straight-forward but a GraphQL one would probably be cooler and give. more convenience out of the box (self-documenting, discoverable).
Database side constraints such as being required or unique can only be set for the initial set of fields added via migrations to an entity. If we would allow them after that point you could have a situation where the migration fails to apply because the existing data does not conform to the new constraint.
Required could arguably be solved by requiring that a default value is also set and have that default applied to all existing data. We should explore whether this is worthwhile.
Always includes an id
field that is a UUID string field. This avoids a number of common problems with entity data and auto-incrementing integer IDs. Especially around importing and exporting data between environments.
Field (example)
Removes a field from active duty. It will not be part of the field definition but not actually removed from the database.
Given that the migration sets are part of a definition and the real source of truth. This is what the user puts into the UI and what we use to produce everything else.
These are used directly by Ecto for schemaless changesets and not needing to derive them from migrations every time we load the definition as well as the readability advantage for the definition will both contribute to a better experience working with the definitions. It has a potential drawback of needing to update two places if editing definitions by hand, similarly to working with Ecto normally and needing to update both migration and schema.
Generated from the migrations. Every entity should provide a few changesets by default and like with fields I think it is helpful for the definition to provide them pre-chewed to some extent. This could arguably be generated at parse-time though.
For field names, source name and types you can use strings or atoms interchangably. For options we will likely use Keywords so they need to be atoms. They will all be strings in the definition.
Ecto has this weird thing where it only allows building the shape of a query at compile time (excludes the possibility to create SQL injection holes) when using Ecto.Query. This works great when coding against this library with known field names and stuff. For our library though we will likely need to use the escape hatch of raw queries to some extent to provide filtering.
Building on top of EctoEntity we can create some more involved abstractions in EctoEntityStorage with powerful instrumentation for making an extensible CMS without tying the events and hooks to the UI. If you want a CLI-based CMS this would be a very useful foundation.
Tailwind CSS and Theme:able library styles? utility classes might cause trouble
UI should restrict filtering to only filter/order on fields that have indexes