mcpField
O
objectstack-query

objectstack-query

github.com/objectstack-aiデータ・データベース76230 回インストール今日更新
リポジトリ

objectstack-query について

objectstack-queryは、ObjectStack開発者向けのクエリDSL設計スキルです。フィルター、ソート、ページネーション、関連レコードの展開、集計、検索、実行コンテキストのルールを解説します。

主な機能

  1. 01比較、集合、論理演算子による絞り込み
  2. 02オフセットまたはキーセットによるソートとページング
  3. 03関連レコードの展開とフィールド選択
  4. 04レコードのグループ化と集計
  5. 05全文検索と実行コンテキストの設定

活用シーン

  1. 01業務オブジェクトのレコードを絞り込みページ表示
  2. 02関連オブジェクトの項目とともにレコードを取得
  3. 03営業または運用データの集計
  4. 04テキスト項目のキーワード検索

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/objectstack-ai/objectstack --skill objectstack-query

インストール後、Claude Code で/objectstack-queryを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
filters-pagination-search.json
references/
_index.md
rules/
filters.md
aggregation.md
pagination.md

詳細

クエリ設計 — ObjectStack Query DSL

呼び出し規約 — object は最初の引数です

サーフェス形式有効なオプションキー
engine find / findOneengine.find('task', {…}, { context })context、where、fields、orderBy、limit、offset、search、searchFields、expand — および 6 つのドライバーへのパススルーキー transaction、tenantId、tenantIds、timezone、bypassTenantAudit、preserveAudit
engine aggregateengine.aggregate('deal', {…})context、where、groupBy、aggregations、having、timezone、search、searchFields — 2 つの検索キーは、find と同様に、where と AND 条件で入力行をグループ化の前に絞り込みます
engine countengine.count('task', {…})context、where
protocol / RESTfindData({ 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以下の $ 演算子このスキル
リストビュー / ナビ項目の filter20 個の演算子を持つ 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.joinsexpand(表示)、または where 内の { relation: { field: value } }(フィルタ)
query.distinctフィールドを groupBy でグループ化 — 一意の組み合わせごとに 1 行になります
query.windowFunctionsレポート / ダッシュボードのメタデータ(objectstack-ui)、またはアプリコードでの順位付け / 累積計算
集計の distinct: truecount_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
$nullnull かどうかを確認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 して確認し、プロパティ名の記憶に頼らないでください。

コメント