@dashai/sdk 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +377 -0
- package/dist/auth/client.cjs +185 -0
- package/dist/auth/client.cjs.map +1 -0
- package/dist/auth/client.d.cts +137 -0
- package/dist/auth/client.d.ts +137 -0
- package/dist/auth/client.js +175 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/middleware.cjs +352 -0
- package/dist/auth/middleware.cjs.map +1 -0
- package/dist/auth/middleware.d.cts +90 -0
- package/dist/auth/middleware.d.ts +90 -0
- package/dist/auth/middleware.js +343 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/server.cjs +445 -0
- package/dist/auth/server.cjs.map +1 -0
- package/dist/auth/server.d.cts +170 -0
- package/dist/auth/server.d.ts +170 -0
- package/dist/auth/server.js +432 -0
- package/dist/auth/server.js.map +1 -0
- package/dist/auth/types.cjs +31 -0
- package/dist/auth/types.cjs.map +1 -0
- package/dist/auth/types.d.cts +163 -0
- package/dist/auth/types.d.ts +163 -0
- package/dist/auth/types.js +29 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/deps/index.cjs +117 -0
- package/dist/deps/index.cjs.map +1 -0
- package/dist/deps/index.d.cts +93 -0
- package/dist/deps/index.d.ts +93 -0
- package/dist/deps/index.js +112 -0
- package/dist/deps/index.js.map +1 -0
- package/dist/errors-BV75u7b9.d.cts +207 -0
- package/dist/errors-BV75u7b9.d.ts +207 -0
- package/dist/index.cjs +721 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +171 -0
- package/dist/index.d.ts +171 -0
- package/dist/index.js +703 -0
- package/dist/index.js.map +1 -0
- package/dist/query/index.cjs +621 -0
- package/dist/query/index.cjs.map +1 -0
- package/dist/query/index.d.cts +800 -0
- package/dist/query/index.d.ts +800 -0
- package/dist/query/index.js +617 -0
- package/dist/query/index.js.map +1 -0
- package/dist/react/index.cjs +199 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +117 -0
- package/dist/react/index.d.ts +117 -0
- package/dist/react/index.js +195 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-BLNQ1S1C.d.cts +396 -0
- package/dist/types-BLNQ1S1C.d.ts +396 -0
- package/package.json +109 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import { b as BaseRow, P as Page, W as Where, a as Client } from '../types-BLNQ1S1C.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wire-format Query AST — the JSON shape the SDK serializes and the
|
|
5
|
+
* backend's query plane consumes.
|
|
6
|
+
*
|
|
7
|
+
* The AST is a **public, versioned** contract. Bumping its shape in a
|
|
8
|
+
* way that can't be transparently re-mapped on the backend is a
|
|
9
|
+
* SemVer-significant change for `@dashai/sdk`. The `v` field at the
|
|
10
|
+
* root is the single discriminator the backend uses to decide whether
|
|
11
|
+
* it can handle a given query.
|
|
12
|
+
*
|
|
13
|
+
* Coverage by phase (per `dashwise-devops-docs/plans/modules-sdk-query-v1.md`):
|
|
14
|
+
*
|
|
15
|
+
* - **Phase 2** (this file, today): `v: 1`, `from: { kind: 'owned' }`.
|
|
16
|
+
* Bare table reads. Subsequent slices add `where`, `select`,
|
|
17
|
+
* `orderBy`, `limit`, `offset`, `count`, then aggregations.
|
|
18
|
+
* - **Phase 3**: `joins[]`, `from: { kind: 'view' }`.
|
|
19
|
+
* - **Phase 4**: `from: { kind: 'dep' }` for cross-module reads,
|
|
20
|
+
* `joins[].to` for cross-module joins via the planner.
|
|
21
|
+
*
|
|
22
|
+
* The shape below carries all the optional fields the v1 plan promises
|
|
23
|
+
* (commented per-field with the phase they activate in). Today's
|
|
24
|
+
* translator only honors `v` + `from`; everything else is parsed +
|
|
25
|
+
* rejected with `QUERY_TOO_COMPLEX` until the corresponding phase
|
|
26
|
+
* lands. Pre-declaring the shape lets the SDK builder stub out the
|
|
27
|
+
* fluent API today without forcing an AST version bump every time we
|
|
28
|
+
* activate a field.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Schema version. Bumped only on breaking changes. The backend
|
|
32
|
+
* supports `v` and `v+1` during deprecation windows so consumers can
|
|
33
|
+
* upgrade independently of the server. Add new optional fields under
|
|
34
|
+
* the current `v`; that's additive and non-breaking.
|
|
35
|
+
*/
|
|
36
|
+
type QueryAstVersion = 1;
|
|
37
|
+
/**
|
|
38
|
+
* The root AST node. Carries the version discriminator + the table
|
|
39
|
+
* reference + every optional clause.
|
|
40
|
+
*
|
|
41
|
+
* Designed so the backend can switch on `v` first, then validate the
|
|
42
|
+
* rest. Unknown root keys are NOT silently dropped — the validator
|
|
43
|
+
* rejects with `VALIDATION_FAILED` so typos surface loudly.
|
|
44
|
+
*/
|
|
45
|
+
interface QueryAst {
|
|
46
|
+
/** Schema discriminator. See {@link QueryAstVersion}. */
|
|
47
|
+
v: QueryAstVersion;
|
|
48
|
+
/** The primary table for this query. */
|
|
49
|
+
from: TableRef;
|
|
50
|
+
/**
|
|
51
|
+
* Optional joins (Phase 3+). When present + the planner can't handle
|
|
52
|
+
* the join (e.g. multi-module before Phase 4), the backend rejects
|
|
53
|
+
* with `QUERY_TOO_COMPLEX`.
|
|
54
|
+
*/
|
|
55
|
+
joins?: JoinNode[];
|
|
56
|
+
/**
|
|
57
|
+
* Where clause (Phase 2 slice 2). Mirrors the existing `Where<Row>`
|
|
58
|
+
* shape from `@dashai/sdk` (`./data/types.ts`) — recursive AND/OR
|
|
59
|
+
* groups + per-field operator objects. Field references inside `where`
|
|
60
|
+
* can be either bare (`'done'`, resolved against `from`) or qualified
|
|
61
|
+
* (`'tasks.done'`) when joins are present.
|
|
62
|
+
*/
|
|
63
|
+
where?: WhereClause;
|
|
64
|
+
/**
|
|
65
|
+
* Group-by fields for aggregation queries (Phase 3). Field refs follow
|
|
66
|
+
* the same bare-or-qualified rule as `where`.
|
|
67
|
+
*/
|
|
68
|
+
groupBy?: FieldRef[];
|
|
69
|
+
/**
|
|
70
|
+
* Aggregations keyed by output column name. Phase 3.
|
|
71
|
+
*
|
|
72
|
+
* { totalDone: { count: '*' }, oldest: { min: 'created_at' } }
|
|
73
|
+
*/
|
|
74
|
+
agg?: Record<string, AggExpr>;
|
|
75
|
+
/**
|
|
76
|
+
* Columns to select. Default = all fields of `from`. Supports `'*'`
|
|
77
|
+
* shorthand + `'as'` aliases (`'users.email as assignee_email'`).
|
|
78
|
+
* Phase 2 slice 2.
|
|
79
|
+
*/
|
|
80
|
+
select?: SelectExpr[];
|
|
81
|
+
/** Order-by clauses (Phase 2 slice 2). */
|
|
82
|
+
orderBy?: OrderClause[];
|
|
83
|
+
/** Max rows to return. Default 50 (matches owned-table CRUD). */
|
|
84
|
+
limit?: number;
|
|
85
|
+
/** Row offset for pagination. Default 0. */
|
|
86
|
+
offset?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Phase 3 slice 3.5 — when joins are present, return rows in
|
|
89
|
+
* SQL-style flat shape (`{ 'users.email': value }`) instead of
|
|
90
|
+
* the nested default (`row.users.email`). Ignored without joins.
|
|
91
|
+
*
|
|
92
|
+
* SDK ships the flag in slice 3.3 so authors can write
|
|
93
|
+
* `.flatten()` today; backend wiring lands in slice 3.5. Until
|
|
94
|
+
* then the backend silently ignores it (defaults to nested).
|
|
95
|
+
*/
|
|
96
|
+
flatten?: boolean;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Discriminated union of every table source. Adding a new kind is
|
|
100
|
+
* additive (consumers using `switch (ref.kind)` must `default` to
|
|
101
|
+
* reject — covered by lint).
|
|
102
|
+
*
|
|
103
|
+
* - `owned`: a table from this module's manifest `owns.tables[]`.
|
|
104
|
+
* - `dep`: a table from another module borrowed via `dependencies[]`
|
|
105
|
+
* (Phase 4 — not yet routable, validator rejects).
|
|
106
|
+
* - `view`: a manifest-declared view (Phase 5 — not yet routable).
|
|
107
|
+
*/
|
|
108
|
+
type TableRef = {
|
|
109
|
+
kind: 'owned';
|
|
110
|
+
slug: string;
|
|
111
|
+
} | {
|
|
112
|
+
kind: 'dep';
|
|
113
|
+
module: string;
|
|
114
|
+
slug: string;
|
|
115
|
+
} | {
|
|
116
|
+
kind: 'view';
|
|
117
|
+
slug: string;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Join node — Phase 3+. Includes `on` as an explicit field-pair map
|
|
121
|
+
* because link-field-derived defaults can't be inferred from the AST
|
|
122
|
+
* alone (they live on the manifest). The SDK builder may compute `on`
|
|
123
|
+
* from a link field at compile time and inline it here.
|
|
124
|
+
*/
|
|
125
|
+
interface JoinNode {
|
|
126
|
+
kind: 'inner' | 'left';
|
|
127
|
+
to: TableRef;
|
|
128
|
+
/**
|
|
129
|
+
* Map of left-side qualified field ref → right-side qualified field
|
|
130
|
+
* ref. E.g. `{ 'tasks.assignee_id': 'users.id' }`.
|
|
131
|
+
*/
|
|
132
|
+
on: Record<string, string>;
|
|
133
|
+
/**
|
|
134
|
+
* Alias on result rows. Defaults to `to.slug` (or the link-field
|
|
135
|
+
* slug when the SDK's `.join('<link-slug>')` resolves through a
|
|
136
|
+
* link field). Reserved for multi-join-to-same-target (out of
|
|
137
|
+
* scope for slice 3.2; field is parsed but unused today).
|
|
138
|
+
*/
|
|
139
|
+
as?: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Where clause — same shape as the existing `Where<Row>` type but
|
|
143
|
+
* untyped here (the AST is the wire format; the typed builder layer
|
|
144
|
+
* provides per-field narrowing). Top-level keys AND together; `AND`
|
|
145
|
+
* and `OR` arrays group explicitly.
|
|
146
|
+
*
|
|
147
|
+
* Examples:
|
|
148
|
+
* { done: false } // shorthand for { equal: false }
|
|
149
|
+
* { title: { contains: 'urgent' } } // operator object
|
|
150
|
+
* { AND: [{ done: false }, { ... }] } // explicit AND group
|
|
151
|
+
* { OR: [{ priority: 'high' }, ...] } // OR group
|
|
152
|
+
* { 'tasks.done': false } // qualified field ref (after joins)
|
|
153
|
+
*/
|
|
154
|
+
interface WhereClause {
|
|
155
|
+
AND?: WhereClause[];
|
|
156
|
+
OR?: WhereClause[];
|
|
157
|
+
[field: string]: any;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Field reference — either bare (`'done'`) or qualified
|
|
161
|
+
* (`'tasks.done'`). Qualified refs are only valid when the AST has
|
|
162
|
+
* joins; the validator enforces this.
|
|
163
|
+
*/
|
|
164
|
+
type FieldRef = string;
|
|
165
|
+
/**
|
|
166
|
+
* Select expression — a field ref, the `'*'` shorthand, or a string
|
|
167
|
+
* with an `'as'` alias.
|
|
168
|
+
*
|
|
169
|
+
* 'id'
|
|
170
|
+
* '*'
|
|
171
|
+
* 'tasks.title'
|
|
172
|
+
* 'users.email as assignee_email'
|
|
173
|
+
*
|
|
174
|
+
* Strict mode (Phase 2 OD2 from the plan): `'*'` selects all fields
|
|
175
|
+
* from `from`, NOT all fields from every joined table. Authors must
|
|
176
|
+
* explicitly opt into joined fields with `'<table>.<field>'`.
|
|
177
|
+
*/
|
|
178
|
+
type SelectExpr = string;
|
|
179
|
+
interface OrderClause {
|
|
180
|
+
field: FieldRef;
|
|
181
|
+
dir: 'asc' | 'desc';
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Aggregation expression. Each function takes either `'*'` (count
|
|
185
|
+
* only) or a specific field ref.
|
|
186
|
+
*
|
|
187
|
+
* { count: '*' }
|
|
188
|
+
* { sum: 'amount' }
|
|
189
|
+
* { avg: 'amount' }
|
|
190
|
+
* { min: 'created_at' }
|
|
191
|
+
* { max: 'updated_at' }
|
|
192
|
+
*/
|
|
193
|
+
type AggExpr = {
|
|
194
|
+
count: '*' | FieldRef;
|
|
195
|
+
} | {
|
|
196
|
+
sum: FieldRef;
|
|
197
|
+
} | {
|
|
198
|
+
avg: FieldRef;
|
|
199
|
+
} | {
|
|
200
|
+
min: FieldRef;
|
|
201
|
+
} | {
|
|
202
|
+
max: FieldRef;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Runtime check that an unknown value matches the AST shape at the
|
|
206
|
+
* structural level (not field-level — that's the backend validator's
|
|
207
|
+
* job). Useful for SDK-side fail-fast checks before serializing.
|
|
208
|
+
*
|
|
209
|
+
* Intentionally permissive about optional fields: the AST grew over
|
|
210
|
+
* phases, and older consumers might lack `joins` / `agg` / etc.
|
|
211
|
+
* Strict checks happen at the backend.
|
|
212
|
+
*/
|
|
213
|
+
declare function isQueryAst(v: unknown): v is QueryAst;
|
|
214
|
+
/**
|
|
215
|
+
* Convenience: the current schema version as a literal. Useful when
|
|
216
|
+
* constructing ASTs programmatically — keeps the magic number out of
|
|
217
|
+
* the call site.
|
|
218
|
+
*/
|
|
219
|
+
declare const CURRENT_QUERY_AST_VERSION: QueryAstVersion;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Typed query builder — fluent API that compiles to a {@link QueryAst}
|
|
223
|
+
* and posts it to `POST /api/modules/<installation>/query`.
|
|
224
|
+
*
|
|
225
|
+
* Phase 2 slice 1 (this file): the minimum E2E proof — `qb.from(slug).execute()`
|
|
226
|
+
* round-trips through the backend. Future slices extend with
|
|
227
|
+
* `.where`, `.select`, `.orderBy`, `.limit`, `.offset`, `.count`, then
|
|
228
|
+
* aggregations + joins. The builder's shape is designed to fit those
|
|
229
|
+
* additions without renaming or breaking anything we ship today.
|
|
230
|
+
*
|
|
231
|
+
* Composability: every chainable method returns a new immutable
|
|
232
|
+
* `Query<Row>` — never mutates `this`. That lets authors derive
|
|
233
|
+
* sub-queries from a base:
|
|
234
|
+
*
|
|
235
|
+
* ```ts
|
|
236
|
+
* const base = qb.from('tasks');
|
|
237
|
+
* const all = await base.execute();
|
|
238
|
+
* // (future: const overdue = await base.where({ done: false }).execute())
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* The factory accepts a {@link Client} (not the raw transport) so the
|
|
242
|
+
* public surface stays small + the same builder works whether the
|
|
243
|
+
* client was constructed with `createClient` or a test stub that
|
|
244
|
+
* implements the {@link Client} interface.
|
|
245
|
+
*
|
|
246
|
+
* Spec: `dashwise-devops-docs/plans/modules-sdk-query-v1.md` §6.3.
|
|
247
|
+
*/
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Options passed to {@link Query.join} / {@link Query.leftJoin}.
|
|
251
|
+
*
|
|
252
|
+
* `on` is REQUIRED in slice 3.3 — link-field-derived inference lands
|
|
253
|
+
* in a follow-up. Both sides of each pair must be qualified
|
|
254
|
+
* (`<table>.<field>`) so the SDK + backend can disambiguate after
|
|
255
|
+
* joins.
|
|
256
|
+
*
|
|
257
|
+
* `as` is reserved for explicit aliasing when joining the same
|
|
258
|
+
* target multiple times (rare; not in v1).
|
|
259
|
+
*/
|
|
260
|
+
interface JoinOpts<K extends 'inner' | 'left'> {
|
|
261
|
+
on: Record<string, string>;
|
|
262
|
+
/** Default 'inner' for `.join()`; `.leftJoin()` hardcodes 'left'. */
|
|
263
|
+
kind?: K;
|
|
264
|
+
as?: string;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Result envelope for aggregation queries — distinct from
|
|
268
|
+
* {@link Page} because aggregation rows have a different shape (one
|
|
269
|
+
* row per group, with output keys matching the `agg` map keys + the
|
|
270
|
+
* `groupBy` field names) and don't carry pagination metadata in the
|
|
271
|
+
* same way.
|
|
272
|
+
*
|
|
273
|
+
* `rows` shape: `Record<string, unknown>` because the keys are
|
|
274
|
+
* dynamic (mix of group-by field names + aggregation output keys);
|
|
275
|
+
* codegen will later emit a per-query typed shape, but the runtime
|
|
276
|
+
* client stays untyped at the value level.
|
|
277
|
+
*
|
|
278
|
+
* `total` = number of result rows (one per group).
|
|
279
|
+
*/
|
|
280
|
+
interface QueryResult {
|
|
281
|
+
rows: Array<Record<string, unknown>>;
|
|
282
|
+
total: number;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Result envelope for `.explain()` (Phase 5, 2026-05-19). Returned by
|
|
286
|
+
* `POST /api/modules/:installationId/query/explain`. The backend runs
|
|
287
|
+
* Postgres `EXPLAIN (FORMAT JSON)` against the SQL it would emit —
|
|
288
|
+
* WITHOUT executing it (no `ANALYZE`, no rows materialized).
|
|
289
|
+
*
|
|
290
|
+
* Fields:
|
|
291
|
+
* - `plan` — the full Postgres plan JSON tree (opaque object;
|
|
292
|
+
* `plan.Plan` is the root node, `plan.Plan.Plans[]` its children).
|
|
293
|
+
* Useful for visualization tools that want the original tree
|
|
294
|
+
* structure preserved.
|
|
295
|
+
* - `nodes` — depth-first flattened array of plan nodes. Each entry
|
|
296
|
+
* carries `Node Type`, `Total Cost`, `Plan Rows`, and a `depth`
|
|
297
|
+
* annotation (root is depth 0; children depth 1; etc.). Easier to
|
|
298
|
+
* render in a flat table than the recursive tree.
|
|
299
|
+
* - `estimatedCost` — `Total Cost` of the root plan node. Postgres'
|
|
300
|
+
* planner-side cost estimate (not actual runtime). Sum of startup +
|
|
301
|
+
* execution cost in arbitrary cost units.
|
|
302
|
+
* - `estimatedRows` — `Plan Rows` of the root plan node. Planner's
|
|
303
|
+
* estimate of rows the query would produce. May differ from actual
|
|
304
|
+
* once table stats are stale (run `ANALYZE` on the underlying
|
|
305
|
+
* tables to refresh).
|
|
306
|
+
* - `sql` — the generated SQL string the query plane would execute.
|
|
307
|
+
* Surfaced for debugging — useful when the plan doesn't match what
|
|
308
|
+
* you expected and you want to see what was actually compiled.
|
|
309
|
+
* - `paramsCount` — number of bound parameters in the SQL. Cross-check
|
|
310
|
+
* against the WHERE / ON clauses in your AST.
|
|
311
|
+
*
|
|
312
|
+
* MVP support (backend slice 5 `.explain()` MVP):
|
|
313
|
+
* - ✅ Joined queries (any `.join()` / `.leftJoin()` call)
|
|
314
|
+
* - ✅ Joined aggregations (joins + `.agg()` / `.groupBy()`)
|
|
315
|
+
* - ⏳ Bare own-table / bare cross-module dep / views / own-table agg
|
|
316
|
+
* — reject with `EXPLAIN_NOT_SUPPORTED_YET` (typed code; the
|
|
317
|
+
* thrown error carries `context: { fromKind, hasJoins, hasAgg }`
|
|
318
|
+
* so callers can detect + fall back to `.execute()` for the
|
|
319
|
+
* unsupported shapes).
|
|
320
|
+
*/
|
|
321
|
+
interface ExplainResult {
|
|
322
|
+
plan: unknown;
|
|
323
|
+
nodes: Array<{
|
|
324
|
+
['Node Type']: string;
|
|
325
|
+
['Total Cost']?: number;
|
|
326
|
+
['Plan Rows']?: number;
|
|
327
|
+
depth: number;
|
|
328
|
+
[key: string]: unknown;
|
|
329
|
+
}>;
|
|
330
|
+
estimatedCost: number;
|
|
331
|
+
estimatedRows: number;
|
|
332
|
+
sql: string;
|
|
333
|
+
paramsCount: number;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Reference to a dependency module's table. Lands with Phase 4 slice
|
|
337
|
+
* 4.1b (the SDK side of the cross-module query plane). The backend
|
|
338
|
+
* routes these through {@link DepsRuntimeService.listRowsForSdk} which
|
|
339
|
+
* enforces the consumer's declared-dep contract + field projection
|
|
340
|
+
* server-side.
|
|
341
|
+
*
|
|
342
|
+
* Authors typically reach this through the codegen-emitted
|
|
343
|
+
* `qb.from(deps.crm.contacts)` shape, but the literal form
|
|
344
|
+
* `qb.from({ kind: 'dep', module: 'crm', slug: 'contacts' })` is also
|
|
345
|
+
* supported for tests + AI-builder authoring.
|
|
346
|
+
*/
|
|
347
|
+
interface QbDepRef {
|
|
348
|
+
kind: 'dep';
|
|
349
|
+
module: string;
|
|
350
|
+
slug: string;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Reference to a view declared in the module's `manifest.owns.views[]`.
|
|
354
|
+
* Lands with Phase 5 slice 5.4b (the SDK side of view querying). The
|
|
355
|
+
* backend's `executeView` resolves the view at runtime — AND-merges
|
|
356
|
+
* the view's static WHERE with the caller's, applies the view's
|
|
357
|
+
* default orderBy when caller omits one, and recurses through
|
|
358
|
+
* `execute()` with a rewritten own-table AST.
|
|
359
|
+
*
|
|
360
|
+
* Authors typically reach this through the codegen-emitted
|
|
361
|
+
* `qb.from(qbViews.active_tasks.ref)` shape (or the more ergonomic
|
|
362
|
+
* `qbViews.active_tasks.where(...).execute()` directly), but the
|
|
363
|
+
* literal form `qb.from({ kind: 'view', slug: 'active_tasks' })`
|
|
364
|
+
* is also supported for tests + AI-builder authoring.
|
|
365
|
+
*/
|
|
366
|
+
interface QbViewRef {
|
|
367
|
+
kind: 'view';
|
|
368
|
+
slug: string;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* The factory's return value. Wraps a {@link Client} + exposes a
|
|
372
|
+
* `.from()` method that binds a table source.
|
|
373
|
+
*
|
|
374
|
+
* `from()` accepts three shapes:
|
|
375
|
+
* - a string — own-table slug (default, since Phase 2 slice 1)
|
|
376
|
+
* - a {@link QbDepRef} — cross-module dep ref (Phase 4 slice 4.1b)
|
|
377
|
+
* - a {@link QbViewRef} — view ref (Phase 5 slice 5.4b)
|
|
378
|
+
*
|
|
379
|
+
* A `.raw()` escape hatch is intentionally NOT in v1 per the plan.
|
|
380
|
+
*/
|
|
381
|
+
interface Qb {
|
|
382
|
+
/** Bind an owned-table source. Returns a chainable {@link Query}. */
|
|
383
|
+
from<Row extends BaseRow = BaseRow>(tableSlug: string): Query<Row>;
|
|
384
|
+
/** Bind a cross-module dep table source. Returns a chainable {@link Query}. */
|
|
385
|
+
from<Row extends BaseRow = BaseRow>(ref: QbDepRef): Query<Row>;
|
|
386
|
+
/** Bind a view source (declared in `manifest.owns.views[]`). */
|
|
387
|
+
from<Row extends BaseRow = BaseRow>(ref: QbViewRef): Query<Row>;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Chainable per-query builder. Each instance is **immutable** — every
|
|
391
|
+
* modifier returns a fresh `Query<Row>` with the updated AST.
|
|
392
|
+
*
|
|
393
|
+
* Today's slice exposes `.toAst()` (introspection), `.execute()`
|
|
394
|
+
* (run + return rows), plus four chainable modifiers (`.where`,
|
|
395
|
+
* `.orderBy`, `.limit`, `.offset`). Subsequent slices add `.select`,
|
|
396
|
+
* `.count`, `.groupBy()`/`.agg()`, then `.join()` / `.leftJoin()`.
|
|
397
|
+
*
|
|
398
|
+
* Each chainable method returns a new `Query<Row>` rather than `this`
|
|
399
|
+
* so authors can derive sub-queries from a common base:
|
|
400
|
+
*
|
|
401
|
+
* ```ts
|
|
402
|
+
* const base = qb.from<TaskRow>('tasks');
|
|
403
|
+
* const open = await base.where({ done: false }).execute();
|
|
404
|
+
* const overdue = await base
|
|
405
|
+
* .where({ done: false, due_date: { date_before: today } })
|
|
406
|
+
* .execute();
|
|
407
|
+
* ```
|
|
408
|
+
*
|
|
409
|
+
* The first `base.execute()` returns ALL tasks (no filter); the
|
|
410
|
+
* `where()` calls produce fresh builders that don't mutate `base`.
|
|
411
|
+
*/
|
|
412
|
+
interface Query<Row extends BaseRow = BaseRow> {
|
|
413
|
+
/**
|
|
414
|
+
* Return the underlying AST. Useful for tests, debugging, future
|
|
415
|
+
* `.explain()` integration. The returned object is a fresh copy —
|
|
416
|
+
* mutating it does NOT affect the builder.
|
|
417
|
+
*/
|
|
418
|
+
toAst(): QueryAst;
|
|
419
|
+
/**
|
|
420
|
+
* Execute the query. Posts the AST to the backend's
|
|
421
|
+
* `POST /api/modules/<installation>/query` endpoint and resolves
|
|
422
|
+
* with the standard {@link Page} envelope. Rejects with a
|
|
423
|
+
* {@link DashwiseError} on backend errors (`QUERY_AST_VERSION`,
|
|
424
|
+
* `QUERY_TOO_COMPLEX`, etc.).
|
|
425
|
+
*/
|
|
426
|
+
execute(): Promise<Page<Row>>;
|
|
427
|
+
/**
|
|
428
|
+
* Phase 6 streaming (2026-05-19): stream query results row-by-row
|
|
429
|
+
* via Server-Sent Events. Returns an `AsyncIterable<Row>` — use
|
|
430
|
+
* `for await (const row of q.stream())` to consume.
|
|
431
|
+
*
|
|
432
|
+
* Backend endpoint: `POST /api/modules/<installation>/query/stream`.
|
|
433
|
+
* Each row arrives as one SSE `data:` event; a terminal `complete`
|
|
434
|
+
* event closes the iterator cleanly. Execution-phase backend
|
|
435
|
+
* errors (RBAC, RESULT_TOO_LARGE, etc.) arrive as in-band `error`
|
|
436
|
+
* events and are rethrown as `DashwiseError` from the iterator.
|
|
437
|
+
* Validation-phase errors (malformed AST) come back as standard
|
|
438
|
+
* HTTP 400 BEFORE the stream opens, so they throw immediately
|
|
439
|
+
* when you start iterating (before the first row).
|
|
440
|
+
*
|
|
441
|
+
* **AbortSignal**: pass `opts.signal` to cancel the stream. When
|
|
442
|
+
* the signal fires:
|
|
443
|
+
* - The underlying `fetch()` is cancelled (network read aborts)
|
|
444
|
+
* - The iterator throws an `AbortError` on the next `next()` call
|
|
445
|
+
* - The backend's SSE writer eventually notices the broken
|
|
446
|
+
* pipe and stops emitting events (no leak)
|
|
447
|
+
*
|
|
448
|
+
* **Backpressure**: the iterator is pull-based. Rows are buffered
|
|
449
|
+
* in the SSE chunk decoder until the consumer requests them via
|
|
450
|
+
* the for-await loop. Slow consumers naturally backpressure the
|
|
451
|
+
* network read.
|
|
452
|
+
*
|
|
453
|
+
* **MVP limitation**: in v1 the BACKEND buffers the full result
|
|
454
|
+
* before streaming (true row-by-row Postgres cursoring is a future
|
|
455
|
+
* perf slice). The SDK consumer still gets the streaming UX — rows
|
|
456
|
+
* arrive incrementally on the wire. Once the backend perf slice
|
|
457
|
+
* lands, both ends become streaming end-to-end with no SDK change.
|
|
458
|
+
*
|
|
459
|
+
* @example Basic iteration
|
|
460
|
+
* for await (const task of qb.from<TaskRow>('tasks').stream()) {
|
|
461
|
+
* process(task);
|
|
462
|
+
* }
|
|
463
|
+
*
|
|
464
|
+
* @example Abort midway via AbortController
|
|
465
|
+
* const controller = new AbortController();
|
|
466
|
+
* setTimeout(() => controller.abort(), 5000);
|
|
467
|
+
* try {
|
|
468
|
+
* for await (const row of qb.from('tasks').stream({ signal: controller.signal })) {
|
|
469
|
+
* // ... rows arrive until 5s timeout
|
|
470
|
+
* }
|
|
471
|
+
* } catch (e) {
|
|
472
|
+
* if (e.name === 'AbortError') console.log('Cancelled');
|
|
473
|
+
* }
|
|
474
|
+
*/
|
|
475
|
+
stream(opts?: {
|
|
476
|
+
signal?: AbortSignal;
|
|
477
|
+
}): AsyncIterable<Row>;
|
|
478
|
+
/**
|
|
479
|
+
* Return the Postgres execution plan for this query — WITHOUT
|
|
480
|
+
* executing it (Phase 5, 2026-05-19). Posts the AST to
|
|
481
|
+
* `POST /api/modules/<installation>/query/explain`; backend wraps the
|
|
482
|
+
* SQL in `EXPLAIN (FORMAT JSON)` and returns the parsed plan tree.
|
|
483
|
+
*
|
|
484
|
+
* **MVP support matrix** (backend slice 5):
|
|
485
|
+
* - ✅ Joined queries — works (`.join()` / `.leftJoin()`)
|
|
486
|
+
* - ✅ Joined aggregations — works (`.join()...agg()`)
|
|
487
|
+
* - ⏳ Bare own-table / bare dep / view / bare-agg → rejects with
|
|
488
|
+
* `EXPLAIN_NOT_SUPPORTED_YET` (typed code; the error's `context`
|
|
489
|
+
* payload lets you fall back to `.execute()` for unsupported
|
|
490
|
+
* shapes)
|
|
491
|
+
*
|
|
492
|
+
* @example Joined query EXPLAIN (works today)
|
|
493
|
+
* const plan = await qb.from<TaskRow>('tasks')
|
|
494
|
+
* .leftJoin<UserRow>('users', { on: { 'tasks.assignee_id': 'users.id' } })
|
|
495
|
+
* .where({ done: false })
|
|
496
|
+
* .explain();
|
|
497
|
+
* console.log('Cost:', plan.estimatedCost);
|
|
498
|
+
* for (const node of plan.nodes) {
|
|
499
|
+
* console.log(' '.repeat(node.depth) + node['Node Type']);
|
|
500
|
+
* }
|
|
501
|
+
*
|
|
502
|
+
* @example Bare query EXPLAIN (currently throws)
|
|
503
|
+
* try {
|
|
504
|
+
* await qb.from<TaskRow>('tasks').explain();
|
|
505
|
+
* } catch (e) {
|
|
506
|
+
* // e.code === 'EXPLAIN_NOT_SUPPORTED_YET'
|
|
507
|
+
* // Fall back to .execute() — bare-path support lands in a follow-up
|
|
508
|
+
* }
|
|
509
|
+
*/
|
|
510
|
+
explain(): Promise<ExplainResult>;
|
|
511
|
+
/**
|
|
512
|
+
* Add or replace the where clause. Same recursive `Where<Row>`
|
|
513
|
+
* shape as `client.db().list({ filter })` — top-level keys AND
|
|
514
|
+
* together; explicit `AND` / `OR` arrays group:
|
|
515
|
+
*
|
|
516
|
+
* ```ts
|
|
517
|
+
* qb.from<TaskRow>('tasks').where({ done: false })
|
|
518
|
+
* qb.from<TaskRow>('tasks').where({ title: { contains: 'urgent' } })
|
|
519
|
+
* qb.from<TaskRow>('tasks').where({
|
|
520
|
+
* OR: [{ priority: 'high' }, { due_date: { date_before: today } }]
|
|
521
|
+
* })
|
|
522
|
+
* ```
|
|
523
|
+
*
|
|
524
|
+
* Calling `.where()` twice REPLACES the prior clause (not merge) —
|
|
525
|
+
* authors who want incremental composition should build their
|
|
526
|
+
* where-clause object explicitly. Replace-vs-merge is the same
|
|
527
|
+
* semantic as `client.db().list({ filter: ... })`.
|
|
528
|
+
*/
|
|
529
|
+
where(clause: Where<Row>): Query<Row>;
|
|
530
|
+
/**
|
|
531
|
+
* Add or replace the order-by clauses. Array of `{ field, dir }`
|
|
532
|
+
* entries; sorts apply in array order (primary first).
|
|
533
|
+
*
|
|
534
|
+
* ```ts
|
|
535
|
+
* qb.from<TaskRow>('tasks').orderBy([
|
|
536
|
+
* { field: 'priority', dir: 'desc' },
|
|
537
|
+
* { field: 'due_date', dir: 'asc' },
|
|
538
|
+
* ])
|
|
539
|
+
* ```
|
|
540
|
+
*/
|
|
541
|
+
orderBy(clauses: ReadonlyArray<OrderClause>): Query<Row>;
|
|
542
|
+
/**
|
|
543
|
+
* Set the maximum number of rows to return. Server defaults to 50
|
|
544
|
+
* when omitted; max 1000 (`RESULT_TOO_LARGE` for higher values).
|
|
545
|
+
*/
|
|
546
|
+
limit(n: number): Query<Row>;
|
|
547
|
+
/**
|
|
548
|
+
* Set the row offset for pagination. Defaults to 0.
|
|
549
|
+
*/
|
|
550
|
+
offset(n: number): Query<Row>;
|
|
551
|
+
/**
|
|
552
|
+
* Convenience: execute the query and return only the total row
|
|
553
|
+
* count (after `where`, before `limit`/`offset`).
|
|
554
|
+
*
|
|
555
|
+
* Equivalent to `(await this.limit(1).execute()).total` but with
|
|
556
|
+
* a clearer call site. The backend's existing list endpoint
|
|
557
|
+
* already computes `total` regardless of `limit`, so this hits
|
|
558
|
+
* the same path as `.execute()` and just extracts the number —
|
|
559
|
+
* no separate `COUNT(*)` round-trip.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* const open = await qb.from<TaskRow>('tasks')
|
|
563
|
+
* .where({ done: false })
|
|
564
|
+
* .count();
|
|
565
|
+
*/
|
|
566
|
+
count(): Promise<number>;
|
|
567
|
+
/**
|
|
568
|
+
* Add a same-module join (Phase 3 slice 3.3, 2026-05-19). Either
|
|
569
|
+
* INNER (when `opts.kind` is omitted or 'inner') or LEFT (use
|
|
570
|
+
* `.leftJoin()` for the read-friendly alias).
|
|
571
|
+
*
|
|
572
|
+
* `opts.on` is REQUIRED in this slice — link-field-derived `on`
|
|
573
|
+
* inference lands in a follow-up. Pass qualified refs on both
|
|
574
|
+
* sides: `{ 'tasks.assignee_id': 'users.id' }`.
|
|
575
|
+
*
|
|
576
|
+
* Returns a {@link JoinedQuery} — the surface omits write methods
|
|
577
|
+
* (`.create / .update / .delete`) + `.count` / `.agg` / `.groupBy`
|
|
578
|
+
* since the query plane's join path is read-only and not yet
|
|
579
|
+
* aggregation-aware (slice 3.2b-1 limits).
|
|
580
|
+
*
|
|
581
|
+
* Today the validator gates joins+filters+ordering as `QUERY_TOO_COMPLEX`
|
|
582
|
+
* pending slices 3.2b-2 / b-3. The SDK lets you write the call
|
|
583
|
+
* anyway so author muscle memory transfers; the backend will
|
|
584
|
+
* surface a clear "lands in slice X" error if combined too early.
|
|
585
|
+
*
|
|
586
|
+
* @example Own-table join
|
|
587
|
+
* await qb.tasks
|
|
588
|
+
* .join<UsersRow>('users', { on: { 'tasks.assignee_id': 'users.id' } })
|
|
589
|
+
* .execute();
|
|
590
|
+
*
|
|
591
|
+
* @example Cross-module join (Phase 4 slice 4.2)
|
|
592
|
+
* await qb.tasks
|
|
593
|
+
* .leftJoin<ContactRow>(
|
|
594
|
+
* { kind: 'dep', module: 'crm', slug: 'contacts' },
|
|
595
|
+
* { on: { 'tasks.contact_id': 'contacts.id' } },
|
|
596
|
+
* )
|
|
597
|
+
* .execute();
|
|
598
|
+
*/
|
|
599
|
+
join<T = unknown>(target: string | QbDepRef, opts: JoinOpts<'inner' | 'left'>): JoinedQuery<Row & Record<string, T>>;
|
|
600
|
+
/**
|
|
601
|
+
* Add a LEFT JOIN. Convenience for `.join(target, { kind: 'left', on })`.
|
|
602
|
+
* The joined object on result rows is `T | null` (matches LEFT JOIN's
|
|
603
|
+
* unmatched-row semantics).
|
|
604
|
+
*
|
|
605
|
+
* Phase 4 slice 4.2: accepts {@link QbDepRef} as the target for
|
|
606
|
+
* cross-module joins. The provider's `exposes[].operations.list`
|
|
607
|
+
* must be enabled and the consumer must declare the dep table in
|
|
608
|
+
* `dependencies[].reads[]` — same contract enforcement as the bare
|
|
609
|
+
* dep list path.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* await qb.tasks
|
|
613
|
+
* .leftJoin<UsersRow>('users', { on: { 'tasks.assignee_id': 'users.id' } })
|
|
614
|
+
* .execute();
|
|
615
|
+
*/
|
|
616
|
+
leftJoin<T = unknown>(target: string | QbDepRef, opts: JoinOpts<'left'>): JoinedQuery<Row & Record<string, T | null>>;
|
|
617
|
+
/**
|
|
618
|
+
* Narrow the response to only the listed field slugs. The manifest's
|
|
619
|
+
* policy projection is the security floor — `.select()` can narrow
|
|
620
|
+
* further but never widens. Slugs not in the projection (because the
|
|
621
|
+
* manifest hides them, or the slug is a typo) reject server-side with
|
|
622
|
+
* `FIELD_NOT_READABLE`.
|
|
623
|
+
*
|
|
624
|
+
* Pass `['*']` (or omit `.select()` entirely) for the full projection.
|
|
625
|
+
*
|
|
626
|
+
* **Type narrowing not yet implemented:** the returned Query is still
|
|
627
|
+
* typed as `Query<Row>` (full row type). Authors who want narrowed
|
|
628
|
+
* types cast at the call site:
|
|
629
|
+
* ```ts
|
|
630
|
+
* const result = await qb.tasks
|
|
631
|
+
* .select(['id', 'title'])
|
|
632
|
+
* .execute() as unknown as Page<Pick<TasksRow, 'id' | 'title'>>;
|
|
633
|
+
* ```
|
|
634
|
+
* A future TS exercise (Pick-based variadic narrowing) lights up
|
|
635
|
+
* inferred types without the cast.
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* await qb.tasks.where({ done: false }).select(['id', 'title']).execute();
|
|
639
|
+
*/
|
|
640
|
+
select(fields: ReadonlyArray<string>): Query<Row>;
|
|
641
|
+
/**
|
|
642
|
+
* Group rows by one or more fields. Required precursor to `.agg()`
|
|
643
|
+
* when aggregating per-group; without `.groupBy()` the aggregation
|
|
644
|
+
* runs against the whole filtered set and returns one row.
|
|
645
|
+
*
|
|
646
|
+
* Transitions the builder to {@link AggQuery} so subsequent calls
|
|
647
|
+
* have access to `.agg()` + the aggregation-shaped `.execute()`.
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* qb.from<TaskRow>('tasks')
|
|
651
|
+
* .where({ done: false })
|
|
652
|
+
* .groupBy(['priority', 'assignee_id'])
|
|
653
|
+
* .agg({ total: { count: '*' } })
|
|
654
|
+
* .execute()
|
|
655
|
+
*/
|
|
656
|
+
groupBy(fields: ReadonlyArray<FieldRef>): AggQuery;
|
|
657
|
+
/**
|
|
658
|
+
* Set the aggregation expressions. Keys are output column names in
|
|
659
|
+
* the result rows; values are aggregation function specs
|
|
660
|
+
* (`{ count: '*' }`, `{ sum: 'amount' }`, etc.). At least one
|
|
661
|
+
* expression is required; calling `.agg({})` rejects.
|
|
662
|
+
*
|
|
663
|
+
* Without a preceding `.groupBy()`, returns a single row with the
|
|
664
|
+
* aggregations applied to the entire filtered set. With `.groupBy()`,
|
|
665
|
+
* returns one row per group.
|
|
666
|
+
*
|
|
667
|
+
* Transitions the builder to {@link AggQuery} — `.execute()` on
|
|
668
|
+
* the returned builder yields {@link QueryResult} (not `Page<Row>`).
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* const stats = await qb.from<TaskRow>('tasks')
|
|
672
|
+
* .where({ done: false })
|
|
673
|
+
* .agg({ total: { count: '*' }, oldest: { min: 'created_at' } })
|
|
674
|
+
* .execute();
|
|
675
|
+
* // stats.rows[0] = { total: 7, oldest: '2026-01-15' }
|
|
676
|
+
*/
|
|
677
|
+
agg(expressions: Record<string, AggExpr>): AggQuery;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Chainable per-query builder after `.join()` / `.leftJoin()` has
|
|
681
|
+
* been called. Same kind of immutable shape as {@link Query} —
|
|
682
|
+
* every modifier returns a fresh instance.
|
|
683
|
+
*
|
|
684
|
+
* Surface intentionally NARROWER than Query<Row>:
|
|
685
|
+
* - Omits `.create / .update / .delete` — joined queries are
|
|
686
|
+
* read-only (writes always go through `client.db()`).
|
|
687
|
+
* - Omits `.count / .agg / .groupBy` — slice 3.2b-1 doesn't yet
|
|
688
|
+
* handle aggregation over joined tables (Phase 5+ work).
|
|
689
|
+
*
|
|
690
|
+
* Today's available surface:
|
|
691
|
+
* - `.where` — bare-ref + qualified-ref WHERE both work as of
|
|
692
|
+
* Phase 3 slice 3.2b-2-full (SQL-side alias-aware WHERE)
|
|
693
|
+
* - `.orderBy` — bare + qualified-ref sort (slice 3.2b-3)
|
|
694
|
+
* - `.limit`, `.offset` — in-memory pagination over post-WHERE
|
|
695
|
+
* rows, capped at 10k (slice 3.2b-3)
|
|
696
|
+
* - `.join`, `.leftJoin` — stack additional joins up to 2 total
|
|
697
|
+
* in v1 (Phase 4 slice 4.3; validator rejects 3+ with a clear
|
|
698
|
+
* QUERY_TOO_COMPLEX). Cross-module dep refs accepted as targets
|
|
699
|
+
* since slice 4.2.
|
|
700
|
+
* - `.flatten()` — switches to SQL-style flat result rows (slice
|
|
701
|
+
* 3.5; backend wired since slice 3.5)
|
|
702
|
+
* - `.toAst()`, `.execute()` — same as Query<Row>.
|
|
703
|
+
*/
|
|
704
|
+
interface JoinedQuery<Row extends BaseRow = BaseRow> {
|
|
705
|
+
toAst(): QueryAst;
|
|
706
|
+
/** Resolves with `Page<Row>` shape — rows carry the nested joined data. */
|
|
707
|
+
execute(): Promise<Page<Row>>;
|
|
708
|
+
/**
|
|
709
|
+
* Phase 6 streaming (2026-05-19): stream joined-row results
|
|
710
|
+
* row-by-row via SSE. Same contract as {@link Query.stream} — see
|
|
711
|
+
* its JSDoc for the full doc. Joined queries stream their nested-
|
|
712
|
+
* row shape (each yielded row has the joined object under the
|
|
713
|
+
* alias key, same as `.execute()` returns).
|
|
714
|
+
*/
|
|
715
|
+
stream(opts?: {
|
|
716
|
+
signal?: AbortSignal;
|
|
717
|
+
}): AsyncIterable<Row>;
|
|
718
|
+
/**
|
|
719
|
+
* Return Postgres EXPLAIN (FORMAT JSON) for this joined query —
|
|
720
|
+
* WITHOUT executing it. The joined path is the canonical supported
|
|
721
|
+
* shape for `.explain()` in MVP (Phase 5, 2026-05-19) because the
|
|
722
|
+
* join translator emits clean parameterized SQL the backend can wrap
|
|
723
|
+
* with EXPLAIN directly.
|
|
724
|
+
*
|
|
725
|
+
* Returns the same {@link ExplainResult} shape as `Query.explain`.
|
|
726
|
+
* See `Query.explain` JSDoc for the full call shape + examples.
|
|
727
|
+
*/
|
|
728
|
+
explain(): Promise<ExplainResult>;
|
|
729
|
+
where(clause: Where<Row>): JoinedQuery<Row>;
|
|
730
|
+
orderBy(clauses: ReadonlyArray<OrderClause>): JoinedQuery<Row>;
|
|
731
|
+
limit(n: number): JoinedQuery<Row>;
|
|
732
|
+
offset(n: number): JoinedQuery<Row>;
|
|
733
|
+
/**
|
|
734
|
+
* Add another join. Capped at 2 total in v1 (validator).
|
|
735
|
+
* Phase 4 slice 4.2: accepts {@link QbDepRef} as the target for
|
|
736
|
+
* cross-module joins (same overload semantics as the entry join).
|
|
737
|
+
*/
|
|
738
|
+
join<T = unknown>(target: string | QbDepRef, opts: JoinOpts<'inner' | 'left'>): JoinedQuery<Row & Record<string, T>>;
|
|
739
|
+
leftJoin<T = unknown>(target: string | QbDepRef, opts: JoinOpts<'left'>): JoinedQuery<Row & Record<string, T | null>>;
|
|
740
|
+
/**
|
|
741
|
+
* Switch to flat-row result shape (`{ 'users.email': ... }`)
|
|
742
|
+
* instead of the nested default. Slice 3.5 wires the backend; the
|
|
743
|
+
* SDK exposes it today + sends the AST flag.
|
|
744
|
+
*/
|
|
745
|
+
flatten(): JoinedQuery<Record<string, unknown> & BaseRow>;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Aggregation-mode query. Reachable from {@link Query.agg} or
|
|
749
|
+
* {@link Query.groupBy}. Exposes the same `where` / `limit` / `offset`
|
|
750
|
+
* surface as the non-agg `Query` (you can still filter + limit groups)
|
|
751
|
+
* but `.execute()` returns {@link QueryResult} instead of `Page<Row>`.
|
|
752
|
+
*
|
|
753
|
+
* Doesn't expose `.create` / `.update` / `.delete` (aggregations are
|
|
754
|
+
* read-only by definition) or `.count()` (use `.agg({ n: { count: '*' } })`
|
|
755
|
+
* directly + read `result.rows[0].n`).
|
|
756
|
+
*/
|
|
757
|
+
interface AggQuery {
|
|
758
|
+
toAst(): QueryAst;
|
|
759
|
+
execute(): Promise<QueryResult>;
|
|
760
|
+
/**
|
|
761
|
+
* Return Postgres EXPLAIN (FORMAT JSON) for the underlying SELECT
|
|
762
|
+
* this aggregation builds. Joined aggregations (where the builder
|
|
763
|
+
* arrived here via `.join().agg()`) work in MVP — the source SELECT
|
|
764
|
+
* dominates cost; the in-memory GROUP BY post-runs above it. Bare
|
|
765
|
+
* own-table aggregations currently reject with `EXPLAIN_NOT_SUPPORTED_YET`
|
|
766
|
+
* (lifts when bare-path support lands; see {@link Query.explain}).
|
|
767
|
+
*/
|
|
768
|
+
explain(): Promise<ExplainResult>;
|
|
769
|
+
where(clause: Record<string, unknown>): AggQuery;
|
|
770
|
+
/** Refine the group-by fields (replace, not merge). */
|
|
771
|
+
groupBy(fields: ReadonlyArray<FieldRef>): AggQuery;
|
|
772
|
+
/** Refine the aggregation expressions (replace, not merge). */
|
|
773
|
+
agg(expressions: Record<string, AggExpr>): AggQuery;
|
|
774
|
+
limit(n: number): AggQuery;
|
|
775
|
+
offset(n: number): AggQuery;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Build a {@link Qb} bound to `client`. The same `qb` instance can
|
|
779
|
+
* be reused across concurrent calls — every call constructs a fresh
|
|
780
|
+
* Query, and the underlying client is already thread-safe.
|
|
781
|
+
*
|
|
782
|
+
* @example
|
|
783
|
+
* import { createClient } from '@dashai/sdk';
|
|
784
|
+
* import { makeQb } from '@dashai/sdk/query';
|
|
785
|
+
*
|
|
786
|
+
* const client = createClient({ baseUrl, installationId, getToken });
|
|
787
|
+
* const qb = makeQb(client);
|
|
788
|
+
*
|
|
789
|
+
* const page = await qb.from<TaskRow>('tasks').execute();
|
|
790
|
+
* console.log(page.rows);
|
|
791
|
+
*
|
|
792
|
+
* The generated SDK (`@dashai/generated/qb`) will eventually export
|
|
793
|
+
* a typed singleton wrapping the default client — `import { qb } from
|
|
794
|
+
* '@dashai/generated'; qb.tasks.execute()`. That layer lands once
|
|
795
|
+
* codegen is updated; until then, `makeQb(client).from('tasks')` is
|
|
796
|
+
* the path.
|
|
797
|
+
*/
|
|
798
|
+
declare function makeQb(client: Client): Qb;
|
|
799
|
+
|
|
800
|
+
export { type AggExpr, type AggQuery, CURRENT_QUERY_AST_VERSION, type ExplainResult, type FieldRef, type JoinNode, type JoinOpts, type JoinedQuery, type OrderClause, type Qb, type QbDepRef, type QbViewRef, type Query, type QueryAst, type QueryAstVersion, type QueryResult, type SelectExpr, type TableRef, type WhereClause, isQueryAst, makeQb };
|