Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions .agents/skills/graphile-meta/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,29 +121,31 @@ map it onto a `Meta*` GraphQL type.**

## Adding a new metadata block to `_meta`

Worked pattern (this is exactly how a `scope` block is being added — mirror the
Worked pattern (this is exactly how the `scope` block was added — mirror the
`storage`/`search` precedent):

1. **(DB, if the data isn't already a physical column)** Emit a smart tag from the
generator so it lands on the codec, e.g.
`metaschema.append_table_smart_tags(v_table_id, jsonb_build_object('scope', v_scope))`
at the single scope decision point (`apply_scope_fields.sql`), then
`pgpm package` + `pnpm run generate:constructive`.
1. **(DB)** Emit a smart tag carrying **all** the metadata from the generator so it
lands on the codec — never make the plugin guess. For scope, emit every field
(`scope`, `scopeTier`, `scopeKey`, `scopeEntityTable`) at the single decision
point (`apply_scope_fields.sql`):
`metaschema.append_table_smart_tags(v_table_id, jsonb_build_object('scope', v_scope, 'scopeTier', v_tier, 'scopeKey', v_key_col, 'scopeEntityTable', v_entity_table))`,
then `pgpm package` + `pnpm run generate:constructive`.
2. **`types.ts`** — add the interface (e.g. `ScopeMeta`) and a nullable field on
`TableMeta` (`scope: ScopeMeta | null`).
3. **A builder** — add `buildScopeMeta(codec, build, inflectAttr): ScopeMeta | null`
in `storage-search-meta-builders.ts` (or a new `scope-meta-builders.ts`). Read
`codec.extensions.tags`, return `null` when the feature is absent (the
convention across all builders). Optionally infer from physical columns as a
fallback (e.g. `database_id` present → database scope) and record provenance.
3. **A builder** — add `buildScopeMeta(codec, inflectAttr): ScopeMeta | null`
in a builder file (`scope` lives in `scope-meta-builders.ts`). Read
`codec.extensions.tags` and return the values **verbatim**; return `null` when
the tag is absent (the convention across all builders). Do NOT infer key
columns from physical column names — generators name key columns freely, so
column-name heuristics are brittle. The DB is the source of truth.
4. **`table-meta-builder.ts`** — call the builder inside `buildTableMeta` next to
`buildStorageMeta`/`buildSearchMeta` and add it to the returned object.
5. **`graphql-meta-field.ts`** — declare a `MetaScope` `GraphQLObjectType` and add
the `scope` field to `MetaTableType` (nullable, with a description).
6. **Tests** — extend `__tests__/meta-schema.test.ts` and its snapshot
(`__snapshots__/meta-schema.test.ts.snap`). Metadata is tested in-process with
mocked PostGraphile build resources (no live DB). Add both a tagged fixture and,
if you added inference, an untagged fixture.
mocked PostGraphile build resources (no live DB). Add a tagged fixture and an
untagged fixture (asserting `null`).
Run: `pnpm --filter graphile-settings exec jest --runInBand`

### Builder conventions (follow these)
Expand Down Expand Up @@ -171,7 +173,7 @@ file, or direct introspection). Beyond raw structure it gives codegen the
algorithm and know when `unifiedSearch` exists.
- **i18n / realtime** — generate translation helpers and subscription hooks only
where applicable.
- **Scope (planned)** — auto-inject `databaseId`/`orgId` scope keys from context,
- **Scope** — auto-inject `databaseId`/`orgId` scope keys from context,
mark them non-required in create inputs, group/emit CLI & docs by tier, and
choose the correct RLS/JWT context for generated test seeds.

Expand All @@ -188,12 +190,12 @@ file, or direct introspection). Beyond raw structure it gives codegen the
|---|---|---|
| `_meta` field missing | `MetaSchemaPreset`/`Plugin` not loaded | Add the preset (or use `ConstructivePreset`) |
| Table missing from `_meta.tables` | Its schema isn't in the configured `pgSchemas`, or resource isn't a real table | Ensure the schema is in `makePgService({ schemas })`; virtual/function resources are skipped |
| `storage`/`search`/`i18n`/`realtime`/`scope` is `null` | No corresponding smart tag / column detected | Emit the smart tag in SQL (`append_table_smart_tags`) and re-introspect; verify `codec.extensions.tags` |
| `storage`/`search`/`i18n`/`realtime`/`scope` is `null` | No corresponding smart tag emitted | Emit the smart tag in SQL (`append_table_smart_tags`) and re-introspect; verify `codec.extensions.tags` |
| Stale metadata after schema change | `_meta` is cached at build time | Rebuild the schema (server uses LISTEN/NOTIFY cache invalidation) |
| New field not appearing | Added to `types.ts`/builder but not to `graphql-meta-field.ts` | The GraphQL types are hand-declared — add the field to `MetaTableType` and its object type |

## Related Skills

- `graphile-search` — the search plugin whose activation `_meta.search` reflects
- `constructive-cli` / `constructive-graphql-codegen` — consumers of `_meta`
- `authoring-scoped-modules` (constructive-db) — the scope model `_meta.scope` will surface
- `authoring-scoped-modules` (constructive-db) — the scope model `_meta.scope` surfaces
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ exports[`MetaSchemaPlugin _meta query contract contains required selection paths
"relations.manyToMany.rightTable.name",
"relations.manyToMany.type",
"schemaName",
"scope",
"scope.entityTable",
"scope.keyColumn",
"scope.scope",
"scope.source",
"scope.tier",
"search",
"search.algorithms",
"search.columns",
Expand Down Expand Up @@ -293,6 +299,13 @@ exports[`MetaSchemaPlugin _meta query contract has stable printed GraphQL text 1
realtime {
subscriptionFieldName
}
scope {
scope
tier
keyColumn
entityTable
source
}
}
}
}"
Expand Down Expand Up @@ -732,6 +745,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult
"manyToMany": [],
},
"schemaName": "app_public",
"scope": null,
"search": null,
"storage": null,
"uniqueConstraints": [],
Expand Down Expand Up @@ -1338,6 +1352,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult
],
},
"schemaName": "app_public",
"scope": null,
"search": null,
"storage": null,
"uniqueConstraints": [],
Expand Down Expand Up @@ -1792,6 +1807,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult
"manyToMany": [],
},
"schemaName": "app_public",
"scope": null,
"search": null,
"storage": null,
"uniqueConstraints": [],
Expand Down Expand Up @@ -2234,6 +2250,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult
],
},
"schemaName": "app_public",
"scope": null,
"search": null,
"storage": null,
"uniqueConstraints": [
Expand Down Expand Up @@ -2587,6 +2604,7 @@ exports[`MetaSchemaPlugin snapshot scenarios produces stable metadata for a mult
"manyToMany": [],
},
"schemaName": "app_public",
"scope": null,
"search": null,
"storage": null,
"uniqueConstraints": [
Expand Down
Loading
Loading