objectstack-query について
objectstack-queryは、ObjectStack開発者向けのクエリDSL設計スキルです。フィルター、ソート、ページネーション、関連レコードの展開、集計、検索、実行コンテキストのルールを解説します。
主な機能
- 01比較、集合、論理演算子による絞り込み
- 02オフセットまたはキーセットによるソートとページング
- 03関連レコードの展開とフィールド選択
- 04レコードのグループ化と集計
- 05全文検索と実行コンテキストの設定
活用シーン
- 01業務オブジェクトのレコードを絞り込みページ表示
- 02関連オブジェクトの項目とともにレコードを取得
- 03営業または運用データの集計
- 04テキスト項目のキーワード検索
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ objectstack-ai/ objectstack --skill objectstack-queryインストール後、Claude Code で/objectstack-queryを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/filters-pagination-search.jsonreferences/_index.mdrules/filters.mdaggregation.mdpagination.md詳細
クエリ設計 — ObjectStack Query DSL
呼び出し規約 — object は最初の引数です
| サーフェス | 形式 | 有効なオプションキー |
|---|---|---|
engine find / findOne | engine.find('task', {…}, { context }) | context、where、fields、orderBy、limit、offset、search、searchFields、expand — および 6 つのドライバーへのパススルーキー transaction、tenantId、tenantIds、timezone、bypassTenantAudit、preserveAudit |
engine aggregate | engine.aggregate('deal', {…}) | context、where、groupBy、aggregations、having、timezone、search、searchFields — 2 つの検索キーは、find と同様に、where と AND 条件で入力行をグループ化の前に絞り込みます |
engine count | engine.count('task', {…}) | context、where |
| protocol / REST | findData({ object: 'task', query: {…} }) | object はクエリの外側に置きます |
ネストされた expand の値 | QueryAST — { object, fields, where } | (Expand を参照) |
ENGINE_FIND_OPTION_KEYS / ENGINE_AGGREGATE_OPTION_KEYS は固定されたセットです。
この表にないキーは名前を示して拒否されます(find('task') does not recognise option 'bogus')。無視されることはありません。したがって、単独の { object: 'account', limit: 20 } リテラルは
QueryAST です。findData の query または expand
の値としては有効ですが、エンジンのオプションバッグではありません。チェックの前に
top は limit に、filter は where に変換されます。
パススルーの 6 つのキーが find / findOne(および update / delete)で渡されるのは、
それらのメソッドではオプションバッグがドライバーオプションのベースとなり、明示的な
tenantId をドライバーに渡せるためです。count と aggregate はこのバッグを転送しないため、
これら 2 つのメソッドでは同じキーは意図的に無効です。受け入れると、まさにこのチェックで
防ごうとしている、オプションが黙って無視される状態になってしまいます。例外は
timezone です。aggregate は日付のバケット化にこれを使うため、ここでは有効であり、
上の表にも記載されています。count はほかのキーと同様にこれを拒否します。
使用するフィルタの形式は?
| 記述対象… | 形式 | 管轄 |
|---|---|---|
ObjectQL の where | 以下の $ 演算子 | このスキル |
リストビュー / ナビ項目の filter | 20 個の演算子を持つ VIEW_FILTER_OPERATORS 列挙型(equals、icontains、is_null、before、between、…)に基づく [{ field, operator, value }] — 不明な演算子は解析時に拒否されます | objectstack-ui |
データセットメジャーの filter | メジャー独自のフィルタ | objectstack-ui |
実行コンテキスト(context)
RLS / システム読み取りのためのエスケープハッチです。フック、ジョブ、エンドポイントが これを指定せずに読み取りを行うと、呼び出し元が持つ ID のまま実行されます。その場合、組織スコープの フックはより少ない行を返し、「データがない」状態と区別できません。
const SYS = { isSystem: true } as const;
const [row] = await engine.find('project', { where: { name }, limit: 1, context: SYS });
実行エンベロープ(ID、テナント、トランザクション)は任意のサブセットを渡せます。
システム読み取りには { isSystem: true } を使用し、それ以外の場合は呼び出し元自身のコンテキスト
(ユーザー、役職、または権限セット)を使用します。指定しない場合は 403 PERMISSION_DENIED になります(ADR-0096 D5)。
READ メソッドでは、クエリバッグ(上記)または末尾のオプション引数
engine.find(obj, query, { context }) のどちらにも指定できます。両方に指定した場合は末尾の引数が優先されます。書き込みでは末尾の引数のみを受け取ります。
削除されたキー → 現在の代替手段
| 削除されたキー | 現在の代替手段 |
|---|---|
query.cursor | キーセットページング — ソートキーに対する where + orderBy + limit |
query.joins | expand(表示)、または where 内の { relation: { field: value } }(フィルタ) |
query.distinct | フィールドを groupBy でグループ化 — 一意の組み合わせごとに 1 行になります |
query.windowFunctions | レポート / ダッシュボードのメタデータ(objectstack-ui)、またはアプリコードでの順位付け / 累積計算 |
集計の distinct: true | count_distinct |
集計の array_agg / string_agg | なし — fields で行を読み取って呼び出し元で整形するか、ロールアップを保存フィールドとして実体化します |
これら 6 つはすべて @objectstack/spec 17 で廃止済みです。tsc では型が never となり、
いずれかを含むクエリはアップグレード手順を示して解析に失敗します。廃止手順と
廃止項目の全一覧については objectstack-upgrade を参照してください。
クイックリファレンス — 詳細ルール
- フィルター — すべての演算子、論理演算子の組み合わせ、関連レコードによるフィルタリング、日付マクロ、セッショントークン
- 集計 — groupBy、日付のバケット化、関数、
having、測定値ごとのfilter - ページネーション — オフセット方式とキーセット方式、ベストプラクティス、パフォーマンス
フィルター演算子
暗黙的な等価(省略形)
最もシンプルなフィルターです。フィールドが値と等しい場合に指定します。
{ where: { status: 'active' } }
// SQL: WHERE status = 'active'
比較演算子
| 演算子 | 用途 | SQL 相当 | 型 |
|---|---|---|---|
$eq | 等しい | = | 任意 |
$ne | 等しくない | <> | 任意 |
$gt | より大きい | > | 数値、日付 |
$gte | 以上 | >= | 数値、日付 |
$lt | より小さい | < | 数値、日付 |
$lte | 以下 | <= | 数値、日付 |
{ where: { age: { $gte: 18 } } }
// SQL: WHERE age >= 18
集合・範囲演算子
| 演算子 | 用途 | SQL 相当 |
|---|---|---|
$in | リスト内にある | IN (...) |
$nin | リスト内にない | NOT IN (...) |
$between | 両端を含む範囲 | BETWEEN ? AND ? |
{ where: { status: { $in: ['active', 'pending'] } } }
{ where: { amount: { $between: [100, 500] } } }
文字列演算子
| 演算子 | 用途 | SQL 相当 |
|---|---|---|
$contains | 部分文字列を含む | LIKE '%?%' |
$notContains | 部分文字列を含まない | NOT LIKE '%?%' |
$startsWith | プレフィックスで始まる | LIKE '?%' |
$endsWith | サフィックスで終わる | LIKE '%?' |
$icontains | 大文字・小文字を区別せずに含む | LIKE '%?%' を折りたたみ |
$like | 値全体に対するパターン。% / _ は呼び出し側が指定 | LIKE ? |
$ilike | $like の大文字・小文字を区別しない版 | ILIKE ? |
$contains / $notContains / $startsWith / $endsWith は
大文字・小文字を区別して比較します。$icontains は大文字・小文字を
区別しない対となる演算子です(ASCII のみ折りたたみます)。そのため、ユーザー向けの用途では $icontains を使います。
{ where: { email: { $icontains: '@company.com' } } }
全一覧、$like の移植性、$ilike の適用範囲については、フィルタールール を参照してください。
Null・存在演算子
| 演算子 | 用途 | SQL / NoSQL |
|---|---|---|
$null | null かどうかを確認 | IS NULL / IS NOT NULL |
$exists | 値があるかどうか | IS NOT NULL / IS NULL |
{ where: { deleted_at: { $null: true } } }
論理演算子
$and、$or、$not を使って条件を組み合わせます。
// OR: active accounts OR accounts with high revenue
{ where: { $or: [{ status: 'active' }, { revenue: { $gt: 1000000 } }] } }
// AND + OR combined
{
where: {
$and: [
{ type: 'enterprise' },
{ $or: [{ region: 'us' }, { region: 'eu' }] },
]
}
}
// NOT: exclude closed accounts
{ where: { $not: { status: 'closed' } } }
関連レコードによるフィルタリング
lookup の下にある { customer: { country: 'US' } } は where で処理されます。エンジンは呼び出し側として関連オブジェクトを読み込み、その ID と照合します。制限(1 階層のみ、順方向のみ、where のみ、ID は 1000 件まで、呼び出し側の権限)と、それらを回避する 2 段階の方法については、フィルタールール → リレーションフィルター を参照してください。
フィールド間の比較
{ $field: '...' } は、同じ行の 2 つの列を比較の位置でのみ比較します。詳しくは フィルタールール → フィールド参照 を参照してください。
並べ替え
orderBy を使って並べ替えます。指定するのはソートノードの配列です。
{
object: 'account',
orderBy: [
{ field: 'priority', order: 'desc' },
{ field: 'name', order: 'asc' }, // Secondary sort
]
}
ルール:
- 配列要素の順序でソートの優先順位が決まります
orderのデフォルトは'asc'です。昇順の場合は省略できます- パフォーマンスのため、ソートフィールドにはインデックスを付けてください(objectstack-data のインデックスルールを参照)
ページネーション
// Offset paging — page 3
{ object: 'account', limit: 20, offset: 40 }
用途: UI のページ、小規模なデータセット、「N ページ目へ移動」などです。オフセットが大きいとパフォーマンスが低下します。データベースはスキップする行も引き続きスキャンするためです。
キーセットページネーション(無限スクロール、API、大規模なデータセット、リアルタイムフィード)では、ソートキーを where に指定して最後に表示した行より後のデータをフィルターし、同じフィールドを同じ方向で必ず orderBy に指定してください。パターン、方向に関するルール、注意点については ページネーションルール を参照してください。
集計
6 つの関数(count、sum、avg、min、max、count_distinct)、日付のバケット化、having、測定値ごとのfilterは、**集計ルール**です。呼び出し形式は次のとおりです。
// Total revenue per region
const rows = await engine.aggregate('deal', {
groupBy: ['region'],
aggregations: [
{ function: 'sum', field: 'amount', alias: 'total_revenue' },
{ function: 'count', alias: 'deal_count' },
],
});
// SQL: SELECT region, SUM(amount) AS total_revenue, COUNT(*) AS deal_count
// FROM deal GROUP BY region
fields と orderBy は ENGINE_AGGREGATE_OPTION_KEYS に含まれていないため、aggregate に含めないでください。グループ化されたフィールドは結果の行に自動で選択されます。各測定値はそのaliasで読み取り、havingでも同じ名前を参照してください。groupBy の項目には、日付のバケット化に使うオブジェクトを指定できます。
{ field: 'closed_at', dateGranularity: 'quarter' }
Expand(関連レコード)
lookup/master_detail フィールドを通じて関連レコードを読み込みます。関係はその列を介して保持されるため、外部キーをfieldsに残してください。
const tasks = await engine.find('task', {
fields: ['title', 'status', 'assignee', 'project'], // the FK columns stay
expand: {
assignee: { object: 'user', fields: ['name', 'email'] },
project: {
object: 'project',
fields: ['name'],
expand: { org: { object: 'org', fields: ['name'] } }, // nested expand
},
},
});
ルール:
- 投影には外部キーカラムを必ず残してください。
expand: { project: … }と併せてfields: ['title']を指定しても、何も解決されません。エンジンは各レコードから外部キーを読み取り、存在しない場合は関係をスキップするため、関連データのない行がエラーなしで返されます。 - 展開の最大深度は、デフォルトで3です。
- エンジンはバッチ
$inクエリで展開を解決するため、N+1 クエリは発生しません。 expandのキーには、lookup または master_detail フィールド名を指定してください。- 各展開値はネストされた
QueryASTですが、エンジンが適用するのは選択(fields)とフィルター(where)のみです。この経路では、親レコードごとのlimit/offset/orderByは適用されません。関連レコードをページネーションまたは並べ替えするには、関連オブジェクトを直接クエリしてください。
全文検索
標準形式は、兄弟項目としてsearchFieldsを指定した単独の文字列です。
const rows = await engine.find('article', {
search: 'machine learning',
searchFields: ['title', 'content'],
limit: 10,
});
// Executes as:
// { $and: [
// { $or: [{ title: { $icontains: 'machine' } }, { content: { $icontains: 'machine' } }] },
// { $or: [{ title: { $icontains: 'learning' } }, { content: { $icontains: 'learning' } }] },
// ]}
各語は、解決された検索可能フィールド全体に対する $icontains 述語の $or になります。空白で区切られた語は AND で結合されるため、各語がいずれかのフィールドに一致する必要があります。select/status フィールドはオプションのラベルに一致し、保存値にマッピングされます。
**1 つの設定に 3 つの表記方法:**エンジンのオプションであるsearchFieldsを出力してください。プロトコルは$searchFieldsをこれに正規化します。また、オブジェクト形式のsearch: { query, fields }では、同じ絞り込み用のfieldsを指定できます。
この項目を省略すると、オブジェクトで宣言されたsearchableFields(または、名前/タイトルと短文フィールドを使った自動デフォルト)がサーバー側で解決され、検索対象になります。この集合を絞り込むことはできますが、広げることはできません。REST/プロトコルの入口では、この集合にない名前を指定すると、全件スキャンへの暗黙のフォールバックではなく、400 INVALID_FIELDが返されます。オブジェクト形式のsearch: { query, fields }は、以下の Tier-2 の設定でも引き続き利用できます。
⚠️ 検証後に無視されます — これらは絶対に出力しないでください。
fuzzy、boost、operator、minScore、language、highlightがすべてです。各項目の.describe()マーカーにその旨が示されています。語は常に AND で結合され、関連度スコアの計算やハイライト表示はありません。
searchは関係をたどりません。 ドット付きパスは拒否されます。
searchFields: ['project_id.name']は、taskが宣言していない列を指定しています。関連レコードのタイトルをクエリ対象オブジェクトの保存フィールドに複製し、そのフィールドを検索してください。フィールド、書き込みフック、lint の文言については、**objectstack-data → Search Fields (searchableFields)**を参照してください。関連レコードの列でフィルターするには、where: { relation: { column: value } }を使います。表示するにはexpandを使います。
よくあるパターン
オブジェクトをまたぐクエリ:どのツールを使う?
| シナリオ | 使用するもの |
|---|---|
| 表示用にlookupフィールドを読み込む | expand |
| lookup先の列で行をフィルターする | whereに{ lookup: { column: value } }を指定します。関連 ID は最大 1000 件までです。上限を超えた場合は対象をクエリし、親側で{ lookup: { $in: ids } }を指定します。multipleの場合は ID ごとに$containsを使います。 |
| 子レコードの条件で親レコードをフィルターする | fields: [lookup]を指定して子をクエリし、親側で{ id: { $in: those ids } }を指定します。 |
| 関連レコードのタイトルでキーワード検索する | タイトルをこのオブジェクトの保存フィールドに複製して検索します — searchは関係をたどりません。 |
| 親レコードの関連レコードをページネーション/並べ替えする | 関連オブジェクトを直接クエリします。 |
| オブジェクトをまたぐ分析クエリ | レポート/ダッシュボードのメタデータを使うか、個別のクエリ結果をアプリコードで結合します。 |
API のページネーションパターン
const page = await engine.find('account', {
where: { status: 'active' },
fields: ['id', 'name', 'email'],
orderBy: [{ field: 'name', order: 'asc' }],
limit: 20,
offset: (pageNumber - 1) * 20,
});
ダッシュボードの集計パターン
ダッシュボードのすべての KPI で、集計呼び出しは1 回だけ共有します。無条件の指標はそのまま記述し、条件付きの指標にはそれぞれ独自の filter を指定します。where は呼び出し全体に適用されるため、すべての指標で同じスコープを使用する場合にのみ指定してください。
const [kpis] = await engine.aggregate('deal', {
aggregations: [
{ function: 'count', alias: 'total_deals' },
{ function: 'sum', field: 'amount', alias: 'pipeline_value' },
{ function: 'avg', field: 'amount', alias: 'avg_deal_size' },
{ function: 'count', alias: 'won_deals', filter: { stage: 'closed_won' } },
],
});
ダッシュボードやレポート自体(KPI ウィジェット、compareTo、dateGranularity によるバケット化、マトリックスの行と列)はメタデータであり、クエリを手書きする必要はありません。objectstack-ui でモデル化すると、レンダラーがクエリを発行します。
作業を検証する
ほとんどのクエリは実行時に動作します(os data query または vitest のテストでスモークテストしてください)。一方、クエリのメタデータ(リストビューのフィルタースペックやレポート/ダッシュボードのデータセット)は静的に検証されます。これらを編集した後は、次を実行してください。
os validate # schema + CEL predicates + widget/dataset bindings (no artifact)
# or: os build # the same gates, plus emits dist/
dataset/dimensions/values が解決できないダッシュボードウィジェットは、空のチャートとして表示されず、ここでエラーになります(ADR-0021)。スキャフォールドされたプロジェクトでは、このチェックを npm run validate で実行します。objectstack-platform の「作業を検証する」を参照してください。
参考資料
Zod スキーマの全一覧(1 行の説明付き)は references/_index.md を参照してください。各項目は node_modules/@objectstack/spec/src/ 内の該当箇所を示します。フィールドの正確な形式は必ずソースを Read して確認し、プロパティ名の記憶に頼らないでください。