feat(modules): group-docs schema + kind-generalized registry (§11.6 docs C1)
Data layer for extending shared group collections from KV to docs. KV behavior
unchanged (callers pass kind="kv").
- 0054_group_docs.sql: widen the group_collections kind CHECK to ('kv','docs')
and add the group-keyed group_docs store (clone of docs/0013, keyed by
group_id, CASCADE on group delete) + its (group_id,collection) and data GIN
indexes.
- group_collection_repo: thread a `kind` parameter through list_for_owner,
resolve_owning_group, insert/delete_collection_tx; add list_all_for_owner ->
(name,kind) for the kind-aware diff (D4). Relocate the injectable
GroupCollectionResolver trait + Postgres impl here (now shared by kv+docs;
resolve takes kind) — was in group_kv_service.
- group_docs_repo: a near-clone of docs_repo keyed by group_id; find reuses the
generalized build_find_query.
- docs_repo: generalize build_find_query(table, owner_col, owner_id, …) — both
are compile-time literals (no injection); app docs passes ("docs","app_id"),
group docs ("group_docs","group_id"). row_to_doc/build_find_query are now
pub(crate) for reuse.
Schema snapshot re-blessed (55 migrations).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -167,7 +167,7 @@ impl DocsRepo for PostgresDocsRepo {
|
||||
collection: &str,
|
||||
filter: &DocsFilter,
|
||||
) -> Result<Vec<DocRow>, DocsRepoError> {
|
||||
let mut qb = build_find_query(app_id, collection, filter);
|
||||
let mut qb = build_find_query("docs", "app_id", app_id.into_inner(), collection, filter);
|
||||
let rows = qb.build().fetch_all(&self.pool).await?;
|
||||
rows.into_iter().map(row_to_doc).collect()
|
||||
}
|
||||
@@ -281,7 +281,7 @@ impl DocsRepo for PostgresDocsRepo {
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_doc(row: PgRow) -> Result<DocRow, DocsRepoError> {
|
||||
pub(crate) fn row_to_doc(row: PgRow) -> Result<DocRow, DocsRepoError> {
|
||||
Ok(DocRow {
|
||||
id: row.try_get("id")?,
|
||||
data: row.try_get("data")?,
|
||||
@@ -316,14 +316,22 @@ fn decode_cursor(cursor: &str) -> Result<Uuid, DocsRepoError> {
|
||||
// **No user input ever lands in the SQL text unparameterized.**
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn build_find_query<'a>(
|
||||
app_id: AppId,
|
||||
/// Build the `find` query for a docs store. `table` and `owner_col` are
|
||||
/// compile-time literals (`"docs"`/`"app_id"` for app docs, `"group_docs"`/
|
||||
/// `"group_id"` for §11.6 group-shared docs) — never user input, so
|
||||
/// interpolating them is injection-safe. `pub(crate)` so the group-docs repo
|
||||
/// reuses this single source for the (security-sensitive) query SQL.
|
||||
pub(crate) fn build_find_query<'a>(
|
||||
table: &'static str,
|
||||
owner_col: &'static str,
|
||||
owner_id: uuid::Uuid,
|
||||
collection: &'a str,
|
||||
filter: &'a DocsFilter,
|
||||
) -> QueryBuilder<'a, Postgres> {
|
||||
let mut qb =
|
||||
QueryBuilder::new("SELECT id, data, created_at, updated_at FROM docs WHERE app_id = ");
|
||||
qb.push_bind(app_id.into_inner());
|
||||
let mut qb = QueryBuilder::new(format!(
|
||||
"SELECT id, data, created_at, updated_at FROM {table} WHERE {owner_col} = "
|
||||
));
|
||||
qb.push_bind(owner_id);
|
||||
qb.push(" AND collection = ");
|
||||
qb.push_bind(collection);
|
||||
|
||||
@@ -448,7 +456,13 @@ mod sql_shape_tests {
|
||||
|
||||
fn sql_for(filter_json: serde_json::Value) -> String {
|
||||
let filter = parse_filter(&filter_json).unwrap();
|
||||
let qb = build_find_query(AppId::new(), "users", &filter);
|
||||
let qb = build_find_query(
|
||||
"docs",
|
||||
"app_id",
|
||||
AppId::new().into_inner(),
|
||||
"users",
|
||||
&filter,
|
||||
);
|
||||
qb.sql().to_string()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user