@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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/dist/auth/client.cjs +185 -0
  4. package/dist/auth/client.cjs.map +1 -0
  5. package/dist/auth/client.d.cts +137 -0
  6. package/dist/auth/client.d.ts +137 -0
  7. package/dist/auth/client.js +175 -0
  8. package/dist/auth/client.js.map +1 -0
  9. package/dist/auth/middleware.cjs +352 -0
  10. package/dist/auth/middleware.cjs.map +1 -0
  11. package/dist/auth/middleware.d.cts +90 -0
  12. package/dist/auth/middleware.d.ts +90 -0
  13. package/dist/auth/middleware.js +343 -0
  14. package/dist/auth/middleware.js.map +1 -0
  15. package/dist/auth/server.cjs +445 -0
  16. package/dist/auth/server.cjs.map +1 -0
  17. package/dist/auth/server.d.cts +170 -0
  18. package/dist/auth/server.d.ts +170 -0
  19. package/dist/auth/server.js +432 -0
  20. package/dist/auth/server.js.map +1 -0
  21. package/dist/auth/types.cjs +31 -0
  22. package/dist/auth/types.cjs.map +1 -0
  23. package/dist/auth/types.d.cts +163 -0
  24. package/dist/auth/types.d.ts +163 -0
  25. package/dist/auth/types.js +29 -0
  26. package/dist/auth/types.js.map +1 -0
  27. package/dist/deps/index.cjs +117 -0
  28. package/dist/deps/index.cjs.map +1 -0
  29. package/dist/deps/index.d.cts +93 -0
  30. package/dist/deps/index.d.ts +93 -0
  31. package/dist/deps/index.js +112 -0
  32. package/dist/deps/index.js.map +1 -0
  33. package/dist/errors-BV75u7b9.d.cts +207 -0
  34. package/dist/errors-BV75u7b9.d.ts +207 -0
  35. package/dist/index.cjs +721 -0
  36. package/dist/index.cjs.map +1 -0
  37. package/dist/index.d.cts +171 -0
  38. package/dist/index.d.ts +171 -0
  39. package/dist/index.js +703 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/query/index.cjs +621 -0
  42. package/dist/query/index.cjs.map +1 -0
  43. package/dist/query/index.d.cts +800 -0
  44. package/dist/query/index.d.ts +800 -0
  45. package/dist/query/index.js +617 -0
  46. package/dist/query/index.js.map +1 -0
  47. package/dist/react/index.cjs +199 -0
  48. package/dist/react/index.cjs.map +1 -0
  49. package/dist/react/index.d.cts +117 -0
  50. package/dist/react/index.d.ts +117 -0
  51. package/dist/react/index.js +195 -0
  52. package/dist/react/index.js.map +1 -0
  53. package/dist/types-BLNQ1S1C.d.cts +396 -0
  54. package/dist/types-BLNQ1S1C.d.ts +396 -0
  55. 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 };