Context module for managing templates and documents via Google Drive.
Google Drive is the single source of truth for file content. This module mirrors file metadata (name, google_doc_id, status, thumbnails, variables) to the local database for fast listing and audit tracking.
API layers
This module provides combined operations (Drive + DB). For direct access:
- Drive-only — Use
PhoenixKitDocumentCreator.GoogleDocsClientfor raw Google Drive/Docs API calls (create files, list folders, export PDF, move files) without touching the local database. - DB-only — Use
list_templates_from_db/0,list_documents_from_db/0,load_cached_thumbnails/1,persist_thumbnail/2for local DB queries. - Combined — Use
create_template/2,create_document/2,sync_from_drive/0,delete_template/2, etc. which coordinate between Drive and DB.
Summary
Functions
Applies a preset by UUID, returning its sections as plain maps.
Broadcast {:files_changed, self()} on the document_creator:files topic.
Compose N template sections into a single Google Doc, persisting the recipe.
Create a blank document in the documents folder. Returns {:ok, %{doc_id, name, url}}.
Create a document from a template by copying and filling variables.
Create a blank template in the templates folder. Returns {:ok, %{doc_id, name, url}}.
Move a document to the deleted/documents folder.
Permanently deletes a preset (hard delete — no trash in Stage 1).
Move a template to the deleted/templates folder.
Detect {{ variables }} in a Google Doc's text content.
Get the Google Drive URL for the documents folder.
Export a Google Doc to PDF. Returns {:ok, pdf_binary}.
Fetch thumbnails for a list of Drive files asynchronously.
Get the folder IDs (auto-discovers if not cached).
Fetches a preset by uuid, or nil.
Fetches a single template row by its Google Doc ID.
Returns the DB-cached variable definitions for a template as a list of
Variable.t() structs, without making any Drive API calls.
Returns the image variable slots defined in a template's Google Doc.
List documents from the local DB. Returns maps compatible with the LiveView.
Lookup the project's enabled languages, sorted by configured position.
Lists presets, optionally filtered by :scope_type and :scope_id.
Results are ordered by name ascending.
Lists published Template structs for a category, ordered by name.
List templates from the local DB. Returns maps compatible with the LiveView.
List trashed documents from the local DB.
List trashed templates from the local DB.
Load cached thumbnails from DB for a list of google_doc_ids.
Log a manual user action to the activity feed.
Move a file into the managed documents folder and classify it as a document.
Move a file into the managed templates folder and classify it as a template.
Persist a thumbnail data URI to the DB by google_doc_id.
Returns staleness info for a preset.
Batched preset_stale_info/1 for a list of presets.
The PubSub topic on which {:files_changed, self()} messages are
broadcast whenever a template or document DB record is mutated.
Returns the sections of a document as an ordered list of plain maps.
Re-discover folder IDs from Drive.
Register a Drive document that the caller has already created (or already knows about) into the local DB.
Register a Drive template that the caller has already created or knows about.
Rename a document: updates its name in Google Drive and in the local DB.
Resolves image value specs against template variable defs and media module.
Restore a trashed document back to the documents folder.
Restore a trashed template back to the templates folder.
Persists a named, reusable preset (template composition recipe).
Persist the file's current parent folder as its accepted location.
Splits variable_values into text values and image specs.
Sync local DB with Google Drive.
Get the Google Drive URL for the templates folder.
Like update_template_taxonomy/3 but for a Document.
Updates an existing preset from attrs.
Set the locale on a template, looked up by google_doc_id.
Sets (or clears) the category and type of a template identified by
google_doc_id. taxonomy is a map with optional :category_uuid
and :type_uuid keys (nil clears). Logs a
template.taxonomy_updated activity row.
Update the config map of a single variable on a template's variables jsonb.
Upsert a document record from a Google Drive file map.
Upsert a template record from a Google Drive file map.
Functions
@spec apply_preset(binary()) :: {:ok, [ %{ template_uuid: binary(), position: non_neg_integer(), variable_values: map(), image_params: map() } ]} | {:error, :not_found}
Applies a preset by UUID, returning its sections as plain maps.
Sections whose template_uuid no longer exists in the database are silently
dropped (a warning is logged listing the removed UUIDs). The remaining
sections are returned in position order.
NOTE: Deliberate deviation from spec line 110 — this function returns
{:ok, [map]} | {:error, :not_found} instead of the spec's bare [map].
This gives callers a clean error path for stale preset references from UI
state (e.g. a preset UUID that was deleted server-side). The spec should be
updated to match after implementation review.
@spec broadcast_files_changed() :: :ok
Broadcast {:files_changed, self()} on the document_creator:files topic.
Use this after a bulk register_existing_document/2 / register_existing_template/2
call that passed emit_pubsub: false, to trigger a single resync in any
connected admin LiveViews.
Silently no-ops if the PubSub system isn't available (e.g. background jobs or tests without a running PubSub registry).
@spec create_composed_document( [PhoenixKitDocumentCreator.Documents.Composer.section_input()], keyword() ) :: {:ok, PhoenixKitDocumentCreator.Schemas.Document.t()} | {:error, term()}
Compose N template sections into a single Google Doc, persisting the recipe.
Required opts: :created_by_uuid, :name. Optional: :separator (default :page_break).
Variable substitution is range-scoped per section: each section's variable_values
are applied only within the character range that section occupies in the composed doc.
Identical placeholder keys in different sections (e.g. {{name}} in section 0 and
section 1) resolve independently.
Create a blank document in the documents folder. Returns {:ok, %{doc_id, name, url}}.
Options
:actor_uuid— UUID of the user performing the action (for activity logging)
@spec create_document_from_template(String.t(), map(), keyword()) :: {:ok, map()} | {:error, term()}
Create a document from a template by copying and filling variables.
- Copies the template Google Doc into the target folder
- Replaces all
{{variable}}placeholders with values - Persists the document record with variable_values and template link
- Returns
{:ok, %{doc_id, url}}
Options
:name— document name (default"New Document"):actor_uuid— UUID of the user performing the action (activity log):parent_folder_id— Drive folder ID to copy into. Defaults to the managed documents folder. Supply this to place the new document in a subfolder (e.g.order-123/sub-4/) you manage yourself.:path— human-readable path string to store on the record. Only meaningful when:parent_folder_idis also supplied. If omitted when:parent_folder_idis given, the storedpathis left unset and the nextsync_from_drive/0fills it from the walker.
Create a blank template in the templates folder. Returns {:ok, %{doc_id, name, url}}.
Options
:actor_uuid— UUID of the user performing the action (for activity logging):language— locale code to tag the template with. Defaults to the project's primary language fromPhoenixKit.Modules.Languages. Passnilto leave it unset; pass an explicit code (e.g."et-EE") to override.
Move a document to the deleted/documents folder.
Options
:actor_uuid— UUID of the user performing the action (for activity logging)
@spec delete_preset(PhoenixKitDocumentCreator.Schemas.TemplatePreset.t()) :: {:ok, PhoenixKitDocumentCreator.Schemas.TemplatePreset.t()} | {:error, Ecto.Changeset.t()}
Permanently deletes a preset (hard delete — no trash in Stage 1).
Move a template to the deleted/templates folder.
Options
:actor_uuid— UUID of the user performing the action (for activity logging)
@spec detect_variables(String.t()) :: {:ok, %{ text: [String.t()], image: [%{name: String.t(), kind: :image | :image_list}] }} | {:error, term()}
Detect {{ variables }} in a Google Doc's text content.
@spec documents_folder_url() :: String.t() | nil
Get the Google Drive URL for the documents folder.
Export a Google Doc to PDF. Returns {:ok, pdf_binary}.
Options
:actor_uuid— UUID of the user performing the action (for activity logging):name— document name (for activity metadata)
Fetch thumbnails for a list of Drive files asynchronously.
Spawns a single supervised parent task under PhoenixKit.TaskSupervisor
that fans out via Task.async_stream/3 with a bounded max_concurrency
so opening a folder with hundreds of files doesn't fire hundreds of
simultaneous Drive requests. Each completion sends {:thumbnail_result, file_id, data_uri} back to caller_pid and persists the thumbnail to
the DB. The parent is restart: :temporary so it dies cleanly if the
caller LV closes mid-fetch — but in-flight persists still complete.
@spec get_folder_ids() :: map()
Get the folder IDs (auto-discovers if not cached).
@spec get_preset(binary()) :: PhoenixKitDocumentCreator.Schemas.TemplatePreset.t() | nil
Fetches a preset by uuid, or nil.
Fetches a single template row by its Google Doc ID.
Returns {:ok, %{"id" => ..., "name" => ...}} or {:error, :not_found}.
@spec get_template_variables_from_db(String.t()) :: [ PhoenixKitDocumentCreator.Variable.t() ]
Returns the DB-cached variable definitions for a template as a list of
Variable.t() structs, without making any Drive API calls.
Returns [] if the template has no cached variables.
@spec image_slots_for_template(UUIDv7.t()) :: {:ok, [%{name: String.t(), kind: :image | :image_list, config: map()}]} | {:error, :not_found | term()}
Returns the image variable slots defined in a template's Google Doc.
Fetches the current document text via the Google Docs client and extracts
all {{ image: name }} / {{ images: name }} tags, returning a list of
%{name: String.t(), kind: :image | :image_list, config: map()} maps sorted
by name.
The :config map is sourced from the saved variable in template.variables
(if present); otherwise it falls back to Variable.default_image_config(kind).
Returns {:error, :not_found} if no template exists for the given UUID.
@spec list_documents_from_db() :: [map()]
List documents from the local DB. Returns maps compatible with the LiveView.
Lookup the project's enabled languages, sorted by configured position.
Returns [%{code: "en-US", name: "English (United States)"}, ...] or
[] when core's PhoenixKit.Modules.Languages is disabled or the
settings table isn't reachable. Safe to call from LiveView mount —
failure is swallowed, never crashes the caller.
@spec list_presets(%{ optional(:scope_type) => String.t(), optional(:scope_id) => String.t() }) :: [ PhoenixKitDocumentCreator.Schemas.TemplatePreset.t() ]
Lists presets, optionally filtered by :scope_type and :scope_id.
Results are ordered by name ascending.
@spec list_templates_for_category(binary()) :: [ PhoenixKitDocumentCreator.Schemas.Template.t() ]
Lists published Template structs for a category, ordered by name.
Returns full schema structs (not file maps) so callers get variables,
status, and atom-key access. Used by the preset section editor.
@spec list_templates_from_db() :: [map()]
List templates from the local DB. Returns maps compatible with the LiveView.
@spec list_trashed_documents_from_db() :: [map()]
List trashed documents from the local DB.
@spec list_trashed_templates_from_db() :: [map()]
List trashed templates from the local DB.
Load cached thumbnails from DB for a list of google_doc_ids.
Log a manual user action to the activity feed.
Move a file into the managed documents folder and classify it as a document.
Options
:actor_uuid— UUID of the user performing the action (for activity logging)
Move a file into the managed templates folder and classify it as a template.
Options
:actor_uuid— UUID of the user performing the action (for activity logging)
Persist a thumbnail data URI to the DB by google_doc_id.
@spec preset_stale_info(PhoenixKitDocumentCreator.Schemas.TemplatePreset.t()) :: %{ broken_count: non_neg_integer(), broken_template_uuids: [binary()] }
Returns staleness info for a preset.
A section is "broken" when its template_uuid references a template that
no longer exists, or whose status is trashed or lost.
Returns %{broken_count: non_neg_integer(), broken_template_uuids: [binary()]}.
@spec preset_stale_info_map([PhoenixKitDocumentCreator.Schemas.TemplatePreset.t()]) :: %{ required(binary()) => %{ broken_count: non_neg_integer(), broken_template_uuids: [binary()] } }
Batched preset_stale_info/1 for a list of presets.
Runs a single Template query covering every referenced template uuid
and returns a map of preset.uuid => stale_info, avoiding the N+1 of
calling preset_stale_info/1 once per preset.
@spec pubsub_topic() :: String.t()
The PubSub topic on which {:files_changed, self()} messages are
broadcast whenever a template or document DB record is mutated.
Admin LiveViews subscribe to this topic in mount/3. Prefer calling
this helper over hard-coding the topic string so the two stay in sync.
@spec recipe_for(PhoenixKitDocumentCreator.Schemas.Document.t()) :: [ %{ template_uuid: binary() | nil, position: non_neg_integer(), variable_values: map(), image_params: map() } ]
Returns the sections of a document as an ordered list of plain maps.
Each map contains :template_uuid, :position, :variable_values, and
:image_params. The list is ordered by position ascending and represents
a point-in-time snapshot — it does not check whether templates still exist.
@spec refresh_folders() :: map()
Re-discover folder IDs from Drive.
@spec register_existing_document( map(), keyword() ) :: {:ok, PhoenixKitDocumentCreator.Schemas.Document.t()} | {:error, Ecto.Changeset.t() | term()}
Register a Drive document that the caller has already created (or already knows about) into the local DB.
Use this when your own wrapper code handles the Drive-side work (copy,
placement in a subfolder, variable substitution) and you just need the
file to appear in list_documents_from_db/0 and be classified correctly
by future sync_from_drive/0 runs.
Makes no Drive API calls. This is a pure DB upsert — garbage inputs
do not error, they self-correct on the next sync (the walker rewrites
path/folder_id, and files that are not actually in the managed tree
get classified :unfiled or :lost per the usual reconciliation rules).
attrs — map keyed by atoms or strings
| Key | Required | Notes |
|---|---|---|
google_doc_id | yes | Drive file ID |
name | yes | Display name |
template_uuid | no | UUID of the source template, if applicable |
variable_values | no | Map of variables substituted during generation |
folder_id | no | Actual Drive folder holding the file. Defaults to managed |
path | no | Human-readable path. Defaults to managed root path |
status | no | Defaults to "published" |
thumbnail | no | Optional data URI |
If :folder_id points outside the managed documents tree, the next
sync_from_drive/0 will classify the record as :unfiled and surface
the resolution popup in the admin UI.
opts
:actor_uuid— user UUID for the activity log:emit_pubsub— defaulttrue. Broadcasts:files_changedon thedocument_creator:filestopic so connected admin LiveViews re-sync. Bulk callers (e.g. a backfill script registering hundreds of rows) should passfalseand trigger one broadcast or sync at the end:Enum.each(rows, &Documents.register_existing_document(&1, emit_pubsub: false)) Documents.broadcast_files_changed()
@spec register_existing_template( map(), keyword() ) :: {:ok, PhoenixKitDocumentCreator.Schemas.Template.t()} | {:error, Ecto.Changeset.t() | term()}
Register a Drive template that the caller has already created or knows about.
Symmetric to register_existing_document/2 — see its documentation for the
attrs shape and options. Unlike documents, template registration does not
accept template_uuid or variable_values.
@spec rename_document(binary(), String.t()) :: {:ok, PhoenixKitDocumentCreator.Schemas.Document.t()} | {:error, term()}
Rename a document: updates its name in Google Drive and in the local DB.
Validates that new_name is non-blank (reuses Document.changeset/2's
length validation). Returns {:ok, %Document{}} on success or
{:error, term()} on failure (including {:error, :not_found} and
{:error, :blank_name}).
Resolves image value specs against template variable defs and media module.
Restore a trashed document back to the documents folder.
Restore a trashed template back to the templates folder.
@spec save_preset(map()) :: {:ok, PhoenixKitDocumentCreator.Schemas.TemplatePreset.t()} | {:error, Ecto.Changeset.t()}
Persists a named, reusable preset (template composition recipe).
Required attrs: :name, :created_by_uuid. Optional: :description,
:scope_type, :scope_id, :sections.
Persist the file's current parent folder as its accepted location.
Options
:actor_uuid— UUID of the user performing the action (for activity logging)
Splits variable_values into text values and image specs.
@spec sync_from_drive() :: :ok | {:error, :sync_failed}
Sync local DB with Google Drive.
Recursively walks both managed trees (templates and documents), upserts every
Google Doc found (including those nested in subfolders) with its actual
parent folder_id and human-readable path, then reconciles DB records
against the walk — records whose google_doc_id is missing from the walk
are re-classified via a per-file Drive API call as trashed, lost, or
unfiled according to their current Drive parents.
Files in any descendant of a managed folder are treated as published.
@spec templates_folder_url() :: String.t() | nil
Get the Google Drive URL for the templates folder.
Like update_template_taxonomy/3 but for a Document.
@spec update_preset(PhoenixKitDocumentCreator.Schemas.TemplatePreset.t(), map()) :: {:ok, PhoenixKitDocumentCreator.Schemas.TemplatePreset.t()} | {:error, Ecto.Changeset.t()}
Updates an existing preset from attrs.
@spec update_template_language(String.t(), String.t() | nil, keyword()) :: {:ok, PhoenixKitDocumentCreator.Schemas.Template.t()} | {:error, :not_found | Ecto.Changeset.t()}
Set the locale on a template, looked up by google_doc_id.
Pass nil (or an empty string) to clear the language. Otherwise the
full locale code (e.g. "en-US", "et-EE") — typically sourced from
PhoenixKit.Modules.Languages.get_enabled_languages/0.
Logs template.language_updated with the from/to pair on success and
broadcasts :files_changed so connected admin LiveViews resync.
Options
:actor_uuid— UUID of the user performing the action (activity log)
Sets (or clears) the category and type of a template identified by
google_doc_id. taxonomy is a map with optional :category_uuid
and :type_uuid keys (nil clears). Logs a
template.taxonomy_updated activity row.
@spec update_template_variable_config(String.t(), String.t(), map()) :: {:ok, PhoenixKitDocumentCreator.Schemas.Template.t()} | {:error, :not_found | :unknown_variable}
Update the config map of a single variable on a template's variables jsonb.
Merges new_config (string-keyed map) into the existing variable's config, coercing
integer-shaped strings to integers (for inputs from HTML form fields).
Uses an atomic SQL UPDATE with jsonb_agg to avoid the read-modify-write race
condition that would occur when two concurrent requests update different variables
on the same template.
Returns {:ok, template} on success, {:error, :not_found} if no template
matches the given file_id, or {:error, :unknown_variable} if the variable
name doesn't exist in the template's variables array.
@spec upsert_document_from_drive(map(), map()) :: {:ok, PhoenixKitDocumentCreator.Schemas.Document.t()} | {:error, Ecto.Changeset.t()}
Upsert a document record from a Google Drive file map.
@spec upsert_template_from_drive(map(), map()) :: {:ok, PhoenixKitDocumentCreator.Schemas.Template.t()} | {:error, Ecto.Changeset.t()}
Upsert a template record from a Google Drive file map.