Go utilities for building services that comply with Google's API Improvement Proposals (AIPs).
Two packages, both focused on turning AIP request fields into safe, parameterized SQL:
| Package | AIP | What it does |
|---|---|---|
aip160 |
AIP-160 | Parse filter strings into a typed AST and generate parameterized SQL WHERE clauses, with per-field type checking and JSONB support. |
aip132 |
AIP-132 | Parse order_by strings into ordered field clauses. |
go get github.com/firetiger-oss/aip@latest
Declare the fields a caller is allowed to filter on (and their SQL types), parse a filter string, then render it to SQL with bound parameters. Unknown fields and type mismatches are rejected at parse/validation time, so untrusted filter input never reaches your database as raw text.
import (
"github.com/firetiger-oss/aip/aip160"
)
ctx, err := aip160.NewFilterContext(
aip160.NewFilterableField(aip160.SQLTypeText, "name"),
aip160.NewFilterableField(aip160.SQLTypeTimestamp, "create_time"),
aip160.NewFilterableField(aip160.SQLTypeInt64, "size"),
)
if err != nil {
// duplicate or malformed field declarations
}
filter, err := aip160.ParseFilter(`name = "report.pdf" AND size > 1024`)
if err != nil {
// invalid filter syntax or unknown field
}
sql, params, err := ctx.ToSQL(filter)
// sql: (("name" = $1) AND ("size" > $2))
// params: []any{"report.pdf", int64(1024)}Columns are quoted and each comparison is parenthesized. Bound parameters use
PostgreSQL $N placeholders.
ToSQL(nil) yields an empty clause, so an absent filter is a no-op.
Filter into a JSONB column with NewJSONBFilterableField /
NewJSONBMapFilterableField. The first string argument is the physical JSONB
column; the remaining segments are the filter path (and the path within the JSON
document). A map<string,string> field is addressed by key with dot notation.
ctx, _ := aip160.NewFilterContext(
aip160.NewJSONBFilterableField(aip160.SQLTypeText, "body", "display_name"),
aip160.NewJSONBMapFilterableField("body", "labels"),
)
filter, _ := aip160.ParseFilter(`display_name = "alice" AND labels.env = "prod"`)
sql, params, _ := ctx.ToSQL(filter)
// sql: (("body"->>'display_name' = $1) AND ("body"->'labels'->>'env' = $2))
// params: []any{"alice", "prod"}import (
"github.com/firetiger-oss/aip/aip132"
"github.com/firetiger-oss/aip/aip160"
)
clauses, err := aip132.ParseOrderBy("create_time desc, name")
// []aip132.OrderBy{{FieldPath: ...}, ...}
orderSQL, err := ctx.OrderByToSQL(clauses) // ctx is an aip160.FilterContext
// orderSQL: "create_time" DESC, "name"OrderByToSQL returns the column list only (no ORDER BY keyword); ascending is
the default, so only descending columns carry a direction suffix.
aip160.FilterContext.OrderByToSQL validates ordering fields against the same
field set as filtering, so the two stay consistent.
Apache-2.0. See LICENSE.
The aip132 package contains code derived from the
LUCI project
(Apache-2.0); see NOTICE.