@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,617 @@
1
+ // src/data/errors.ts
2
+ var DashwiseErrorCode = {
3
+ // Auth / authz
4
+ UNAUTHENTICATED: "UNAUTHENTICATED",
5
+ FORBIDDEN: "FORBIDDEN",
6
+ TOKEN_EXPIRED: "TOKEN_EXPIRED",
7
+ // Contract enforcement
8
+ CONTRACT_VIOLATION: "CONTRACT_VIOLATION",
9
+ FIELD_NOT_READABLE: "FIELD_NOT_READABLE",
10
+ FILTER_OP_UNKNOWN: "FILTER_OP_UNKNOWN",
11
+ OPERATION_NOT_ALLOWED: "OPERATION_NOT_ALLOWED",
12
+ // Cross-module dependencies (Phase 1 of the SDK query plane —
13
+ // see dashwise-devops-docs/plans/modules-sdk-query-v1.md).
14
+ // Surfaced when reading borrowed tables via `client.deps(slug)`.
15
+ DEPENDENCY_NOT_DECLARED: "DEPENDENCY_NOT_DECLARED",
16
+ TABLE_NOT_IN_CONTRACT: "TABLE_NOT_IN_CONTRACT",
17
+ PROVIDER_TABLE_MISSING: "PROVIDER_TABLE_MISSING",
18
+ OPERATION_NOT_ALLOWED_BY_PROVIDER: "OPERATION_NOT_ALLOWED_BY_PROVIDER",
19
+ // Query-plane joins (Phase 3 — see
20
+ // dashwise-devops-docs/plans/modules-sdk-query-v1-p3-execution.md).
21
+ // Surfaced when `.join('<slug>')` / `.leftJoin('<slug>')` can't
22
+ // resolve the target. Pre-declared in slice 3.1 so slice 3.2's
23
+ // backend translator can emit them through fromBackendEnvelope
24
+ // without an SDK roundtrip.
25
+ LINK_FIELD_NOT_FOUND: "LINK_FIELD_NOT_FOUND",
26
+ NOT_A_LINK_FIELD: "NOT_A_LINK_FIELD",
27
+ JOIN_NOT_LINKED: "JOIN_NOT_LINKED",
28
+ // Cross-module actions (Phase 7 slice 7.3a — see
29
+ // dashwise-devops-docs/plans/modules-sdk-query-v1-p7-cross-module-writes.md).
30
+ // Surfaced when `qb.deps.<dep>.actions.<name>(input)` (generated
31
+ // factory) dispatches against the new backend endpoint
32
+ // `POST :installationId/deps/:providerSlug/actions/:actionName`.
33
+ // Slice 7.2b ships the backend emitter; slice 7.3a (this slice)
34
+ // reserves the codes + maps them to DepActionError in the SDK.
35
+ ACTION_NOT_EXPOSED: "ACTION_NOT_EXPOSED",
36
+ ACTION_NOT_DECLARED: "ACTION_NOT_DECLARED",
37
+ ACTION_INPUT_INVALID: "ACTION_INPUT_INVALID",
38
+ ACTION_OUTPUT_INVALID: "ACTION_OUTPUT_INVALID",
39
+ ACTION_RATE_LIMITED: "ACTION_RATE_LIMITED",
40
+ ACTION_EXECUTION_FAILED: "ACTION_EXECUTION_FAILED",
41
+ // Data plane
42
+ ROW_NOT_FOUND: "ROW_NOT_FOUND",
43
+ VALIDATION_FAILED: "VALIDATION_FAILED",
44
+ // Server
45
+ INTERNAL_ERROR: "INTERNAL_ERROR"
46
+ };
47
+ var DashwiseError = class extends Error {
48
+ code;
49
+ status;
50
+ retriable;
51
+ context;
52
+ /**
53
+ * Backend-issued trace anchor (Phase 6 slice 6.2, 2026-05-19).
54
+ * Mirrors the `x-request-id` response header. Populated whenever
55
+ * the backend's `RuntimeQueryController` (or any future endpoint
56
+ * that echoes the header) is the origin of this error. `null` when
57
+ * the failure is client-side (NetworkError / TimeoutError — the
58
+ * request never reached the server, so there's no server-issued
59
+ * ID to surface).
60
+ *
61
+ * Use this as a debugging trace anchor when filing a bug against a
62
+ * specific failed query — the matching row in `module_query_log`
63
+ * carries the full backend context (ast_hash, error_code,
64
+ * duration_ms, etc.).
65
+ */
66
+ requestId;
67
+ constructor(code, message, options = {}) {
68
+ super(message);
69
+ this.name = this.constructor.name;
70
+ this.code = code;
71
+ this.status = options.status ?? 0;
72
+ this.retriable = options.retriable ?? false;
73
+ this.context = options.context;
74
+ this.requestId = options.requestId ?? null;
75
+ if (options.cause !== void 0) {
76
+ this.cause = options.cause;
77
+ }
78
+ }
79
+ };
80
+ var UnauthenticatedError = class extends DashwiseError {
81
+ constructor(message = "Authentication required", requestId = null) {
82
+ super(DashwiseErrorCode.UNAUTHENTICATED, message, { status: 401, requestId });
83
+ }
84
+ };
85
+ var ForbiddenError = class extends DashwiseError {
86
+ constructor(message, context, requestId = null) {
87
+ super(DashwiseErrorCode.FORBIDDEN, message, { status: 403, context, requestId });
88
+ }
89
+ };
90
+ var RowNotFoundError = class extends DashwiseError {
91
+ constructor(message, context, requestId = null) {
92
+ super(DashwiseErrorCode.ROW_NOT_FOUND, message, { status: 404, context, requestId });
93
+ }
94
+ };
95
+ var ContractViolationError = class extends DashwiseError {
96
+ constructor(message, context, requestId = null) {
97
+ super(DashwiseErrorCode.CONTRACT_VIOLATION, message, {
98
+ status: 403,
99
+ context,
100
+ requestId
101
+ });
102
+ }
103
+ };
104
+ var ValidationError = class extends DashwiseError {
105
+ constructor(message, context, requestId = null) {
106
+ super(DashwiseErrorCode.VALIDATION_FAILED, message, {
107
+ status: 400,
108
+ context,
109
+ requestId
110
+ });
111
+ }
112
+ };
113
+ var DependencyContractError = class extends DashwiseError {
114
+ constructor(code, message, context, requestId = null) {
115
+ super(code, message, { status: 404, context, requestId });
116
+ }
117
+ };
118
+ var ProviderTableMissingError = class extends DashwiseError {
119
+ constructor(message, context, requestId = null) {
120
+ super(DashwiseErrorCode.PROVIDER_TABLE_MISSING, message, {
121
+ status: 404,
122
+ context,
123
+ requestId
124
+ });
125
+ }
126
+ };
127
+ var OperationNotAllowedByProviderError = class extends DashwiseError {
128
+ constructor(message, context, requestId = null) {
129
+ super(DashwiseErrorCode.OPERATION_NOT_ALLOWED_BY_PROVIDER, message, {
130
+ status: 403,
131
+ context,
132
+ requestId
133
+ });
134
+ }
135
+ };
136
+ var JoinError = class extends DashwiseError {
137
+ constructor(code, message, context, requestId = null) {
138
+ super(code, message, { status: 400, context, requestId });
139
+ }
140
+ };
141
+ var DepActionError = class extends DashwiseError {
142
+ constructor(code, message, status, context, requestId = null) {
143
+ super(code, message, {
144
+ status,
145
+ retriable: code === DashwiseErrorCode.ACTION_RATE_LIMITED,
146
+ context,
147
+ requestId
148
+ });
149
+ }
150
+ };
151
+ function fromBackendEnvelope(status, envelope, requestId = null) {
152
+ const code = envelope?.code ?? codeFromStatus(status);
153
+ const message = envelope?.message ?? `HTTP ${status}`;
154
+ const context = envelope?.context;
155
+ switch (code) {
156
+ case DashwiseErrorCode.UNAUTHENTICATED:
157
+ case DashwiseErrorCode.TOKEN_EXPIRED:
158
+ return new UnauthenticatedError(message, requestId);
159
+ case DashwiseErrorCode.FORBIDDEN:
160
+ return new ForbiddenError(message, context, requestId);
161
+ case DashwiseErrorCode.ROW_NOT_FOUND:
162
+ return new RowNotFoundError(message, context, requestId);
163
+ case DashwiseErrorCode.CONTRACT_VIOLATION:
164
+ case DashwiseErrorCode.FIELD_NOT_READABLE:
165
+ case DashwiseErrorCode.FILTER_OP_UNKNOWN:
166
+ case DashwiseErrorCode.OPERATION_NOT_ALLOWED:
167
+ return new ContractViolationError(message, context, requestId);
168
+ case DashwiseErrorCode.VALIDATION_FAILED:
169
+ return new ValidationError(message, context, requestId);
170
+ case DashwiseErrorCode.DEPENDENCY_NOT_DECLARED:
171
+ case DashwiseErrorCode.TABLE_NOT_IN_CONTRACT:
172
+ return new DependencyContractError(code, message, context, requestId);
173
+ case DashwiseErrorCode.PROVIDER_TABLE_MISSING:
174
+ return new ProviderTableMissingError(message, context, requestId);
175
+ case DashwiseErrorCode.OPERATION_NOT_ALLOWED_BY_PROVIDER:
176
+ return new OperationNotAllowedByProviderError(message, context, requestId);
177
+ case DashwiseErrorCode.LINK_FIELD_NOT_FOUND:
178
+ case DashwiseErrorCode.NOT_A_LINK_FIELD:
179
+ case DashwiseErrorCode.JOIN_NOT_LINKED:
180
+ return new JoinError(code, message, context, requestId);
181
+ case DashwiseErrorCode.ACTION_NOT_EXPOSED:
182
+ case DashwiseErrorCode.ACTION_NOT_DECLARED:
183
+ return new DepActionError(code, message, 403, context, requestId);
184
+ case DashwiseErrorCode.ACTION_INPUT_INVALID:
185
+ return new DepActionError(code, message, 400, context, requestId);
186
+ case DashwiseErrorCode.ACTION_OUTPUT_INVALID:
187
+ return new DepActionError(code, message, 500, context, requestId);
188
+ case DashwiseErrorCode.ACTION_RATE_LIMITED:
189
+ return new DepActionError(code, message, 429, context, requestId);
190
+ case DashwiseErrorCode.ACTION_EXECUTION_FAILED:
191
+ return new DepActionError(code, message, 200, context, requestId);
192
+ default:
193
+ return new DashwiseError(code, message, {
194
+ status,
195
+ retriable: envelope?.retriable ?? false,
196
+ context,
197
+ requestId
198
+ });
199
+ }
200
+ }
201
+ function codeFromStatus(status) {
202
+ if (status === 401) return DashwiseErrorCode.UNAUTHENTICATED;
203
+ if (status === 403) return DashwiseErrorCode.FORBIDDEN;
204
+ if (status === 404) return DashwiseErrorCode.ROW_NOT_FOUND;
205
+ if (status >= 500) return DashwiseErrorCode.INTERNAL_ERROR;
206
+ return "HTTP_ERROR";
207
+ }
208
+
209
+ // src/query/ast.ts
210
+ function isQueryAst(v) {
211
+ if (!v || typeof v !== "object") return false;
212
+ const a = v;
213
+ if (a.v !== 1) return false;
214
+ if (!a.from || typeof a.from !== "object") return false;
215
+ const f = a.from;
216
+ if (typeof f.kind !== "string") return false;
217
+ if (f.kind === "owned" || f.kind === "view") {
218
+ return typeof f.slug === "string";
219
+ }
220
+ if (f.kind === "dep") {
221
+ return typeof f.module === "string" && typeof f.slug === "string";
222
+ }
223
+ return false;
224
+ }
225
+ var CURRENT_QUERY_AST_VERSION = 1;
226
+
227
+ // src/query/builder.ts
228
+ var TABLE_SLUG_REGEX = /^[a-z][a-z0-9_]*[a-z0-9]$/;
229
+ var MODULE_SLUG_REGEX = /^[a-z][a-z0-9-]*[a-z0-9]$/;
230
+ function assertValidTableSlug(slug) {
231
+ if (typeof slug !== "string" || !TABLE_SLUG_REGEX.test(slug)) {
232
+ throw new Error(
233
+ `@dashai/sdk/query: invalid table slug "${slug}". Slugs must match /^[a-z][a-z0-9_]*[a-z0-9]$/ (snake_case).`
234
+ );
235
+ }
236
+ }
237
+ function assertValidModuleSlug(slug) {
238
+ if (typeof slug !== "string" || !MODULE_SLUG_REGEX.test(slug)) {
239
+ throw new Error(
240
+ `@dashai/sdk/query: invalid module slug "${slug}". Module slugs must match /^[a-z][a-z0-9-]*[a-z0-9]$/ (kebab-case).`
241
+ );
242
+ }
243
+ }
244
+ function makeQb(client) {
245
+ return {
246
+ // Single runtime function backs all three overloads — TypeScript
247
+ // narrows at the call site, but at runtime we discriminate on the
248
+ // input shape:
249
+ // string → own-table slug
250
+ // { kind: 'dep', ... } → cross-module dep ref (slice 4.1b)
251
+ // { kind: 'view', slug } → view ref (slice 5.4b)
252
+ from(target) {
253
+ let ref;
254
+ if (typeof target === "string") {
255
+ assertValidTableSlug(target);
256
+ ref = { kind: "owned", slug: target };
257
+ } else if (target && typeof target === "object" && target.kind === "dep") {
258
+ assertValidModuleSlug(target.module);
259
+ assertValidTableSlug(target.slug);
260
+ ref = { kind: "dep", module: target.module, slug: target.slug };
261
+ } else if (target && typeof target === "object" && target.kind === "view") {
262
+ assertValidTableSlug(target.slug);
263
+ ref = { kind: "view", slug: target.slug };
264
+ } else {
265
+ throw new Error(
266
+ `@dashai/sdk/query: qb.from() expects a string table slug, { kind: 'dep', module, slug }, or { kind: 'view', slug }; got ${JSON.stringify(target)}.`
267
+ );
268
+ }
269
+ const ast = {
270
+ v: CURRENT_QUERY_AST_VERSION,
271
+ from: ref
272
+ };
273
+ return makeQuery(client, ast);
274
+ }
275
+ };
276
+ }
277
+ async function postExplain(client, ast) {
278
+ return client.request("POST", "/query/explain", { ast });
279
+ }
280
+ async function* streamRowsViaSSE(client, ast, signal) {
281
+ const res = await client.streamRaw("POST", "/query/stream", { ast }, signal);
282
+ const requestId = res.headers.get("x-request-id");
283
+ if (!res.ok) {
284
+ const envelope = await readErrorEnvelope(res);
285
+ throw fromBackendEnvelope(res.status, envelope, requestId);
286
+ }
287
+ const reader = res.body?.getReader();
288
+ if (!reader) {
289
+ throw new Error(
290
+ "@dashai/sdk/query: streaming response has no readable body \u2014 backend bug or misconfigured fetch impl."
291
+ );
292
+ }
293
+ const decoder = new TextDecoder();
294
+ let buffer = "";
295
+ try {
296
+ while (true) {
297
+ const { done, value } = await reader.read();
298
+ if (done) {
299
+ if (buffer.trim().length > 0) {
300
+ const last = parseSseBlock(buffer);
301
+ if (last && last.event === "message") {
302
+ yield last.data;
303
+ }
304
+ }
305
+ return;
306
+ }
307
+ buffer += decoder.decode(value, { stream: true });
308
+ const parts = buffer.split("\n\n");
309
+ buffer = parts.pop() ?? "";
310
+ for (const block of parts) {
311
+ if (!block.trim()) continue;
312
+ const parsed = parseSseBlock(block);
313
+ if (!parsed) continue;
314
+ if (parsed.event === "message") {
315
+ yield parsed.data;
316
+ } else if (parsed.event === "complete") {
317
+ return;
318
+ } else if (parsed.event === "error") {
319
+ const err = parsed.data;
320
+ throw fromBackendEnvelope(
321
+ 500,
322
+ { code: err.code, message: err.message },
323
+ requestId
324
+ );
325
+ }
326
+ }
327
+ }
328
+ } finally {
329
+ try {
330
+ reader.releaseLock();
331
+ } catch {
332
+ }
333
+ }
334
+ }
335
+ function parseSseBlock(block) {
336
+ const trimmed = block.trim();
337
+ if (trimmed.length === 0) return null;
338
+ let event = "message";
339
+ const dataLines = [];
340
+ for (const line of trimmed.split("\n")) {
341
+ if (line.startsWith("event:")) {
342
+ event = line.slice(6).trim();
343
+ } else if (line.startsWith("data:")) {
344
+ const v = line.slice(5);
345
+ dataLines.push(v.startsWith(" ") ? v.slice(1) : v);
346
+ }
347
+ }
348
+ if (dataLines.length === 0) return null;
349
+ const joined = dataLines.join("\n");
350
+ let data;
351
+ try {
352
+ data = JSON.parse(joined);
353
+ } catch {
354
+ data = joined;
355
+ }
356
+ return { event, data };
357
+ }
358
+ async function readErrorEnvelope(res) {
359
+ try {
360
+ const parsed = await res.json();
361
+ if (parsed && typeof parsed === "object") {
362
+ return parsed;
363
+ }
364
+ return null;
365
+ } catch {
366
+ return null;
367
+ }
368
+ }
369
+ function makeQuery(client, ast) {
370
+ const fork = (patch) => makeQuery(client, { ...ast, ...patch });
371
+ return {
372
+ toAst() {
373
+ return JSON.parse(JSON.stringify(ast));
374
+ },
375
+ where(clause) {
376
+ return fork({ where: clause });
377
+ },
378
+ orderBy(clauses) {
379
+ return fork({ orderBy: [...clauses] });
380
+ },
381
+ limit(n) {
382
+ if (!Number.isInteger(n) || n < 1) {
383
+ throw new Error(
384
+ `@dashai/sdk/query: .limit(${n}) must be a positive integer.`
385
+ );
386
+ }
387
+ return fork({ limit: n });
388
+ },
389
+ offset(n) {
390
+ if (!Number.isInteger(n) || n < 0) {
391
+ throw new Error(
392
+ `@dashai/sdk/query: .offset(${n}) must be a non-negative integer.`
393
+ );
394
+ }
395
+ return fork({ offset: n });
396
+ },
397
+ select(fields) {
398
+ return fork({ select: [...fields] });
399
+ },
400
+ async count() {
401
+ const minimalAst = {
402
+ ...ast,
403
+ limit: ast.limit ?? 1
404
+ };
405
+ const result = await client.request(
406
+ "POST",
407
+ "/query",
408
+ { ast: minimalAst }
409
+ );
410
+ if (Array.isArray(result)) {
411
+ return result.length;
412
+ }
413
+ return result.total;
414
+ },
415
+ async execute() {
416
+ const result = await client.request(
417
+ "POST",
418
+ "/query",
419
+ { ast }
420
+ );
421
+ if (Array.isArray(result)) {
422
+ return {
423
+ rows: result,
424
+ total: result.length,
425
+ limit: ast.limit ?? result.length,
426
+ offset: ast.offset ?? 0
427
+ };
428
+ }
429
+ return result;
430
+ },
431
+ async explain() {
432
+ return postExplain(client, ast);
433
+ },
434
+ stream(opts) {
435
+ return streamRowsViaSSE(client, ast, opts?.signal);
436
+ },
437
+ groupBy(fields) {
438
+ return makeAggQuery(client, { ...ast, groupBy: [...fields] });
439
+ },
440
+ agg(expressions) {
441
+ return makeAggQuery(client, { ...ast, agg: expressions });
442
+ },
443
+ // ── Join entries (Phase 3 slice 3.3) ──────────────────────────
444
+ //
445
+ // .join() / .leftJoin() transition to JoinedQuery whose surface
446
+ // omits write methods + the aggregation suite. The SDK delegates
447
+ // shape validation to the backend's AST validator + the
448
+ // join-translator; today's slice 3.2b-1 cap means combinations
449
+ // with where/orderBy/limit/offset/agg backend-reject with clear
450
+ // "lands in slice X" errors.
451
+ join(target, opts) {
452
+ return makeJoinedQuery(client, {
453
+ ...ast,
454
+ joins: [...ast.joins ?? [], buildJoinNode(target, opts, "inner")]
455
+ });
456
+ },
457
+ leftJoin(target, opts) {
458
+ return makeJoinedQuery(client, {
459
+ ...ast,
460
+ joins: [...ast.joins ?? [], buildJoinNode(target, opts, "left")]
461
+ });
462
+ }
463
+ };
464
+ }
465
+ function makeJoinedQuery(client, ast) {
466
+ const fork = (patch) => makeJoinedQuery(client, { ...ast, ...patch });
467
+ return {
468
+ toAst() {
469
+ return JSON.parse(JSON.stringify(ast));
470
+ },
471
+ where(clause) {
472
+ return fork({ where: clause });
473
+ },
474
+ orderBy(clauses) {
475
+ return fork({ orderBy: [...clauses] });
476
+ },
477
+ limit(n) {
478
+ if (!Number.isInteger(n) || n < 1) {
479
+ throw new Error(
480
+ `@dashai/sdk/query: .limit(${n}) must be a positive integer.`
481
+ );
482
+ }
483
+ return fork({ limit: n });
484
+ },
485
+ offset(n) {
486
+ if (!Number.isInteger(n) || n < 0) {
487
+ throw new Error(
488
+ `@dashai/sdk/query: .offset(${n}) must be a non-negative integer.`
489
+ );
490
+ }
491
+ return fork({ offset: n });
492
+ },
493
+ join(target, opts) {
494
+ return makeJoinedQuery(client, {
495
+ ...ast,
496
+ joins: [...ast.joins ?? [], buildJoinNode(target, opts, "inner")]
497
+ });
498
+ },
499
+ leftJoin(target, opts) {
500
+ return makeJoinedQuery(client, {
501
+ ...ast,
502
+ joins: [...ast.joins ?? [], buildJoinNode(target, opts, "left")]
503
+ });
504
+ },
505
+ flatten() {
506
+ return makeJoinedQuery(client, {
507
+ ...ast,
508
+ flatten: true
509
+ });
510
+ },
511
+ async execute() {
512
+ const result = await client.request(
513
+ "POST",
514
+ "/query",
515
+ { ast }
516
+ );
517
+ if (Array.isArray(result)) {
518
+ return {
519
+ rows: result,
520
+ total: result.length,
521
+ limit: ast.limit ?? result.length,
522
+ offset: ast.offset ?? 0
523
+ };
524
+ }
525
+ return result;
526
+ },
527
+ async explain() {
528
+ return postExplain(client, ast);
529
+ },
530
+ stream(opts) {
531
+ return streamRowsViaSSE(client, ast, opts?.signal);
532
+ }
533
+ };
534
+ }
535
+ function buildJoinNode(target, opts, defaultKind) {
536
+ let to;
537
+ let displaySlug;
538
+ if (typeof target === "string") {
539
+ assertValidTableSlug(target);
540
+ to = { kind: "owned", slug: target };
541
+ displaySlug = target;
542
+ } else if (target && typeof target === "object" && target.kind === "dep") {
543
+ assertValidModuleSlug(target.module);
544
+ assertValidTableSlug(target.slug);
545
+ to = { kind: "dep", module: target.module, slug: target.slug };
546
+ displaySlug = `${target.module}:${target.slug}`;
547
+ } else {
548
+ throw new Error(
549
+ `@dashai/sdk/query: .join() target must be a table slug string or { kind: 'dep', module, slug }; got ${JSON.stringify(target)}.`
550
+ );
551
+ }
552
+ if (!opts.on || Object.keys(opts.on).length === 0) {
553
+ throw new Error(
554
+ `@dashai/sdk/query: .join('${displaySlug}') requires an \`on\` clause with at least one field pair. Link-field-derived \`on\` inference lands in a follow-up slice.`
555
+ );
556
+ }
557
+ return {
558
+ kind: opts.kind ?? defaultKind,
559
+ to,
560
+ on: opts.on,
561
+ ...opts.as !== void 0 ? { as: opts.as } : {}
562
+ };
563
+ }
564
+ function makeAggQuery(client, ast) {
565
+ const fork = (patch) => makeAggQuery(client, { ...ast, ...patch });
566
+ return {
567
+ toAst() {
568
+ return JSON.parse(JSON.stringify(ast));
569
+ },
570
+ where(clause) {
571
+ return fork({ where: clause });
572
+ },
573
+ groupBy(fields) {
574
+ return fork({ groupBy: [...fields] });
575
+ },
576
+ agg(expressions) {
577
+ return fork({ agg: expressions });
578
+ },
579
+ limit(n) {
580
+ if (!Number.isInteger(n) || n < 1) {
581
+ throw new Error(
582
+ `@dashai/sdk/query: .limit(${n}) must be a positive integer.`
583
+ );
584
+ }
585
+ return fork({ limit: n });
586
+ },
587
+ offset(n) {
588
+ if (!Number.isInteger(n) || n < 0) {
589
+ throw new Error(
590
+ `@dashai/sdk/query: .offset(${n}) must be a non-negative integer.`
591
+ );
592
+ }
593
+ return fork({ offset: n });
594
+ },
595
+ async execute() {
596
+ const result = await client.request(
597
+ "POST",
598
+ "/query",
599
+ { ast }
600
+ );
601
+ if (Array.isArray(result)) {
602
+ return {
603
+ rows: result,
604
+ total: result.length
605
+ };
606
+ }
607
+ return result;
608
+ },
609
+ async explain() {
610
+ return postExplain(client, ast);
611
+ }
612
+ };
613
+ }
614
+
615
+ export { CURRENT_QUERY_AST_VERSION, isQueryAst, makeQb };
616
+ //# sourceMappingURL=index.js.map
617
+ //# sourceMappingURL=index.js.map