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