@biffo/cli 0.281.0 → 0.283.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,8 +69,42 @@ plugin deploying clean with empty tables and the symptom surfacing wherever
69
69
  those rows were read
70
70
  ([#709](https://github.com/keiranholloway/biffo-template/issues/709)).
71
71
 
72
- Baseline data goes in one of two places instead, and `seed_default_widget()` in
73
- `plugin.py` documents both at the call site:
72
+ **Baseline data the rows a feature needs before it works at all, not a
73
+ single plugin's own config has a declared mechanism now
74
+ ([#1554](https://github.com/keiranholloway/biffo-template/issues/1554)):**
75
+ `biffo.plugin.json`'s `seed` block. This skeleton demonstrates it:
76
+
77
+ ```json
78
+ "seed": {
79
+ "dir": "db/seed",
80
+ "baseline_tables": ["example_widgets"]
81
+ }
82
+ ```
83
+
84
+ `dir` names a directory of idempotent `.sql` files (`db/seed/000_default_widget.sql`
85
+ here) — `biffo plugin install`/`upgrade` vendor them into the installing
86
+ instance's `db/imports/_plugin-example-plugin/`, where that instance's
87
+ existing "Apply DDL imports" deploy step (ADR-0005) applies them on every
88
+ deploy. No token, no per-tenant API call, no new deploy machinery — it is the
89
+ same mechanism `db/imports/<name>/` always was, just declared in the manifest
90
+ instead of a step nobody was told to perform. `baseline_tables` names which of
91
+ this manifest's own `tables` the seed populates; the instance's deploy then
92
+ fails loudly, by table and by tenant, if one ends up empty
93
+ (`biffo:plugin-baseline-check`) — instead of the feature silently rendering
94
+ nothing, which is what actually happened and is the incident #1554 records.
95
+
96
+ **The idempotency contract is not optional and is checksum-enforced.** Once a
97
+ seed file has been applied anywhere, editing it makes the next deploy fail
98
+ loudly (ADR-0005 §4) rather than silently re-applying or silently skipping. A
99
+ later change to your baseline data ships as a new, additively-numbered file —
100
+ see `db/seed/000_default_widget.sql`'s own header comment for the full
101
+ contract and the `INSERT ... SELECT ... WHERE NOT EXISTS` shape every file
102
+ here must follow.
103
+
104
+ This is a second, complementary mechanism to the one below — not a
105
+ replacement for it. `seed_default_widget()` in `plugin.py` still demonstrates
106
+ **self-seeding at startup**, which is the right tool for a plugin's own
107
+ runtime config rather than tenant-scoped table rows:
74
108
 
75
109
  - **If your plugin declares an `api_ingress` ASGI app** (ADR-0021), seed from
76
110
  that app's lifespan. The shared plugin host runs each mounted app's lifespan
@@ -82,8 +116,11 @@ Baseline data goes in one of two places instead, and `seed_default_widget()` in
82
116
  [#1000](https://github.com/keiranholloway/biffo-template/pull/1000), so treat
83
117
  this route as new and check your own.
84
118
  - **If it doesn't** — like `example_plugin`, which is event-only — there is no
85
- startup to hang anything on. Put the rows in a SQL module in the instance's
86
- `db/imports/<name>/`, applied by `biffo data apply` on every deploy.
119
+ startup to hang anything on. The `seed` manifest block above is its only
120
+ option, and is now the recommended default for baseline table rows even for
121
+ a plugin that does have an ASGI app: it reaches every tenant in one
122
+ statement, needs no running plugin process, and fails the deploy loudly
123
+ instead of only whenever a request happens to hit the lifespan hook.
87
124
 
88
125
  > Earlier revisions of this README pointed at an RBAC reference plugin at
89
126
  > `services/rbac/`. That plugin was removed by
@@ -83,6 +83,10 @@
83
83
  { "type": "nav-link", "label": "Example Plugin", "path": "/admin/example-plugin" },
84
84
  { "type": "page", "label": "Widgets", "path": "/admin/example-plugin/widgets" }
85
85
  ],
86
+ "seed": {
87
+ "dir": "db/seed",
88
+ "baseline_tables": ["example_widgets"]
89
+ },
86
90
  "dependencies": {
87
91
  "biffo-plugin-sdk": "^1.0"
88
92
  }
@@ -0,0 +1,47 @@
1
+ -- example-plugin's baseline-row seed (biffo-template#1554).
2
+ --
3
+ -- Declared by biffo.plugin.json's `seed` block (`dir: "db/seed"`,
4
+ -- `baseline_tables: ["example_widgets"]`). `biffo plugin install`/`upgrade`
5
+ -- vendor every *.sql file in this directory into the installing instance's
6
+ -- `db/imports/_plugin-example-plugin/`, where that instance's existing
7
+ -- "Apply DDL imports" deploy step (ADR-0005) applies it on every deploy —
8
+ -- no token, no per-tenant API call, no new deploy machinery. See
9
+ -- README.md's "Seeding" section and `plugin.py`'s `seed_default_widget()`
10
+ -- docstring for the two-mechanism picture this is one half of.
11
+ --
12
+ -- IDEMPOTENCY CONTRACT (read before copying this into your own plugin):
13
+ -- once applied, this file is checksum-tracked in ddl_import_history — an
14
+ -- edit to it after that point makes the next deploy fail loudly rather than
15
+ -- silently re-applying or silently skipping (ADR-0005 section 4). A later
16
+ -- change to this plugin's baseline data must ship as a NEW, additively
17
+ -- numbered file (001_..., 002_...), never as an edit to this one.
18
+ --
19
+ -- INSERT ... SELECT ... WHERE NOT EXISTS is the shape every seed file here
20
+ -- must follow: it reads the known tenants from `users` (Core's own
21
+ -- Alembic-managed table — see plugin_baseline_check.py's module docstring
22
+ -- for why that, not a generic `tenants` table, is what "known tenant" means
23
+ -- in this template) and inserts exactly one starter widget per tenant that
24
+ -- doesn't already have one, so re-running this file on every deploy is
25
+ -- always a no-op once each tenant has its row. Mirrors the shape (same
26
+ -- name, same description, same idempotent "insert if missing by name" logic)
27
+ -- of `seed_default_widget()` in `plugin.py` — that one seeds lazily, over
28
+ -- the API, from one plugin process's own ASGI lifespan; this one seeds every
29
+ -- known tenant in a single statement, from the instance's own deploy step.
30
+ -- Both are legitimate; which one to use for YOUR plugin is exactly the
31
+ -- question BiffoPluginBase's class docstring answers.
32
+ INSERT INTO example_widgets (id, tenant_id, name, description, is_active, created_at, updated_at)
33
+ SELECT
34
+ gen_random_uuid()::text,
35
+ known_tenant.tenant_id,
36
+ 'starter-widget',
37
+ 'Created by this plugin''s baseline seed. Safe to delete.',
38
+ true,
39
+ now(),
40
+ now()
41
+ FROM (SELECT DISTINCT tenant_id FROM users) AS known_tenant
42
+ WHERE NOT EXISTS (
43
+ SELECT 1
44
+ FROM example_widgets existing
45
+ WHERE existing.tenant_id = known_tenant.tenant_id
46
+ AND existing.name = 'starter-widget'
47
+ );
@@ -284,6 +284,25 @@
284
284
  },
285
285
  "description": "Portal UI elements the plugin adds."
286
286
  },
287
+ "seed": {
288
+ "type": "object",
289
+ "additionalProperties": false,
290
+ "required": ["dir"],
291
+ "properties": {
292
+ "dir": {
293
+ "type": "string",
294
+ "pattern": "^[\\w][\\w./-]*$",
295
+ "description": "Plugin-relative directory of baseline-seed .sql files (ADR-0005 DDL import). `biffo plugin install`/`upgrade` vendor every *.sql file directly under this directory (non-recursive) into the instance's db/imports/_plugin-<name>/, where the instance's existing deploy step applies it idempotently on every deploy via ddl_import_history checksum tracking. IDEMPOTENCY CONTRACT: files here must be safe to re-run — an INSERT ... SELECT ... WHERE NOT EXISTS pattern against a stable natural key, never a bare INSERT. They are checksum-locked once applied: editing an already-shipped file makes the next deploy fail loudly (ADR-0005 section 4) rather than silently re-applying or silently skipping. Ship a new, additively-numbered file for a later change instead of editing one already released."
296
+ },
297
+ "baseline_tables": {
298
+ "type": "array",
299
+ "items": { "type": "string" },
300
+ "default": [],
301
+ "description": "Names of this manifest's own `tables` that the seed DDL in `dir` guarantees are populated for every known tenant. Checked post-deploy (the `biffo:plugin-baseline-check` Lambda event, run from the deploy workflow after DDL imports are applied) — a table listed here with no rows for a tenant this deployment already knows about is a loud, specific deploy failure instead of a silently-empty feature (biffo-template#1554)."
302
+ }
303
+ },
304
+ "description": "Declares this plugin's tenant-scoped baseline-row seed. Optional — a plugin with no baseline data omits this key entirely, and nothing is vendored or checked for it."
305
+ },
287
306
  "tools": {
288
307
  "type": "array",
289
308
  "items": {
@@ -64,6 +64,18 @@ class ExamplePlugin(BiffoPluginBase):
64
64
  async def seed_default_widget(self) -> None:
65
65
  """Create this plugin's baseline row, idempotently.
66
66
 
67
+ **This is the ASGI-lifespan self-seeding path — for tenant-scoped
68
+ BASELINE TABLE rows (what a fresh install needs before the feature
69
+ works at all), prefer the declared `seed` manifest block instead
70
+ (biffo-template#1554): see `biffo.plugin.json`'s `seed` key and
71
+ `db/seed/000_default_widget.sql`, which seeds this same table for
72
+ every known tenant in one statement via the instance's existing
73
+ DDL-import deploy step, with no running plugin process and no
74
+ per-tenant request needed to trigger it.** This method stays as the
75
+ worked example of the *other* legitimate path — a plugin's own
76
+ runtime config, or anything that genuinely needs to run as this
77
+ plugin's own code rather than as SQL.
78
+
67
79
  Nothing in this skeleton calls this — deliberately. **Where you call
68
80
  it from depends on what kind of plugin you are building**, and
69
81
  neither answer is `on_install()`:
@@ -88,10 +100,9 @@ class ExamplePlugin(BiffoPluginBase):
88
100
  idempotent until #1000. Verify your seed; do not assume it.
89
101
 
90
102
  - **An event-only plugin like this one** declares no `api_ingress`,
91
- so it has no startup at all. Its baseline rows belong in a SQL
92
- module in the *instance's* `db/imports/<name>/`, applied by
93
- `biffo data apply` on every deploy the mechanism the first-party
94
- plugins use.
103
+ so it has no startup at all. Its baseline rows belong in the `seed`
104
+ manifest block described above — the mechanism the first-party
105
+ plugins use, now declared rather than a hand-run script.
95
106
 
96
107
  Idempotency: generic CRUD (issue #19) has no upsert — every POST
97
108
  creates a new row — so this lists existing widgets first and only
@@ -284,6 +284,25 @@
284
284
  },
285
285
  "description": "Portal UI elements the plugin adds."
286
286
  },
287
+ "seed": {
288
+ "type": "object",
289
+ "additionalProperties": false,
290
+ "required": ["dir"],
291
+ "properties": {
292
+ "dir": {
293
+ "type": "string",
294
+ "pattern": "^[\\w][\\w./-]*$",
295
+ "description": "Plugin-relative directory of baseline-seed .sql files (ADR-0005 DDL import). `biffo plugin install`/`upgrade` vendor every *.sql file directly under this directory (non-recursive) into the instance's db/imports/_plugin-<name>/, where the instance's existing deploy step applies it idempotently on every deploy via ddl_import_history checksum tracking. IDEMPOTENCY CONTRACT: files here must be safe to re-run — an INSERT ... SELECT ... WHERE NOT EXISTS pattern against a stable natural key, never a bare INSERT. They are checksum-locked once applied: editing an already-shipped file makes the next deploy fail loudly (ADR-0005 section 4) rather than silently re-applying or silently skipping. Ship a new, additively-numbered file for a later change instead of editing one already released."
296
+ },
297
+ "baseline_tables": {
298
+ "type": "array",
299
+ "items": { "type": "string" },
300
+ "default": [],
301
+ "description": "Names of this manifest's own `tables` that the seed DDL in `dir` guarantees are populated for every known tenant. Checked post-deploy (the `biffo:plugin-baseline-check` Lambda event, run from the deploy workflow after DDL imports are applied) — a table listed here with no rows for a tenant this deployment already knows about is a loud, specific deploy failure instead of a silently-empty feature (biffo-template#1554)."
302
+ }
303
+ },
304
+ "description": "Declares this plugin's tenant-scoped baseline-row seed. Optional — a plugin with no baseline data omits this key entirely, and nothing is vendored or checked for it."
305
+ },
287
306
  "tools": {
288
307
  "type": "array",
289
308
  "items": {