@dashai/sdk 0.9.0 → 2.0.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/README.md +53 -13
- package/dist/auth/client.cjs +23 -1
- package/dist/auth/client.cjs.map +1 -1
- package/dist/auth/client.d.cts +30 -5
- package/dist/auth/client.d.ts +30 -5
- package/dist/auth/client.js +23 -1
- package/dist/auth/client.js.map +1 -1
- package/dist/auth/middleware.cjs +11 -1
- package/dist/auth/middleware.cjs.map +1 -1
- package/dist/auth/middleware.js +11 -1
- package/dist/auth/middleware.js.map +1 -1
- package/dist/auth/server.cjs +36 -7
- package/dist/auth/server.cjs.map +1 -1
- package/dist/auth/server.js +36 -7
- package/dist/auth/server.js.map +1 -1
- package/dist/auth/types.cjs.map +1 -1
- package/dist/auth/types.d.cts +13 -0
- package/dist/auth/types.d.ts +13 -0
- package/dist/auth/types.js.map +1 -1
- package/dist/deps/index.cjs +82 -7
- package/dist/deps/index.cjs.map +1 -1
- package/dist/deps/index.d.cts +28 -10
- package/dist/deps/index.d.ts +28 -10
- package/dist/deps/index.js +81 -8
- package/dist/deps/index.js.map +1 -1
- package/dist/index-BsSFz58g.d.cts +431 -0
- package/dist/index-BsSFz58g.d.ts +431 -0
- package/dist/index.cjs +318 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -53
- package/dist/index.d.ts +66 -53
- package/dist/index.js +313 -26
- package/dist/index.js.map +1 -1
- package/dist/query/index.cjs +158 -2
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +7 -7
- package/dist/query/index.d.ts +7 -7
- package/dist/query/index.js +158 -2
- package/dist/query/index.js.map +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/{types-DXsbCVkb.d.cts → types-CkAfiS4k.d.cts} +106 -38
- package/dist/{types-DXsbCVkb.d.ts → types-CkAfiS4k.d.ts} +106 -38
- package/dist/uses/index.cjs +147 -0
- package/dist/uses/index.cjs.map +1 -0
- package/dist/uses/index.d.cts +1 -0
- package/dist/uses/index.d.ts +1 -0
- package/dist/uses/index.js +142 -0
- package/dist/uses/index.js.map +1 -0
- package/package.json +6 -1
- package/dist/errors-BV75u7b9.d.cts +0 -207
- package/dist/errors-BV75u7b9.d.ts +0 -207
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/data/errors.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Error model for @dashai/sdk.\n *\n * The backend speaks a structured error envelope:\n *\n * {\n * \"code\": \"ROW_NOT_FOUND\",\n * \"message\": \"Row 123 not found in tasks\",\n * \"retriable\": false,\n * \"context\": { ... }\n * }\n *\n * This module wraps that envelope in a typed `DashwiseError` plus a small\n * set of subclasses for the codes consumers most commonly want to branch\n * on (network errors, auth failures, row-not-found, contract violations).\n *\n * Anything not matched by a specific subclass becomes a plain\n * `DashwiseError` carrying the raw `code`. Branch on `.code` for codes\n * we don't have a class for; use `instanceof` for the common cases.\n */\n\n/**\n * Known error codes. Not exhaustive — the backend emits many codes\n * specific to individual modules / endpoints. Use these constants when\n * you want compile-time safety; otherwise treat `err.code` as a string.\n *\n * The list is curated to the codes a module author is most likely to\n * branch on. Adding a new code here is a non-breaking change.\n */\nexport const DashwiseErrorCode = {\n // Auth / authz\n UNAUTHENTICATED: 'UNAUTHENTICATED',\n FORBIDDEN: 'FORBIDDEN',\n TOKEN_EXPIRED: 'TOKEN_EXPIRED',\n INSTALLATION_NOT_FOUND: 'INSTALLATION_NOT_FOUND',\n INSTALLATION_MODE_UNSUPPORTED: 'INSTALLATION_MODE_UNSUPPORTED',\n // Contract enforcement\n CONTRACT_VIOLATION: 'CONTRACT_VIOLATION',\n FIELD_NOT_READABLE: 'FIELD_NOT_READABLE',\n FILTER_OP_UNKNOWN: 'FILTER_OP_UNKNOWN',\n OPERATION_NOT_ALLOWED: 'OPERATION_NOT_ALLOWED',\n // Cross-module dependencies (Phase 1 of the SDK query plane —\n // see dashwise-devops-docs/plans/modules-sdk-query-v1.md).\n // Surfaced when reading borrowed tables via `client.deps(slug)`.\n DEPENDENCY_NOT_DECLARED: 'DEPENDENCY_NOT_DECLARED',\n TABLE_NOT_IN_CONTRACT: 'TABLE_NOT_IN_CONTRACT',\n PROVIDER_TABLE_MISSING: 'PROVIDER_TABLE_MISSING',\n OPERATION_NOT_ALLOWED_BY_PROVIDER: 'OPERATION_NOT_ALLOWED_BY_PROVIDER',\n // Cross-module dependency PARK state (Phase 3 modules-platform-v2 —\n // dependency connect/bind lifecycle). Surfaced at RUNTIME (409) when a\n // module reads a borrowed table (`client.deps(slug)` / `qb.deps.<slug>`)\n // or invokes a dep action whose `module_dependencies` row is currently\n // `status='unbound'` — the dependency is declared but not connected to a\n // provider installation (no provider in the workspace, version mismatch,\n // manually unbound, provider uninstalled, etc.). The consumer app should\n // render a \"connect your provider\" state rather than treating this as a\n // hard failure. `err.context` carries `{ provider, range, reason }`.\n DEP_UNBOUND: 'DEP_UNBOUND',\n // Workspace-table `uses` plane (Phase 4 modules-platform-v2 — the\n // declared/portable path for a module to read+write a WORKSPACE table\n // the workspace already owns, as opposed to `deps` = another module's\n // tables). Surfaced at RUNTIME against `client.uses(slug)` /\n // `qb.uses.<slug>` / the `POST /api/module-api/uses/:slug/...` REST\n // plane. The BINDING is the grant on this plane (for sandbox AND\n // api_key auth alike — a kind-local key does NOT bypass it):\n // - USES_UNBOUND 409 — the `uses` slot is declared but its\n // `module_table_bindings` row isn't `status='bound'` (never bound,\n // ambiguous match, no match, table trashed, ops widened pending\n // re-consent, manually unbound). `err.context` carries\n // `{ uses_slug, reason }`. Recoverable: a workspace admin binds a\n // table (`dashwise bind <uses_slug>`), then the read succeeds.\n // - USES_OP_NOT_ALLOWED 403 — the operation isn't in the binding's\n // enforcement set (manifest `ops` ∩ `consented_ops`). `err.context`\n // carries `{ uses_slug, op }`. Fix: widen the manifest ops +\n // re-consent (re-bind), or the admin re-binds to consent.\n // - CONTRACT_FIELD_MISSING 409 — a field the binding maps no longer\n // exists on the physical table (schema drift). `err.context`\n // carries `{ uses_slug, field }`. The binding is best-effort parked\n // on detection; an admin re-binds to a table that has the field.\n USES_UNBOUND: 'USES_UNBOUND',\n USES_OP_NOT_ALLOWED: 'USES_OP_NOT_ALLOWED',\n CONTRACT_FIELD_MISSING: 'CONTRACT_FIELD_MISSING',\n // `uses`-plane query joins are NOT supported in v1 — a\n // `qb.uses.<slug>.join(...)` / cross-plane join is rejected with a\n // typed 400. (Joins WITH a uses table as a target are v2 territory.)\n USES_JOIN_UNSUPPORTED: 'USES_JOIN_UNSUPPORTED',\n // Query-plane joins (Phase 3 — see\n // dashwise-devops-docs/plans/modules-sdk-query-v1-p3-execution.md).\n // Surfaced when `.join('<slug>')` / `.leftJoin('<slug>')` can't\n // resolve the target. Pre-declared in slice 3.1 so slice 3.2's\n // backend translator can emit them through fromBackendEnvelope\n // without an SDK roundtrip.\n LINK_FIELD_NOT_FOUND: 'LINK_FIELD_NOT_FOUND',\n NOT_A_LINK_FIELD: 'NOT_A_LINK_FIELD',\n JOIN_NOT_LINKED: 'JOIN_NOT_LINKED',\n // Cross-module actions (Phase 7 slice 7.3a — see\n // dashwise-devops-docs/plans/modules-sdk-query-v1-p7-cross-module-writes.md).\n // Surfaced when `qb.deps.<dep>.actions.<name>(input)` (generated\n // factory) dispatches against the backend endpoint\n // `POST /api/module-api/deps/:providerSlug/actions/:actionName`.\n // Slice 7.2b ships the backend emitter; slice 7.3a (this slice)\n // reserves the codes + maps them to DepActionError in the SDK.\n ACTION_NOT_EXPOSED: 'ACTION_NOT_EXPOSED',\n ACTION_NOT_DECLARED: 'ACTION_NOT_DECLARED',\n ACTION_INPUT_INVALID: 'ACTION_INPUT_INVALID',\n ACTION_OUTPUT_INVALID: 'ACTION_OUTPUT_INVALID',\n ACTION_RATE_LIMITED: 'ACTION_RATE_LIMITED',\n ACTION_EXECUTION_FAILED: 'ACTION_EXECUTION_FAILED',\n // Data plane\n ROW_NOT_FOUND: 'ROW_NOT_FOUND',\n TABLE_NOT_FOUND: 'TABLE_NOT_FOUND',\n VALIDATION_FAILED: 'VALIDATION_FAILED',\n ROW_LIMIT_EXCEEDED: 'ROW_LIMIT_EXCEEDED',\n STORAGE_LIMIT_EXCEEDED: 'STORAGE_LIMIT_EXCEEDED',\n // Outbound / runtime\n OUTBOUND_NOT_ALLOWED: 'OUTBOUND_NOT_ALLOWED',\n OUTBOUND_CONCURRENCY_LIMITED: 'OUTBOUND_CONCURRENCY_LIMITED',\n // Transport\n NETWORK_ERROR: 'NETWORK_ERROR',\n TIMEOUT: 'TIMEOUT',\n // Server\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n} as const;\n\nexport type DashwiseErrorCode = (typeof DashwiseErrorCode)[keyof typeof DashwiseErrorCode];\n\n/**\n * Base class for every error thrown by @dashai/sdk. Carries the\n * structured envelope from the backend (or a synthetic one for\n * client-side failures like network errors / timeouts).\n *\n * Always thrown — never returned as a value. The async client methods\n * `.list()` / `.get()` etc. reject with one of these on failure.\n */\nexport class DashwiseError extends Error {\n readonly code: string;\n readonly status: number;\n readonly retriable: boolean;\n readonly context: Record<string, unknown> | undefined;\n /**\n * Backend-issued trace anchor (Phase 6 slice 6.2, 2026-05-19).\n * Mirrors the `x-request-id` response header. Populated whenever\n * the backend's `RuntimeQueryController` (or any future endpoint\n * that echoes the header) is the origin of this error. `null` when\n * the failure is client-side (NetworkError / TimeoutError — the\n * request never reached the server, so there's no server-issued\n * ID to surface).\n *\n * Use this as a debugging trace anchor when filing a bug against a\n * specific failed query — the matching row in `module_query_log`\n * carries the full backend context (ast_hash, error_code,\n * duration_ms, etc.).\n */\n readonly requestId: string | null;\n\n constructor(\n code: string,\n message: string,\n options: {\n status?: number;\n retriable?: boolean;\n context?: Record<string, unknown>;\n cause?: unknown;\n /**\n * Phase 6 slice 6.2: backend `x-request-id` response header,\n * captured by the transport before throwing. Pass `undefined`\n * (or omit) for client-side errors that never hit the server.\n */\n requestId?: string | null;\n } = {},\n ) {\n super(message);\n this.name = this.constructor.name;\n this.code = code;\n this.status = options.status ?? 0;\n this.retriable = options.retriable ?? false;\n this.context = options.context;\n this.requestId = options.requestId ?? null;\n if (options.cause !== undefined) {\n // Node 18+ and modern browsers support Error.cause natively.\n // Use a property assignment because `super({ cause })` is awkward\n // when the base class only takes a string.\n (this as { cause?: unknown }).cause = options.cause;\n }\n }\n}\n\n/** Network-level failure (DNS, connection refused, TLS, fetch threw). */\nexport class NetworkError extends DashwiseError {\n constructor(message: string, cause?: unknown) {\n super(DashwiseErrorCode.NETWORK_ERROR, message, { retriable: true, cause });\n }\n}\n\n/** Request exceeded the configured timeout (`timeoutMs`). */\nexport class TimeoutError extends DashwiseError {\n constructor(message: string, timeoutMs: number) {\n super(DashwiseErrorCode.TIMEOUT, message, {\n retriable: true,\n context: { timeout_ms: timeoutMs },\n });\n }\n}\n\n/** 401 — token missing/invalid. */\nexport class UnauthenticatedError extends DashwiseError {\n constructor(message = 'Authentication required', requestId: string | null = null) {\n super(DashwiseErrorCode.UNAUTHENTICATED, message, { status: 401, requestId });\n }\n}\n\n/** 403 — caller lacks the role/scope for this operation. */\nexport class ForbiddenError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.FORBIDDEN, message, { status: 403, context, requestId });\n }\n}\n\n/** 404 — `GET /db/<table>/<id>` for a missing or soft-deleted row. */\nexport class RowNotFoundError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.ROW_NOT_FOUND, message, { status: 404, context, requestId });\n }\n}\n\n/**\n * Schema-level rejection: dep contract violated, table not exposed, field\n * not readable, filter op not allowed. Covers the family of \"you asked for\n * something the manifest doesn't permit\" errors.\n */\nexport class ContractViolationError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.CONTRACT_VIOLATION, message, {\n status: 403,\n context,\n requestId,\n });\n }\n}\n\n/** 422-ish — request body failed validation. Inspect `context` for field-level errors. */\nexport class ValidationError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.VALIDATION_FAILED, message, {\n status: 400,\n context,\n requestId,\n });\n }\n}\n\n// ── Cross-module dependency errors ─────────────────────────────────────\n//\n// Surfaced when calling `client.deps(slug).db<Row>(table).<op>(...)`.\n// Each subclass preserves the original backend `code` (unlike the older\n// `ContractViolationError` which collapses several codes onto a single\n// `CONTRACT_VIOLATION` string — that's a known wart we don't replicate\n// in new subclasses). Branch on `instanceof` for the common cases;\n// inspect `err.code` if you need to distinguish the two contract-error\n// codes (`DEPENDENCY_NOT_DECLARED` vs `TABLE_NOT_IN_CONTRACT`).\n\n/**\n * 404 — the caller's `module.json#dependencies` doesn't declare a\n * dependency on the requested provider module, OR the declared dep\n * exists but its `reads[]` block doesn't include the requested table.\n *\n * Either way the fix is on the **caller's** side: update the manifest\n * to declare the dep / add the table to `reads[]`, then re-publish.\n *\n * Covers both `DEPENDENCY_NOT_DECLARED` and `TABLE_NOT_IN_CONTRACT` —\n * inspect `err.code` to distinguish if you need different UX per case.\n */\nexport class DependencyContractError extends DashwiseError {\n constructor(\n code: string,\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(code, message, { status: 404, context, requestId });\n }\n}\n\n/**\n * 404 — the provider's physical table no longer exists. This means the\n * provider published a version that dropped the table while the\n * consumer's installation still pinned the old contract. Recovery is\n * on the **provider's** side (republish with the table) or the\n * workspace admin's (downgrade the provider, or upgrade the consumer\n * to a version whose `dependencies[]` no longer references it).\n */\nexport class ProviderTableMissingError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.PROVIDER_TABLE_MISSING, message, {\n status: 404,\n context,\n requestId,\n });\n }\n}\n\n/**\n * 403 — the provider's `exposes.operations.<verb>.allowed` is explicitly\n * `false` for the requested verb. Distinct from `OPERATION_NOT_ALLOWED`\n * (which is the **owner's** own-table policy) — `OPERATION_NOT_ALLOWED_BY_PROVIDER`\n * is the provider opting out of letting depending modules use a verb.\n *\n * No caller-side fix; either the provider needs to flip the gate, or\n * the consumer needs to find a different way to satisfy the use case.\n */\nexport class OperationNotAllowedByProviderError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.OPERATION_NOT_ALLOWED_BY_PROVIDER, message, {\n status: 403,\n context,\n requestId,\n });\n }\n}\n\n/**\n * The vocabulary of `unbound_reason` values a parked dependency can\n * carry, surfaced in {@link DepUnboundError.reason}. Mirrors the\n * backend's `module_dependencies.unbound_reason` enum (Phase 3\n * modules-platform-v2). Not exhaustive by design — treat `.reason` as a\n * string and branch on these constants when you want compile-time\n * safety. Adding a new reason is a non-breaking change.\n *\n * Resolve/bind-time reasons (why the dep never bound):\n * - `MISSING_DEPENDENCY` no install of the provider module in the workspace\n * - `DEPENDENCY_VERSION_MISMATCH` an install exists but its version is outside the declared range\n * - `PROVIDER_VERSION_RECORD_MISSING` the provider's version row is gone (data-integrity edge)\n * - `EXPOSES_MISSING` the provider no longer exposes a table/field the dep reads\n * - `EXPOSES_PRIVATE` the exposed surface is `private` to the consumer\n * - `ACTION_NOT_EXPOSED` a declared dep action isn't exposed by the provider (Rule #11)\n * - `PROVIDER_NOT_PRODUCTION` kind hygiene: a production consumer can only bind a production provider\n *\n * Lifecycle reasons (the dep bound once, then got parked):\n * - `MANUALLY_UNBOUND` a workspace admin ran `deps unbind` / the unbind endpoint\n * - `PROVIDER_UPGRADE_BROKE_CONTRACT` a provider upgrade (with `force`) broke this consumer's contract\n * - `PROVIDER_UNINSTALLED` the provider was uninstalled with `park_consumers`\n */\nexport const DepUnboundReason = {\n MISSING_DEPENDENCY: 'MISSING_DEPENDENCY',\n DEPENDENCY_VERSION_MISMATCH: 'DEPENDENCY_VERSION_MISMATCH',\n PROVIDER_VERSION_RECORD_MISSING: 'PROVIDER_VERSION_RECORD_MISSING',\n EXPOSES_MISSING: 'EXPOSES_MISSING',\n EXPOSES_PRIVATE: 'EXPOSES_PRIVATE',\n ACTION_NOT_EXPOSED: 'ACTION_NOT_EXPOSED',\n PROVIDER_NOT_PRODUCTION: 'PROVIDER_NOT_PRODUCTION',\n MANUALLY_UNBOUND: 'MANUALLY_UNBOUND',\n PROVIDER_UPGRADE_BROKE_CONTRACT: 'PROVIDER_UPGRADE_BROKE_CONTRACT',\n PROVIDER_UNINSTALLED: 'PROVIDER_UNINSTALLED',\n} as const;\n\nexport type DepUnboundReason =\n (typeof DepUnboundReason)[keyof typeof DepUnboundReason];\n\n/**\n * 409 — a runtime read (borrowed-table `list` / `get`, query-plane\n * `qb.deps.<slug>.<table>`, or a cross-module action) targeted a\n * dependency whose `module_dependencies` row is `status='unbound'`\n * (Phase 3 modules-platform-v2 dependency connect lifecycle).\n *\n * \"Unbound\" means the dependency is *declared* in the consumer's\n * manifest but not *connected* to a provider installation. It happens\n * when the resolver PARKED the dep at install/apply time (no provider\n * in the workspace, version mismatch, exposes/action coverage gap) or\n * when the dependency was later parked (manually unbound, provider\n * upgrade broke the contract, provider uninstalled).\n *\n * This is a *recoverable* state, not a bug in the consumer. Catch it and\n * render a \"connect your provider\" prompt — a workspace admin resolves it\n * by installing/binding the provider (`dashwise deps bind <slug>` or the\n * `POST /api/installations/:id/dependencies/:providerSlug/bind` endpoint).\n * Once bound, the same read succeeds with no code change.\n *\n * Convenience accessors ({@link DepUnboundError.provider},\n * {@link DepUnboundError.range}, {@link DepUnboundError.reason}) read the\n * backend's `context: { provider, range, reason }` so callers don't have\n * to reach into `err.context` by hand.\n *\n * @example\n * ```ts\n * import { DepUnboundError } from '@dashai/sdk';\n *\n * try {\n * const contacts = await client.deps('crm').db<ContactRow>('contacts').list();\n * // …or the typed builder: await qb.deps.crm.contacts.execute();\n * } catch (err) {\n * if (err instanceof DepUnboundError) {\n * // Declared but not connected — show a connect-your-provider CTA.\n * return <ConnectProvider provider={err.provider} range={err.range} reason={err.reason} />;\n * }\n * throw err;\n * }\n * ```\n */\nexport class DepUnboundError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.DEP_UNBOUND, message, {\n status: 409,\n context,\n requestId,\n });\n }\n\n /**\n * The provider module slug the parked dependency points at (kebab-case,\n * e.g. `crm`). Read from `context.provider`; `undefined` if the backend\n * omitted it.\n */\n get provider(): string | undefined {\n const v = this.context?.provider;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * The SemVer range the consumer's manifest declared for this dependency\n * (e.g. `^1.2.0`). Read from `context.range`; `undefined` if omitted.\n */\n get range(): string | undefined {\n const v = this.context?.range;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * Why the dependency is unbound — one of {@link DepUnboundReason} (but\n * typed as `string` for forward-compat with reasons the backend may add).\n * Read from `context.reason`; `undefined` if omitted.\n */\n get reason(): string | undefined {\n const v = this.context?.reason;\n return typeof v === 'string' ? v : undefined;\n }\n}\n\n// ── Workspace-table `uses` errors (Phase 4 modules-platform-v2) ────────\n//\n// The `uses` plane lets a module read+write a WORKSPACE table the\n// workspace already owns — the declared, portable counterpart to the\n// ad-hoc numeric-tableId Data Hub bridge. A module declares an ABSTRACT\n// contract in `module.json#uses[].tables[]` (slug + fields + abstract\n// types + ops); at install/bind time that slot is BOUND to a physical\n// workspace table via a `field_map` (declared field → physical field id).\n// The binding IS the grant: even a kind-local `dwk_` key can't touch a\n// uses table until its slot is bound.\n//\n// Surfaced against `client.uses(slug).db<Row>(table)` /\n// `qb.uses.<slug>.<table>` / the REST plane at\n// `POST /api/module-api/uses/:usesSlug/rows/...`. Same\n// preserve-the-code, convenience-accessor pattern as DepUnboundError.\n\n/**\n * The vocabulary of `reason` values a parked `uses` binding can carry,\n * surfaced in {@link UsesUnboundError.reason}. Mirrors the backend's\n * `module_table_bindings` unbound_reason enum for this plane\n * (Phase 4 modules-platform-v2). Not exhaustive by design — treat\n * `.reason` as a string; branch on these constants for compile-time\n * safety. Adding a new reason is non-breaking.\n *\n * - `NOT_BOUND_YET` the slot has never been bound (declared\n * but no workspace table connected yet)\n * - `AMBIGUOUS_MATCH` auto-match found >1 candidate table; a\n * human must pick one (`dashwise bind`)\n * - `NO_MATCH` auto-match found no candidate table\n * - `TABLE_DELETED` the bound table was trashed (or a mapped\n * field drifted away) → the row parked\n * - `OPS_WIDENED_PENDING_CONSENT` the manifest widened `ops` beyond what\n * was consented at bind time; re-bind to\n * refresh consent\n * - `MANUALLY_UNBOUND` a workspace admin ran `dashwise unbind`\n */\nexport const UsesUnboundReason = {\n NOT_BOUND_YET: 'NOT_BOUND_YET',\n AMBIGUOUS_MATCH: 'AMBIGUOUS_MATCH',\n NO_MATCH: 'NO_MATCH',\n TABLE_DELETED: 'TABLE_DELETED',\n OPS_WIDENED_PENDING_CONSENT: 'OPS_WIDENED_PENDING_CONSENT',\n MANUALLY_UNBOUND: 'MANUALLY_UNBOUND',\n} as const;\n\nexport type UsesUnboundReason =\n (typeof UsesUnboundReason)[keyof typeof UsesUnboundReason];\n\n/**\n * 409 — a runtime read/write against a `uses` table whose\n * `module_table_bindings` row is not `status='bound'` (Phase 4\n * modules-platform-v2 workspace-table binding lifecycle).\n *\n * \"Unbound\" means the workspace-table slot is *declared* in the module's\n * manifest `uses` block but not *connected* to a physical workspace\n * table. It happens when install-time auto-match parked the slot (no\n * candidate / ambiguous candidates / a write contract that never\n * auto-binds), the bound table was later trashed, the manifest widened\n * `ops` beyond consent, or an admin manually unbound it.\n *\n * A *recoverable* state, not a bug. Catch it and render a \"connect a\n * table\" prompt — a workspace admin resolves it by binding a workspace\n * table to the slot (`dashwise bind <uses_slug>` / the\n * `PUT /api/installations/:id/bindings/:usesSlug` endpoint). Once bound,\n * the same call succeeds with no code change.\n *\n * Convenience accessors read the backend's `context: { uses_slug, reason }`.\n *\n * @example\n * ```ts\n * import { UsesUnboundError } from '@dashai/sdk';\n *\n * try {\n * const staff = await client.uses('employees').db<EmployeeRow>('employees').list();\n * // …or the typed builder: await qb.uses.employees.execute();\n * } catch (err) {\n * if (err instanceof UsesUnboundError) {\n * return <BindWorkspaceTable slot={err.usesSlug} reason={err.reason} />;\n * }\n * throw err;\n * }\n * ```\n */\nexport class UsesUnboundError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.USES_UNBOUND, message, {\n status: 409,\n context,\n requestId,\n });\n }\n\n /**\n * The manifest `uses[].tables[].slug` of the unbound slot (kebab-case,\n * e.g. `employees`). Read from `context.uses_slug`; `undefined` if the\n * backend omitted it.\n */\n get usesSlug(): string | undefined {\n const v = this.context?.uses_slug;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * Why the slot is unbound — one of {@link UsesUnboundReason} (typed as\n * `string` for forward-compat with reasons the backend may add). Read\n * from `context.reason`; `undefined` if omitted.\n */\n get reason(): string | undefined {\n const v = this.context?.reason;\n return typeof v === 'string' ? v : undefined;\n }\n}\n\n/**\n * 403 — a `uses`-plane operation that isn't in the binding's enforced\n * operation set (the manifest's declared `ops` INTERSECT the admin's\n * `consented_ops` at bind time). E.g. the manifest declares\n * `ops: ['read','update']` but the admin consented only to `read` when\n * binding, so an `update` is rejected.\n *\n * Fix: either narrow the call to a consented op, or widen the manifest\n * `ops` + have an admin re-bind (which refreshes `consented_ops`).\n *\n * `err.op` / `err.usesSlug` read the backend's `context: { uses_slug, op }`.\n */\nexport class UsesOpNotAllowedError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.USES_OP_NOT_ALLOWED, message, {\n status: 403,\n context,\n requestId,\n });\n }\n\n /** The `uses` slot slug the rejected op targeted. Read from `context.uses_slug`. */\n get usesSlug(): string | undefined {\n const v = this.context?.uses_slug;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * The operation that was rejected — one of `create` / `read` /\n * `update` / `delete`. Read from `context.op`; `undefined` if omitted.\n */\n get op(): string | undefined {\n const v = this.context?.op;\n return typeof v === 'string' ? v : undefined;\n }\n}\n\n/**\n * 409 — a field the `uses` binding's `field_map` points at no longer\n * exists on the physical workspace table (schema drift: the table owner\n * deleted or renamed the column after the bind). The backend best-effort\n * parks the binding (reason `TABLE_DELETED`/field-drift) on detection.\n *\n * Recovery is the workspace admin's: re-bind the slot to a table that\n * still has the field, or re-map the field (`dashwise bind <uses_slug>\n * --map <usesField>=<physicalField>`).\n *\n * `err.field` / `err.usesSlug` read the backend's\n * `context: { uses_slug, field }`.\n */\nexport class ContractFieldMissingError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.CONTRACT_FIELD_MISSING, message, {\n status: 409,\n context,\n requestId,\n });\n }\n\n /** The `uses` slot slug whose contract broke. Read from `context.uses_slug`. */\n get usesSlug(): string | undefined {\n const v = this.context?.uses_slug;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * The declared field slug (the `uses` side name) whose mapped physical\n * field is gone. Read from `context.field`; `undefined` if omitted.\n */\n get field(): string | undefined {\n const v = this.context?.field;\n return typeof v === 'string' ? v : undefined;\n }\n}\n\n// ── Join errors (Phase 3) ──────────────────────────────────────────────\n//\n// Surfaced when `.join('<slug>')` / `.leftJoin('<slug>')` on the\n// generated `qb` builder can't resolve the target against the\n// manifest. Pre-declared in slice 3.1; raised by the backend\n// translator in slice 3.2. Same one-subclass-with-preserved-code\n// pattern as DependencyContractError so authors can switch on\n// `err.code` for the three sub-cases:\n//\n// LINK_FIELD_NOT_FOUND `.join('xyz')` where `xyz` isn't a field\n// on the source table. Fix: pass an\n// explicit `on` argument, or correct the slug.\n//\n// NOT_A_LINK_FIELD `.join('title')` where `title` exists on\n// the source table but its type isn't\n// `link_row`. Fix: use a real link field,\n// or pass explicit `on`.\n//\n// JOIN_NOT_LINKED `.join('sibling_table')` where the target\n// is a sibling table but no link field\n// bridges them. Fix: pass explicit `on`.\n\n/**\n * 400 — `.join()` / `.leftJoin()` referenced a target the manifest\n * can't resolve. Covers the three sub-cases above; inspect\n * `err.code` to distinguish.\n *\n * Author-side fix in every case: either correct the slug, or pass\n * an explicit `on` argument to bypass link-field inference.\n */\nexport class JoinError extends DashwiseError {\n constructor(\n code: string,\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(code, message, { status: 400, context, requestId });\n }\n}\n\n// ── Cross-module action errors (Phase 7 slice 7.3a, 2026-05-20) ──────\n//\n// Surfaced by the codegen-emitted `qb.deps.<dep>.actions.<name>(input)`\n// factory when the backend's cross-module dispatch\n// (`CrossModuleActionService.dispatch()`) rejects. Six sub-cases all\n// covered by `DepActionError`; inspect `err.code` to discriminate.\n//\n// ACTION_NOT_EXPOSED 403 — provider's manifest doesn't expose\n// the action, OR visibility='private'.\n// Recovery: provider author needs to\n// publish a version that exposes it.\n// ACTION_NOT_DECLARED 403 — consumer's manifest doesn't list\n// the action in dependencies[].actions[].\n// Recovery: add it + republish.\n// ACTION_INPUT_INVALID 400 — input fails Ajv against the\n// provider's declared input schema.\n// Recovery: fix the call site (typical\n// with stale codegen + drift).\n// ACTION_OUTPUT_INVALID 500 — provider returned a value that fails\n// the declared output schema. This is a\n// PROVIDER-author bug; consumer can\n// only file an issue.\n// ACTION_RATE_LIMITED 429 — per-actor or per-caller-installation\n// rate limit exceeded. Retry with\n// backoff after Retry-After (slice 7.4).\n// ACTION_EXECUTION_FAILED 200-envelope — action body threw, timed out,\n// or OOM-ed. Provider-author bug;\n// trace surfaced in err.context.trace.\n\n/**\n * Single subclass for every cross-module action failure. The code\n * (and HTTP status) varies per sub-case; the wrapping class lets\n * consumers `catch (e) { if (e instanceof DepActionError) {...} }`\n * regardless of which sub-case fired.\n */\nexport class DepActionError extends DashwiseError {\n constructor(\n code: string,\n message: string,\n status: number,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(code, message, {\n status,\n retriable: code === DashwiseErrorCode.ACTION_RATE_LIMITED,\n context,\n requestId,\n });\n }\n}\n\n// ── Factory: backend envelope → typed error ────────────────────────────\n\ninterface BackendErrorEnvelope {\n code?: string;\n message?: string;\n retriable?: boolean;\n context?: Record<string, unknown>;\n}\n\n/**\n * Build a typed `DashwiseError` from the backend's response envelope.\n *\n * The mapping picks the most specific subclass for codes we have a class\n * for; everything else becomes a plain `DashwiseError` carrying the raw\n * code. This keeps the type set small while still letting consumers\n * branch on string codes when they want to.\n *\n * Phase 6 slice 6.2 (2026-05-19): accepts an optional `requestId` —\n * the backend's `x-request-id` response header — and threads it onto\n * every produced error so consumers can use it as a debugging trace\n * anchor. Default `null` for backward compat; the transport always\n * passes it through.\n */\nexport function fromBackendEnvelope(\n status: number,\n envelope: BackendErrorEnvelope | null,\n requestId: string | null = null,\n): DashwiseError {\n const code = envelope?.code ?? codeFromStatus(status);\n const message = envelope?.message ?? `HTTP ${status}`;\n const context = envelope?.context;\n\n switch (code) {\n case DashwiseErrorCode.UNAUTHENTICATED:\n case DashwiseErrorCode.TOKEN_EXPIRED:\n return new UnauthenticatedError(message, requestId);\n case DashwiseErrorCode.FORBIDDEN:\n return new ForbiddenError(message, context, requestId);\n case DashwiseErrorCode.ROW_NOT_FOUND:\n return new RowNotFoundError(message, context, requestId);\n case DashwiseErrorCode.CONTRACT_VIOLATION:\n case DashwiseErrorCode.FIELD_NOT_READABLE:\n case DashwiseErrorCode.FILTER_OP_UNKNOWN:\n case DashwiseErrorCode.OPERATION_NOT_ALLOWED:\n return new ContractViolationError(message, context, requestId);\n case DashwiseErrorCode.VALIDATION_FAILED:\n return new ValidationError(message, context, requestId);\n case DashwiseErrorCode.DEPENDENCY_NOT_DECLARED:\n case DashwiseErrorCode.TABLE_NOT_IN_CONTRACT:\n return new DependencyContractError(code, message, context, requestId);\n case DashwiseErrorCode.PROVIDER_TABLE_MISSING:\n return new ProviderTableMissingError(message, context, requestId);\n case DashwiseErrorCode.OPERATION_NOT_ALLOWED_BY_PROVIDER:\n return new OperationNotAllowedByProviderError(message, context, requestId);\n case DashwiseErrorCode.DEP_UNBOUND:\n return new DepUnboundError(message, context, requestId);\n case DashwiseErrorCode.USES_UNBOUND:\n return new UsesUnboundError(message, context, requestId);\n case DashwiseErrorCode.USES_OP_NOT_ALLOWED:\n return new UsesOpNotAllowedError(message, context, requestId);\n case DashwiseErrorCode.CONTRACT_FIELD_MISSING:\n return new ContractFieldMissingError(message, context, requestId);\n case DashwiseErrorCode.LINK_FIELD_NOT_FOUND:\n case DashwiseErrorCode.NOT_A_LINK_FIELD:\n case DashwiseErrorCode.JOIN_NOT_LINKED:\n // `uses`-plane joins are unsupported in v1; the backend emits a 400\n // with this code. Map to JoinError so it lands in the same\n // \"your join can't be resolved\" family (code preserved for `err.code`).\n case DashwiseErrorCode.USES_JOIN_UNSUPPORTED:\n return new JoinError(code, message, context, requestId);\n case DashwiseErrorCode.ACTION_NOT_EXPOSED:\n case DashwiseErrorCode.ACTION_NOT_DECLARED:\n return new DepActionError(code, message, 403, context, requestId);\n case DashwiseErrorCode.ACTION_INPUT_INVALID:\n return new DepActionError(code, message, 400, context, requestId);\n case DashwiseErrorCode.ACTION_OUTPUT_INVALID:\n return new DepActionError(code, message, 500, context, requestId);\n case DashwiseErrorCode.ACTION_RATE_LIMITED:\n return new DepActionError(code, message, 429, context, requestId);\n case DashwiseErrorCode.ACTION_EXECUTION_FAILED:\n // ACTION_EXECUTION_FAILED arrives as a 200 envelope from the\n // backend (the host succeeded; the action body crashed). The\n // _callAction helper unwraps the envelope and synthesizes this\n // via the factory call — status 200 reflects \"the transport was\n // fine; the failure is in the application layer\".\n return new DepActionError(code, message, 200, context, requestId);\n default:\n return new DashwiseError(code, message, {\n status,\n retriable: envelope?.retriable ?? false,\n context,\n requestId,\n });\n }\n}\n\nfunction codeFromStatus(status: number): string {\n if (status === 401) return DashwiseErrorCode.UNAUTHENTICATED;\n if (status === 403) return DashwiseErrorCode.FORBIDDEN;\n if (status === 404) return DashwiseErrorCode.ROW_NOT_FOUND;\n if (status >= 500) return DashwiseErrorCode.INTERNAL_ERROR;\n return 'HTTP_ERROR';\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { C as ContractFieldMissingError, f as UsesOpNotAllowedError, g as UsesUnboundError, h as UsesUnboundReason } from '../index-BsSFz58g.cjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { C as ContractFieldMissingError, f as UsesOpNotAllowedError, g as UsesUnboundError, h as UsesUnboundReason } from '../index-BsSFz58g.js';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/data/errors.ts
|
|
2
|
+
var DashwiseErrorCode = {
|
|
3
|
+
// Workspace-table `uses` plane (Phase 4 modules-platform-v2 — the
|
|
4
|
+
// declared/portable path for a module to read+write a WORKSPACE table
|
|
5
|
+
// the workspace already owns, as opposed to `deps` = another module's
|
|
6
|
+
// tables). Surfaced at RUNTIME against `client.uses(slug)` /
|
|
7
|
+
// `qb.uses.<slug>` / the `POST /api/module-api/uses/:slug/...` REST
|
|
8
|
+
// plane. The BINDING is the grant on this plane (for sandbox AND
|
|
9
|
+
// api_key auth alike — a kind-local key does NOT bypass it):
|
|
10
|
+
// - USES_UNBOUND 409 — the `uses` slot is declared but its
|
|
11
|
+
// `module_table_bindings` row isn't `status='bound'` (never bound,
|
|
12
|
+
// ambiguous match, no match, table trashed, ops widened pending
|
|
13
|
+
// re-consent, manually unbound). `err.context` carries
|
|
14
|
+
// `{ uses_slug, reason }`. Recoverable: a workspace admin binds a
|
|
15
|
+
// table (`dashwise bind <uses_slug>`), then the read succeeds.
|
|
16
|
+
// - USES_OP_NOT_ALLOWED 403 — the operation isn't in the binding's
|
|
17
|
+
// enforcement set (manifest `ops` ∩ `consented_ops`). `err.context`
|
|
18
|
+
// carries `{ uses_slug, op }`. Fix: widen the manifest ops +
|
|
19
|
+
// re-consent (re-bind), or the admin re-binds to consent.
|
|
20
|
+
// - CONTRACT_FIELD_MISSING 409 — a field the binding maps no longer
|
|
21
|
+
// exists on the physical table (schema drift). `err.context`
|
|
22
|
+
// carries `{ uses_slug, field }`. The binding is best-effort parked
|
|
23
|
+
// on detection; an admin re-binds to a table that has the field.
|
|
24
|
+
USES_UNBOUND: "USES_UNBOUND",
|
|
25
|
+
USES_OP_NOT_ALLOWED: "USES_OP_NOT_ALLOWED",
|
|
26
|
+
CONTRACT_FIELD_MISSING: "CONTRACT_FIELD_MISSING"};
|
|
27
|
+
var DashwiseError = class extends Error {
|
|
28
|
+
code;
|
|
29
|
+
status;
|
|
30
|
+
retriable;
|
|
31
|
+
context;
|
|
32
|
+
/**
|
|
33
|
+
* Backend-issued trace anchor (Phase 6 slice 6.2, 2026-05-19).
|
|
34
|
+
* Mirrors the `x-request-id` response header. Populated whenever
|
|
35
|
+
* the backend's `RuntimeQueryController` (or any future endpoint
|
|
36
|
+
* that echoes the header) is the origin of this error. `null` when
|
|
37
|
+
* the failure is client-side (NetworkError / TimeoutError — the
|
|
38
|
+
* request never reached the server, so there's no server-issued
|
|
39
|
+
* ID to surface).
|
|
40
|
+
*
|
|
41
|
+
* Use this as a debugging trace anchor when filing a bug against a
|
|
42
|
+
* specific failed query — the matching row in `module_query_log`
|
|
43
|
+
* carries the full backend context (ast_hash, error_code,
|
|
44
|
+
* duration_ms, etc.).
|
|
45
|
+
*/
|
|
46
|
+
requestId;
|
|
47
|
+
constructor(code, message, options = {}) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = this.constructor.name;
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.status = options.status ?? 0;
|
|
52
|
+
this.retriable = options.retriable ?? false;
|
|
53
|
+
this.context = options.context;
|
|
54
|
+
this.requestId = options.requestId ?? null;
|
|
55
|
+
if (options.cause !== void 0) {
|
|
56
|
+
this.cause = options.cause;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var UsesUnboundReason = {
|
|
61
|
+
NOT_BOUND_YET: "NOT_BOUND_YET",
|
|
62
|
+
AMBIGUOUS_MATCH: "AMBIGUOUS_MATCH",
|
|
63
|
+
NO_MATCH: "NO_MATCH",
|
|
64
|
+
TABLE_DELETED: "TABLE_DELETED",
|
|
65
|
+
OPS_WIDENED_PENDING_CONSENT: "OPS_WIDENED_PENDING_CONSENT",
|
|
66
|
+
MANUALLY_UNBOUND: "MANUALLY_UNBOUND"
|
|
67
|
+
};
|
|
68
|
+
var UsesUnboundError = class extends DashwiseError {
|
|
69
|
+
constructor(message, context, requestId = null) {
|
|
70
|
+
super(DashwiseErrorCode.USES_UNBOUND, message, {
|
|
71
|
+
status: 409,
|
|
72
|
+
context,
|
|
73
|
+
requestId
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The manifest `uses[].tables[].slug` of the unbound slot (kebab-case,
|
|
78
|
+
* e.g. `employees`). Read from `context.uses_slug`; `undefined` if the
|
|
79
|
+
* backend omitted it.
|
|
80
|
+
*/
|
|
81
|
+
get usesSlug() {
|
|
82
|
+
const v = this.context?.uses_slug;
|
|
83
|
+
return typeof v === "string" ? v : void 0;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Why the slot is unbound — one of {@link UsesUnboundReason} (typed as
|
|
87
|
+
* `string` for forward-compat with reasons the backend may add). Read
|
|
88
|
+
* from `context.reason`; `undefined` if omitted.
|
|
89
|
+
*/
|
|
90
|
+
get reason() {
|
|
91
|
+
const v = this.context?.reason;
|
|
92
|
+
return typeof v === "string" ? v : void 0;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var UsesOpNotAllowedError = class extends DashwiseError {
|
|
96
|
+
constructor(message, context, requestId = null) {
|
|
97
|
+
super(DashwiseErrorCode.USES_OP_NOT_ALLOWED, message, {
|
|
98
|
+
status: 403,
|
|
99
|
+
context,
|
|
100
|
+
requestId
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** The `uses` slot slug the rejected op targeted. Read from `context.uses_slug`. */
|
|
104
|
+
get usesSlug() {
|
|
105
|
+
const v = this.context?.uses_slug;
|
|
106
|
+
return typeof v === "string" ? v : void 0;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The operation that was rejected — one of `create` / `read` /
|
|
110
|
+
* `update` / `delete`. Read from `context.op`; `undefined` if omitted.
|
|
111
|
+
*/
|
|
112
|
+
get op() {
|
|
113
|
+
const v = this.context?.op;
|
|
114
|
+
return typeof v === "string" ? v : void 0;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var ContractFieldMissingError = class extends DashwiseError {
|
|
118
|
+
constructor(message, context, requestId = null) {
|
|
119
|
+
super(DashwiseErrorCode.CONTRACT_FIELD_MISSING, message, {
|
|
120
|
+
status: 409,
|
|
121
|
+
context,
|
|
122
|
+
requestId
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** The `uses` slot slug whose contract broke. Read from `context.uses_slug`. */
|
|
126
|
+
get usesSlug() {
|
|
127
|
+
const v = this.context?.uses_slug;
|
|
128
|
+
return typeof v === "string" ? v : void 0;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The declared field slug (the `uses` side name) whose mapped physical
|
|
132
|
+
* field is gone. Read from `context.field`; `undefined` if omitted.
|
|
133
|
+
*/
|
|
134
|
+
get field() {
|
|
135
|
+
const v = this.context?.field;
|
|
136
|
+
return typeof v === "string" ? v : void 0;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export { ContractFieldMissingError, UsesOpNotAllowedError, UsesUnboundError, UsesUnboundReason };
|
|
141
|
+
//# sourceMappingURL=index.js.map
|
|
142
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/data/errors.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Error model for @dashai/sdk.\n *\n * The backend speaks a structured error envelope:\n *\n * {\n * \"code\": \"ROW_NOT_FOUND\",\n * \"message\": \"Row 123 not found in tasks\",\n * \"retriable\": false,\n * \"context\": { ... }\n * }\n *\n * This module wraps that envelope in a typed `DashwiseError` plus a small\n * set of subclasses for the codes consumers most commonly want to branch\n * on (network errors, auth failures, row-not-found, contract violations).\n *\n * Anything not matched by a specific subclass becomes a plain\n * `DashwiseError` carrying the raw `code`. Branch on `.code` for codes\n * we don't have a class for; use `instanceof` for the common cases.\n */\n\n/**\n * Known error codes. Not exhaustive — the backend emits many codes\n * specific to individual modules / endpoints. Use these constants when\n * you want compile-time safety; otherwise treat `err.code` as a string.\n *\n * The list is curated to the codes a module author is most likely to\n * branch on. Adding a new code here is a non-breaking change.\n */\nexport const DashwiseErrorCode = {\n // Auth / authz\n UNAUTHENTICATED: 'UNAUTHENTICATED',\n FORBIDDEN: 'FORBIDDEN',\n TOKEN_EXPIRED: 'TOKEN_EXPIRED',\n INSTALLATION_NOT_FOUND: 'INSTALLATION_NOT_FOUND',\n INSTALLATION_MODE_UNSUPPORTED: 'INSTALLATION_MODE_UNSUPPORTED',\n // Contract enforcement\n CONTRACT_VIOLATION: 'CONTRACT_VIOLATION',\n FIELD_NOT_READABLE: 'FIELD_NOT_READABLE',\n FILTER_OP_UNKNOWN: 'FILTER_OP_UNKNOWN',\n OPERATION_NOT_ALLOWED: 'OPERATION_NOT_ALLOWED',\n // Cross-module dependencies (Phase 1 of the SDK query plane —\n // see dashwise-devops-docs/plans/modules-sdk-query-v1.md).\n // Surfaced when reading borrowed tables via `client.deps(slug)`.\n DEPENDENCY_NOT_DECLARED: 'DEPENDENCY_NOT_DECLARED',\n TABLE_NOT_IN_CONTRACT: 'TABLE_NOT_IN_CONTRACT',\n PROVIDER_TABLE_MISSING: 'PROVIDER_TABLE_MISSING',\n OPERATION_NOT_ALLOWED_BY_PROVIDER: 'OPERATION_NOT_ALLOWED_BY_PROVIDER',\n // Cross-module dependency PARK state (Phase 3 modules-platform-v2 —\n // dependency connect/bind lifecycle). Surfaced at RUNTIME (409) when a\n // module reads a borrowed table (`client.deps(slug)` / `qb.deps.<slug>`)\n // or invokes a dep action whose `module_dependencies` row is currently\n // `status='unbound'` — the dependency is declared but not connected to a\n // provider installation (no provider in the workspace, version mismatch,\n // manually unbound, provider uninstalled, etc.). The consumer app should\n // render a \"connect your provider\" state rather than treating this as a\n // hard failure. `err.context` carries `{ provider, range, reason }`.\n DEP_UNBOUND: 'DEP_UNBOUND',\n // Workspace-table `uses` plane (Phase 4 modules-platform-v2 — the\n // declared/portable path for a module to read+write a WORKSPACE table\n // the workspace already owns, as opposed to `deps` = another module's\n // tables). Surfaced at RUNTIME against `client.uses(slug)` /\n // `qb.uses.<slug>` / the `POST /api/module-api/uses/:slug/...` REST\n // plane. The BINDING is the grant on this plane (for sandbox AND\n // api_key auth alike — a kind-local key does NOT bypass it):\n // - USES_UNBOUND 409 — the `uses` slot is declared but its\n // `module_table_bindings` row isn't `status='bound'` (never bound,\n // ambiguous match, no match, table trashed, ops widened pending\n // re-consent, manually unbound). `err.context` carries\n // `{ uses_slug, reason }`. Recoverable: a workspace admin binds a\n // table (`dashwise bind <uses_slug>`), then the read succeeds.\n // - USES_OP_NOT_ALLOWED 403 — the operation isn't in the binding's\n // enforcement set (manifest `ops` ∩ `consented_ops`). `err.context`\n // carries `{ uses_slug, op }`. Fix: widen the manifest ops +\n // re-consent (re-bind), or the admin re-binds to consent.\n // - CONTRACT_FIELD_MISSING 409 — a field the binding maps no longer\n // exists on the physical table (schema drift). `err.context`\n // carries `{ uses_slug, field }`. The binding is best-effort parked\n // on detection; an admin re-binds to a table that has the field.\n USES_UNBOUND: 'USES_UNBOUND',\n USES_OP_NOT_ALLOWED: 'USES_OP_NOT_ALLOWED',\n CONTRACT_FIELD_MISSING: 'CONTRACT_FIELD_MISSING',\n // `uses`-plane query joins are NOT supported in v1 — a\n // `qb.uses.<slug>.join(...)` / cross-plane join is rejected with a\n // typed 400. (Joins WITH a uses table as a target are v2 territory.)\n USES_JOIN_UNSUPPORTED: 'USES_JOIN_UNSUPPORTED',\n // Query-plane joins (Phase 3 — see\n // dashwise-devops-docs/plans/modules-sdk-query-v1-p3-execution.md).\n // Surfaced when `.join('<slug>')` / `.leftJoin('<slug>')` can't\n // resolve the target. Pre-declared in slice 3.1 so slice 3.2's\n // backend translator can emit them through fromBackendEnvelope\n // without an SDK roundtrip.\n LINK_FIELD_NOT_FOUND: 'LINK_FIELD_NOT_FOUND',\n NOT_A_LINK_FIELD: 'NOT_A_LINK_FIELD',\n JOIN_NOT_LINKED: 'JOIN_NOT_LINKED',\n // Cross-module actions (Phase 7 slice 7.3a — see\n // dashwise-devops-docs/plans/modules-sdk-query-v1-p7-cross-module-writes.md).\n // Surfaced when `qb.deps.<dep>.actions.<name>(input)` (generated\n // factory) dispatches against the backend endpoint\n // `POST /api/module-api/deps/:providerSlug/actions/:actionName`.\n // Slice 7.2b ships the backend emitter; slice 7.3a (this slice)\n // reserves the codes + maps them to DepActionError in the SDK.\n ACTION_NOT_EXPOSED: 'ACTION_NOT_EXPOSED',\n ACTION_NOT_DECLARED: 'ACTION_NOT_DECLARED',\n ACTION_INPUT_INVALID: 'ACTION_INPUT_INVALID',\n ACTION_OUTPUT_INVALID: 'ACTION_OUTPUT_INVALID',\n ACTION_RATE_LIMITED: 'ACTION_RATE_LIMITED',\n ACTION_EXECUTION_FAILED: 'ACTION_EXECUTION_FAILED',\n // Data plane\n ROW_NOT_FOUND: 'ROW_NOT_FOUND',\n TABLE_NOT_FOUND: 'TABLE_NOT_FOUND',\n VALIDATION_FAILED: 'VALIDATION_FAILED',\n ROW_LIMIT_EXCEEDED: 'ROW_LIMIT_EXCEEDED',\n STORAGE_LIMIT_EXCEEDED: 'STORAGE_LIMIT_EXCEEDED',\n // Outbound / runtime\n OUTBOUND_NOT_ALLOWED: 'OUTBOUND_NOT_ALLOWED',\n OUTBOUND_CONCURRENCY_LIMITED: 'OUTBOUND_CONCURRENCY_LIMITED',\n // Transport\n NETWORK_ERROR: 'NETWORK_ERROR',\n TIMEOUT: 'TIMEOUT',\n // Server\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n} as const;\n\nexport type DashwiseErrorCode = (typeof DashwiseErrorCode)[keyof typeof DashwiseErrorCode];\n\n/**\n * Base class for every error thrown by @dashai/sdk. Carries the\n * structured envelope from the backend (or a synthetic one for\n * client-side failures like network errors / timeouts).\n *\n * Always thrown — never returned as a value. The async client methods\n * `.list()` / `.get()` etc. reject with one of these on failure.\n */\nexport class DashwiseError extends Error {\n readonly code: string;\n readonly status: number;\n readonly retriable: boolean;\n readonly context: Record<string, unknown> | undefined;\n /**\n * Backend-issued trace anchor (Phase 6 slice 6.2, 2026-05-19).\n * Mirrors the `x-request-id` response header. Populated whenever\n * the backend's `RuntimeQueryController` (or any future endpoint\n * that echoes the header) is the origin of this error. `null` when\n * the failure is client-side (NetworkError / TimeoutError — the\n * request never reached the server, so there's no server-issued\n * ID to surface).\n *\n * Use this as a debugging trace anchor when filing a bug against a\n * specific failed query — the matching row in `module_query_log`\n * carries the full backend context (ast_hash, error_code,\n * duration_ms, etc.).\n */\n readonly requestId: string | null;\n\n constructor(\n code: string,\n message: string,\n options: {\n status?: number;\n retriable?: boolean;\n context?: Record<string, unknown>;\n cause?: unknown;\n /**\n * Phase 6 slice 6.2: backend `x-request-id` response header,\n * captured by the transport before throwing. Pass `undefined`\n * (or omit) for client-side errors that never hit the server.\n */\n requestId?: string | null;\n } = {},\n ) {\n super(message);\n this.name = this.constructor.name;\n this.code = code;\n this.status = options.status ?? 0;\n this.retriable = options.retriable ?? false;\n this.context = options.context;\n this.requestId = options.requestId ?? null;\n if (options.cause !== undefined) {\n // Node 18+ and modern browsers support Error.cause natively.\n // Use a property assignment because `super({ cause })` is awkward\n // when the base class only takes a string.\n (this as { cause?: unknown }).cause = options.cause;\n }\n }\n}\n\n/** Network-level failure (DNS, connection refused, TLS, fetch threw). */\nexport class NetworkError extends DashwiseError {\n constructor(message: string, cause?: unknown) {\n super(DashwiseErrorCode.NETWORK_ERROR, message, { retriable: true, cause });\n }\n}\n\n/** Request exceeded the configured timeout (`timeoutMs`). */\nexport class TimeoutError extends DashwiseError {\n constructor(message: string, timeoutMs: number) {\n super(DashwiseErrorCode.TIMEOUT, message, {\n retriable: true,\n context: { timeout_ms: timeoutMs },\n });\n }\n}\n\n/** 401 — token missing/invalid. */\nexport class UnauthenticatedError extends DashwiseError {\n constructor(message = 'Authentication required', requestId: string | null = null) {\n super(DashwiseErrorCode.UNAUTHENTICATED, message, { status: 401, requestId });\n }\n}\n\n/** 403 — caller lacks the role/scope for this operation. */\nexport class ForbiddenError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.FORBIDDEN, message, { status: 403, context, requestId });\n }\n}\n\n/** 404 — `GET /db/<table>/<id>` for a missing or soft-deleted row. */\nexport class RowNotFoundError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.ROW_NOT_FOUND, message, { status: 404, context, requestId });\n }\n}\n\n/**\n * Schema-level rejection: dep contract violated, table not exposed, field\n * not readable, filter op not allowed. Covers the family of \"you asked for\n * something the manifest doesn't permit\" errors.\n */\nexport class ContractViolationError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.CONTRACT_VIOLATION, message, {\n status: 403,\n context,\n requestId,\n });\n }\n}\n\n/** 422-ish — request body failed validation. Inspect `context` for field-level errors. */\nexport class ValidationError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.VALIDATION_FAILED, message, {\n status: 400,\n context,\n requestId,\n });\n }\n}\n\n// ── Cross-module dependency errors ─────────────────────────────────────\n//\n// Surfaced when calling `client.deps(slug).db<Row>(table).<op>(...)`.\n// Each subclass preserves the original backend `code` (unlike the older\n// `ContractViolationError` which collapses several codes onto a single\n// `CONTRACT_VIOLATION` string — that's a known wart we don't replicate\n// in new subclasses). Branch on `instanceof` for the common cases;\n// inspect `err.code` if you need to distinguish the two contract-error\n// codes (`DEPENDENCY_NOT_DECLARED` vs `TABLE_NOT_IN_CONTRACT`).\n\n/**\n * 404 — the caller's `module.json#dependencies` doesn't declare a\n * dependency on the requested provider module, OR the declared dep\n * exists but its `reads[]` block doesn't include the requested table.\n *\n * Either way the fix is on the **caller's** side: update the manifest\n * to declare the dep / add the table to `reads[]`, then re-publish.\n *\n * Covers both `DEPENDENCY_NOT_DECLARED` and `TABLE_NOT_IN_CONTRACT` —\n * inspect `err.code` to distinguish if you need different UX per case.\n */\nexport class DependencyContractError extends DashwiseError {\n constructor(\n code: string,\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(code, message, { status: 404, context, requestId });\n }\n}\n\n/**\n * 404 — the provider's physical table no longer exists. This means the\n * provider published a version that dropped the table while the\n * consumer's installation still pinned the old contract. Recovery is\n * on the **provider's** side (republish with the table) or the\n * workspace admin's (downgrade the provider, or upgrade the consumer\n * to a version whose `dependencies[]` no longer references it).\n */\nexport class ProviderTableMissingError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.PROVIDER_TABLE_MISSING, message, {\n status: 404,\n context,\n requestId,\n });\n }\n}\n\n/**\n * 403 — the provider's `exposes.operations.<verb>.allowed` is explicitly\n * `false` for the requested verb. Distinct from `OPERATION_NOT_ALLOWED`\n * (which is the **owner's** own-table policy) — `OPERATION_NOT_ALLOWED_BY_PROVIDER`\n * is the provider opting out of letting depending modules use a verb.\n *\n * No caller-side fix; either the provider needs to flip the gate, or\n * the consumer needs to find a different way to satisfy the use case.\n */\nexport class OperationNotAllowedByProviderError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.OPERATION_NOT_ALLOWED_BY_PROVIDER, message, {\n status: 403,\n context,\n requestId,\n });\n }\n}\n\n/**\n * The vocabulary of `unbound_reason` values a parked dependency can\n * carry, surfaced in {@link DepUnboundError.reason}. Mirrors the\n * backend's `module_dependencies.unbound_reason` enum (Phase 3\n * modules-platform-v2). Not exhaustive by design — treat `.reason` as a\n * string and branch on these constants when you want compile-time\n * safety. Adding a new reason is a non-breaking change.\n *\n * Resolve/bind-time reasons (why the dep never bound):\n * - `MISSING_DEPENDENCY` no install of the provider module in the workspace\n * - `DEPENDENCY_VERSION_MISMATCH` an install exists but its version is outside the declared range\n * - `PROVIDER_VERSION_RECORD_MISSING` the provider's version row is gone (data-integrity edge)\n * - `EXPOSES_MISSING` the provider no longer exposes a table/field the dep reads\n * - `EXPOSES_PRIVATE` the exposed surface is `private` to the consumer\n * - `ACTION_NOT_EXPOSED` a declared dep action isn't exposed by the provider (Rule #11)\n * - `PROVIDER_NOT_PRODUCTION` kind hygiene: a production consumer can only bind a production provider\n *\n * Lifecycle reasons (the dep bound once, then got parked):\n * - `MANUALLY_UNBOUND` a workspace admin ran `deps unbind` / the unbind endpoint\n * - `PROVIDER_UPGRADE_BROKE_CONTRACT` a provider upgrade (with `force`) broke this consumer's contract\n * - `PROVIDER_UNINSTALLED` the provider was uninstalled with `park_consumers`\n */\nexport const DepUnboundReason = {\n MISSING_DEPENDENCY: 'MISSING_DEPENDENCY',\n DEPENDENCY_VERSION_MISMATCH: 'DEPENDENCY_VERSION_MISMATCH',\n PROVIDER_VERSION_RECORD_MISSING: 'PROVIDER_VERSION_RECORD_MISSING',\n EXPOSES_MISSING: 'EXPOSES_MISSING',\n EXPOSES_PRIVATE: 'EXPOSES_PRIVATE',\n ACTION_NOT_EXPOSED: 'ACTION_NOT_EXPOSED',\n PROVIDER_NOT_PRODUCTION: 'PROVIDER_NOT_PRODUCTION',\n MANUALLY_UNBOUND: 'MANUALLY_UNBOUND',\n PROVIDER_UPGRADE_BROKE_CONTRACT: 'PROVIDER_UPGRADE_BROKE_CONTRACT',\n PROVIDER_UNINSTALLED: 'PROVIDER_UNINSTALLED',\n} as const;\n\nexport type DepUnboundReason =\n (typeof DepUnboundReason)[keyof typeof DepUnboundReason];\n\n/**\n * 409 — a runtime read (borrowed-table `list` / `get`, query-plane\n * `qb.deps.<slug>.<table>`, or a cross-module action) targeted a\n * dependency whose `module_dependencies` row is `status='unbound'`\n * (Phase 3 modules-platform-v2 dependency connect lifecycle).\n *\n * \"Unbound\" means the dependency is *declared* in the consumer's\n * manifest but not *connected* to a provider installation. It happens\n * when the resolver PARKED the dep at install/apply time (no provider\n * in the workspace, version mismatch, exposes/action coverage gap) or\n * when the dependency was later parked (manually unbound, provider\n * upgrade broke the contract, provider uninstalled).\n *\n * This is a *recoverable* state, not a bug in the consumer. Catch it and\n * render a \"connect your provider\" prompt — a workspace admin resolves it\n * by installing/binding the provider (`dashwise deps bind <slug>` or the\n * `POST /api/installations/:id/dependencies/:providerSlug/bind` endpoint).\n * Once bound, the same read succeeds with no code change.\n *\n * Convenience accessors ({@link DepUnboundError.provider},\n * {@link DepUnboundError.range}, {@link DepUnboundError.reason}) read the\n * backend's `context: { provider, range, reason }` so callers don't have\n * to reach into `err.context` by hand.\n *\n * @example\n * ```ts\n * import { DepUnboundError } from '@dashai/sdk';\n *\n * try {\n * const contacts = await client.deps('crm').db<ContactRow>('contacts').list();\n * // …or the typed builder: await qb.deps.crm.contacts.execute();\n * } catch (err) {\n * if (err instanceof DepUnboundError) {\n * // Declared but not connected — show a connect-your-provider CTA.\n * return <ConnectProvider provider={err.provider} range={err.range} reason={err.reason} />;\n * }\n * throw err;\n * }\n * ```\n */\nexport class DepUnboundError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.DEP_UNBOUND, message, {\n status: 409,\n context,\n requestId,\n });\n }\n\n /**\n * The provider module slug the parked dependency points at (kebab-case,\n * e.g. `crm`). Read from `context.provider`; `undefined` if the backend\n * omitted it.\n */\n get provider(): string | undefined {\n const v = this.context?.provider;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * The SemVer range the consumer's manifest declared for this dependency\n * (e.g. `^1.2.0`). Read from `context.range`; `undefined` if omitted.\n */\n get range(): string | undefined {\n const v = this.context?.range;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * Why the dependency is unbound — one of {@link DepUnboundReason} (but\n * typed as `string` for forward-compat with reasons the backend may add).\n * Read from `context.reason`; `undefined` if omitted.\n */\n get reason(): string | undefined {\n const v = this.context?.reason;\n return typeof v === 'string' ? v : undefined;\n }\n}\n\n// ── Workspace-table `uses` errors (Phase 4 modules-platform-v2) ────────\n//\n// The `uses` plane lets a module read+write a WORKSPACE table the\n// workspace already owns — the declared, portable counterpart to the\n// ad-hoc numeric-tableId Data Hub bridge. A module declares an ABSTRACT\n// contract in `module.json#uses[].tables[]` (slug + fields + abstract\n// types + ops); at install/bind time that slot is BOUND to a physical\n// workspace table via a `field_map` (declared field → physical field id).\n// The binding IS the grant: even a kind-local `dwk_` key can't touch a\n// uses table until its slot is bound.\n//\n// Surfaced against `client.uses(slug).db<Row>(table)` /\n// `qb.uses.<slug>.<table>` / the REST plane at\n// `POST /api/module-api/uses/:usesSlug/rows/...`. Same\n// preserve-the-code, convenience-accessor pattern as DepUnboundError.\n\n/**\n * The vocabulary of `reason` values a parked `uses` binding can carry,\n * surfaced in {@link UsesUnboundError.reason}. Mirrors the backend's\n * `module_table_bindings` unbound_reason enum for this plane\n * (Phase 4 modules-platform-v2). Not exhaustive by design — treat\n * `.reason` as a string; branch on these constants for compile-time\n * safety. Adding a new reason is non-breaking.\n *\n * - `NOT_BOUND_YET` the slot has never been bound (declared\n * but no workspace table connected yet)\n * - `AMBIGUOUS_MATCH` auto-match found >1 candidate table; a\n * human must pick one (`dashwise bind`)\n * - `NO_MATCH` auto-match found no candidate table\n * - `TABLE_DELETED` the bound table was trashed (or a mapped\n * field drifted away) → the row parked\n * - `OPS_WIDENED_PENDING_CONSENT` the manifest widened `ops` beyond what\n * was consented at bind time; re-bind to\n * refresh consent\n * - `MANUALLY_UNBOUND` a workspace admin ran `dashwise unbind`\n */\nexport const UsesUnboundReason = {\n NOT_BOUND_YET: 'NOT_BOUND_YET',\n AMBIGUOUS_MATCH: 'AMBIGUOUS_MATCH',\n NO_MATCH: 'NO_MATCH',\n TABLE_DELETED: 'TABLE_DELETED',\n OPS_WIDENED_PENDING_CONSENT: 'OPS_WIDENED_PENDING_CONSENT',\n MANUALLY_UNBOUND: 'MANUALLY_UNBOUND',\n} as const;\n\nexport type UsesUnboundReason =\n (typeof UsesUnboundReason)[keyof typeof UsesUnboundReason];\n\n/**\n * 409 — a runtime read/write against a `uses` table whose\n * `module_table_bindings` row is not `status='bound'` (Phase 4\n * modules-platform-v2 workspace-table binding lifecycle).\n *\n * \"Unbound\" means the workspace-table slot is *declared* in the module's\n * manifest `uses` block but not *connected* to a physical workspace\n * table. It happens when install-time auto-match parked the slot (no\n * candidate / ambiguous candidates / a write contract that never\n * auto-binds), the bound table was later trashed, the manifest widened\n * `ops` beyond consent, or an admin manually unbound it.\n *\n * A *recoverable* state, not a bug. Catch it and render a \"connect a\n * table\" prompt — a workspace admin resolves it by binding a workspace\n * table to the slot (`dashwise bind <uses_slug>` / the\n * `PUT /api/installations/:id/bindings/:usesSlug` endpoint). Once bound,\n * the same call succeeds with no code change.\n *\n * Convenience accessors read the backend's `context: { uses_slug, reason }`.\n *\n * @example\n * ```ts\n * import { UsesUnboundError } from '@dashai/sdk';\n *\n * try {\n * const staff = await client.uses('employees').db<EmployeeRow>('employees').list();\n * // …or the typed builder: await qb.uses.employees.execute();\n * } catch (err) {\n * if (err instanceof UsesUnboundError) {\n * return <BindWorkspaceTable slot={err.usesSlug} reason={err.reason} />;\n * }\n * throw err;\n * }\n * ```\n */\nexport class UsesUnboundError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.USES_UNBOUND, message, {\n status: 409,\n context,\n requestId,\n });\n }\n\n /**\n * The manifest `uses[].tables[].slug` of the unbound slot (kebab-case,\n * e.g. `employees`). Read from `context.uses_slug`; `undefined` if the\n * backend omitted it.\n */\n get usesSlug(): string | undefined {\n const v = this.context?.uses_slug;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * Why the slot is unbound — one of {@link UsesUnboundReason} (typed as\n * `string` for forward-compat with reasons the backend may add). Read\n * from `context.reason`; `undefined` if omitted.\n */\n get reason(): string | undefined {\n const v = this.context?.reason;\n return typeof v === 'string' ? v : undefined;\n }\n}\n\n/**\n * 403 — a `uses`-plane operation that isn't in the binding's enforced\n * operation set (the manifest's declared `ops` INTERSECT the admin's\n * `consented_ops` at bind time). E.g. the manifest declares\n * `ops: ['read','update']` but the admin consented only to `read` when\n * binding, so an `update` is rejected.\n *\n * Fix: either narrow the call to a consented op, or widen the manifest\n * `ops` + have an admin re-bind (which refreshes `consented_ops`).\n *\n * `err.op` / `err.usesSlug` read the backend's `context: { uses_slug, op }`.\n */\nexport class UsesOpNotAllowedError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.USES_OP_NOT_ALLOWED, message, {\n status: 403,\n context,\n requestId,\n });\n }\n\n /** The `uses` slot slug the rejected op targeted. Read from `context.uses_slug`. */\n get usesSlug(): string | undefined {\n const v = this.context?.uses_slug;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * The operation that was rejected — one of `create` / `read` /\n * `update` / `delete`. Read from `context.op`; `undefined` if omitted.\n */\n get op(): string | undefined {\n const v = this.context?.op;\n return typeof v === 'string' ? v : undefined;\n }\n}\n\n/**\n * 409 — a field the `uses` binding's `field_map` points at no longer\n * exists on the physical workspace table (schema drift: the table owner\n * deleted or renamed the column after the bind). The backend best-effort\n * parks the binding (reason `TABLE_DELETED`/field-drift) on detection.\n *\n * Recovery is the workspace admin's: re-bind the slot to a table that\n * still has the field, or re-map the field (`dashwise bind <uses_slug>\n * --map <usesField>=<physicalField>`).\n *\n * `err.field` / `err.usesSlug` read the backend's\n * `context: { uses_slug, field }`.\n */\nexport class ContractFieldMissingError extends DashwiseError {\n constructor(\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(DashwiseErrorCode.CONTRACT_FIELD_MISSING, message, {\n status: 409,\n context,\n requestId,\n });\n }\n\n /** The `uses` slot slug whose contract broke. Read from `context.uses_slug`. */\n get usesSlug(): string | undefined {\n const v = this.context?.uses_slug;\n return typeof v === 'string' ? v : undefined;\n }\n\n /**\n * The declared field slug (the `uses` side name) whose mapped physical\n * field is gone. Read from `context.field`; `undefined` if omitted.\n */\n get field(): string | undefined {\n const v = this.context?.field;\n return typeof v === 'string' ? v : undefined;\n }\n}\n\n// ── Join errors (Phase 3) ──────────────────────────────────────────────\n//\n// Surfaced when `.join('<slug>')` / `.leftJoin('<slug>')` on the\n// generated `qb` builder can't resolve the target against the\n// manifest. Pre-declared in slice 3.1; raised by the backend\n// translator in slice 3.2. Same one-subclass-with-preserved-code\n// pattern as DependencyContractError so authors can switch on\n// `err.code` for the three sub-cases:\n//\n// LINK_FIELD_NOT_FOUND `.join('xyz')` where `xyz` isn't a field\n// on the source table. Fix: pass an\n// explicit `on` argument, or correct the slug.\n//\n// NOT_A_LINK_FIELD `.join('title')` where `title` exists on\n// the source table but its type isn't\n// `link_row`. Fix: use a real link field,\n// or pass explicit `on`.\n//\n// JOIN_NOT_LINKED `.join('sibling_table')` where the target\n// is a sibling table but no link field\n// bridges them. Fix: pass explicit `on`.\n\n/**\n * 400 — `.join()` / `.leftJoin()` referenced a target the manifest\n * can't resolve. Covers the three sub-cases above; inspect\n * `err.code` to distinguish.\n *\n * Author-side fix in every case: either correct the slug, or pass\n * an explicit `on` argument to bypass link-field inference.\n */\nexport class JoinError extends DashwiseError {\n constructor(\n code: string,\n message: string,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(code, message, { status: 400, context, requestId });\n }\n}\n\n// ── Cross-module action errors (Phase 7 slice 7.3a, 2026-05-20) ──────\n//\n// Surfaced by the codegen-emitted `qb.deps.<dep>.actions.<name>(input)`\n// factory when the backend's cross-module dispatch\n// (`CrossModuleActionService.dispatch()`) rejects. Six sub-cases all\n// covered by `DepActionError`; inspect `err.code` to discriminate.\n//\n// ACTION_NOT_EXPOSED 403 — provider's manifest doesn't expose\n// the action, OR visibility='private'.\n// Recovery: provider author needs to\n// publish a version that exposes it.\n// ACTION_NOT_DECLARED 403 — consumer's manifest doesn't list\n// the action in dependencies[].actions[].\n// Recovery: add it + republish.\n// ACTION_INPUT_INVALID 400 — input fails Ajv against the\n// provider's declared input schema.\n// Recovery: fix the call site (typical\n// with stale codegen + drift).\n// ACTION_OUTPUT_INVALID 500 — provider returned a value that fails\n// the declared output schema. This is a\n// PROVIDER-author bug; consumer can\n// only file an issue.\n// ACTION_RATE_LIMITED 429 — per-actor or per-caller-installation\n// rate limit exceeded. Retry with\n// backoff after Retry-After (slice 7.4).\n// ACTION_EXECUTION_FAILED 200-envelope — action body threw, timed out,\n// or OOM-ed. Provider-author bug;\n// trace surfaced in err.context.trace.\n\n/**\n * Single subclass for every cross-module action failure. The code\n * (and HTTP status) varies per sub-case; the wrapping class lets\n * consumers `catch (e) { if (e instanceof DepActionError) {...} }`\n * regardless of which sub-case fired.\n */\nexport class DepActionError extends DashwiseError {\n constructor(\n code: string,\n message: string,\n status: number,\n context?: Record<string, unknown>,\n requestId: string | null = null,\n ) {\n super(code, message, {\n status,\n retriable: code === DashwiseErrorCode.ACTION_RATE_LIMITED,\n context,\n requestId,\n });\n }\n}\n\n// ── Factory: backend envelope → typed error ────────────────────────────\n\ninterface BackendErrorEnvelope {\n code?: string;\n message?: string;\n retriable?: boolean;\n context?: Record<string, unknown>;\n}\n\n/**\n * Build a typed `DashwiseError` from the backend's response envelope.\n *\n * The mapping picks the most specific subclass for codes we have a class\n * for; everything else becomes a plain `DashwiseError` carrying the raw\n * code. This keeps the type set small while still letting consumers\n * branch on string codes when they want to.\n *\n * Phase 6 slice 6.2 (2026-05-19): accepts an optional `requestId` —\n * the backend's `x-request-id` response header — and threads it onto\n * every produced error so consumers can use it as a debugging trace\n * anchor. Default `null` for backward compat; the transport always\n * passes it through.\n */\nexport function fromBackendEnvelope(\n status: number,\n envelope: BackendErrorEnvelope | null,\n requestId: string | null = null,\n): DashwiseError {\n const code = envelope?.code ?? codeFromStatus(status);\n const message = envelope?.message ?? `HTTP ${status}`;\n const context = envelope?.context;\n\n switch (code) {\n case DashwiseErrorCode.UNAUTHENTICATED:\n case DashwiseErrorCode.TOKEN_EXPIRED:\n return new UnauthenticatedError(message, requestId);\n case DashwiseErrorCode.FORBIDDEN:\n return new ForbiddenError(message, context, requestId);\n case DashwiseErrorCode.ROW_NOT_FOUND:\n return new RowNotFoundError(message, context, requestId);\n case DashwiseErrorCode.CONTRACT_VIOLATION:\n case DashwiseErrorCode.FIELD_NOT_READABLE:\n case DashwiseErrorCode.FILTER_OP_UNKNOWN:\n case DashwiseErrorCode.OPERATION_NOT_ALLOWED:\n return new ContractViolationError(message, context, requestId);\n case DashwiseErrorCode.VALIDATION_FAILED:\n return new ValidationError(message, context, requestId);\n case DashwiseErrorCode.DEPENDENCY_NOT_DECLARED:\n case DashwiseErrorCode.TABLE_NOT_IN_CONTRACT:\n return new DependencyContractError(code, message, context, requestId);\n case DashwiseErrorCode.PROVIDER_TABLE_MISSING:\n return new ProviderTableMissingError(message, context, requestId);\n case DashwiseErrorCode.OPERATION_NOT_ALLOWED_BY_PROVIDER:\n return new OperationNotAllowedByProviderError(message, context, requestId);\n case DashwiseErrorCode.DEP_UNBOUND:\n return new DepUnboundError(message, context, requestId);\n case DashwiseErrorCode.USES_UNBOUND:\n return new UsesUnboundError(message, context, requestId);\n case DashwiseErrorCode.USES_OP_NOT_ALLOWED:\n return new UsesOpNotAllowedError(message, context, requestId);\n case DashwiseErrorCode.CONTRACT_FIELD_MISSING:\n return new ContractFieldMissingError(message, context, requestId);\n case DashwiseErrorCode.LINK_FIELD_NOT_FOUND:\n case DashwiseErrorCode.NOT_A_LINK_FIELD:\n case DashwiseErrorCode.JOIN_NOT_LINKED:\n // `uses`-plane joins are unsupported in v1; the backend emits a 400\n // with this code. Map to JoinError so it lands in the same\n // \"your join can't be resolved\" family (code preserved for `err.code`).\n case DashwiseErrorCode.USES_JOIN_UNSUPPORTED:\n return new JoinError(code, message, context, requestId);\n case DashwiseErrorCode.ACTION_NOT_EXPOSED:\n case DashwiseErrorCode.ACTION_NOT_DECLARED:\n return new DepActionError(code, message, 403, context, requestId);\n case DashwiseErrorCode.ACTION_INPUT_INVALID:\n return new DepActionError(code, message, 400, context, requestId);\n case DashwiseErrorCode.ACTION_OUTPUT_INVALID:\n return new DepActionError(code, message, 500, context, requestId);\n case DashwiseErrorCode.ACTION_RATE_LIMITED:\n return new DepActionError(code, message, 429, context, requestId);\n case DashwiseErrorCode.ACTION_EXECUTION_FAILED:\n // ACTION_EXECUTION_FAILED arrives as a 200 envelope from the\n // backend (the host succeeded; the action body crashed). The\n // _callAction helper unwraps the envelope and synthesizes this\n // via the factory call — status 200 reflects \"the transport was\n // fine; the failure is in the application layer\".\n return new DepActionError(code, message, 200, context, requestId);\n default:\n return new DashwiseError(code, message, {\n status,\n retriable: envelope?.retriable ?? false,\n context,\n requestId,\n });\n }\n}\n\nfunction codeFromStatus(status: number): string {\n if (status === 401) return DashwiseErrorCode.UNAUTHENTICATED;\n if (status === 403) return DashwiseErrorCode.FORBIDDEN;\n if (status === 404) return DashwiseErrorCode.ROW_NOT_FOUND;\n if (status >= 500) return DashwiseErrorCode.INTERNAL_ERROR;\n return 'HTTP_ERROR';\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashai/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Runtime client for DashWise: typed data access + auth helpers for modules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
"import": "./dist/deps/index.js",
|
|
45
45
|
"require": "./dist/deps/index.cjs"
|
|
46
46
|
},
|
|
47
|
+
"./uses": {
|
|
48
|
+
"types": "./dist/uses/index.d.ts",
|
|
49
|
+
"import": "./dist/uses/index.js",
|
|
50
|
+
"require": "./dist/uses/index.cjs"
|
|
51
|
+
},
|
|
47
52
|
"./query": {
|
|
48
53
|
"types": "./dist/query/index.d.ts",
|
|
49
54
|
"import": "./dist/query/index.js",
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Error model for @dashai/sdk.
|
|
3
|
-
*
|
|
4
|
-
* The backend speaks a structured error envelope:
|
|
5
|
-
*
|
|
6
|
-
* {
|
|
7
|
-
* "code": "ROW_NOT_FOUND",
|
|
8
|
-
* "message": "Row 123 not found in tasks",
|
|
9
|
-
* "retriable": false,
|
|
10
|
-
* "context": { ... }
|
|
11
|
-
* }
|
|
12
|
-
*
|
|
13
|
-
* This module wraps that envelope in a typed `DashwiseError` plus a small
|
|
14
|
-
* set of subclasses for the codes consumers most commonly want to branch
|
|
15
|
-
* on (network errors, auth failures, row-not-found, contract violations).
|
|
16
|
-
*
|
|
17
|
-
* Anything not matched by a specific subclass becomes a plain
|
|
18
|
-
* `DashwiseError` carrying the raw `code`. Branch on `.code` for codes
|
|
19
|
-
* we don't have a class for; use `instanceof` for the common cases.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* Known error codes. Not exhaustive — the backend emits many codes
|
|
23
|
-
* specific to individual modules / endpoints. Use these constants when
|
|
24
|
-
* you want compile-time safety; otherwise treat `err.code` as a string.
|
|
25
|
-
*
|
|
26
|
-
* The list is curated to the codes a module author is most likely to
|
|
27
|
-
* branch on. Adding a new code here is a non-breaking change.
|
|
28
|
-
*/
|
|
29
|
-
declare const DashwiseErrorCode: {
|
|
30
|
-
readonly UNAUTHENTICATED: "UNAUTHENTICATED";
|
|
31
|
-
readonly FORBIDDEN: "FORBIDDEN";
|
|
32
|
-
readonly TOKEN_EXPIRED: "TOKEN_EXPIRED";
|
|
33
|
-
readonly INSTALLATION_NOT_FOUND: "INSTALLATION_NOT_FOUND";
|
|
34
|
-
readonly INSTALLATION_MODE_UNSUPPORTED: "INSTALLATION_MODE_UNSUPPORTED";
|
|
35
|
-
readonly CONTRACT_VIOLATION: "CONTRACT_VIOLATION";
|
|
36
|
-
readonly FIELD_NOT_READABLE: "FIELD_NOT_READABLE";
|
|
37
|
-
readonly FILTER_OP_UNKNOWN: "FILTER_OP_UNKNOWN";
|
|
38
|
-
readonly OPERATION_NOT_ALLOWED: "OPERATION_NOT_ALLOWED";
|
|
39
|
-
readonly DEPENDENCY_NOT_DECLARED: "DEPENDENCY_NOT_DECLARED";
|
|
40
|
-
readonly TABLE_NOT_IN_CONTRACT: "TABLE_NOT_IN_CONTRACT";
|
|
41
|
-
readonly PROVIDER_TABLE_MISSING: "PROVIDER_TABLE_MISSING";
|
|
42
|
-
readonly OPERATION_NOT_ALLOWED_BY_PROVIDER: "OPERATION_NOT_ALLOWED_BY_PROVIDER";
|
|
43
|
-
readonly LINK_FIELD_NOT_FOUND: "LINK_FIELD_NOT_FOUND";
|
|
44
|
-
readonly NOT_A_LINK_FIELD: "NOT_A_LINK_FIELD";
|
|
45
|
-
readonly JOIN_NOT_LINKED: "JOIN_NOT_LINKED";
|
|
46
|
-
readonly ACTION_NOT_EXPOSED: "ACTION_NOT_EXPOSED";
|
|
47
|
-
readonly ACTION_NOT_DECLARED: "ACTION_NOT_DECLARED";
|
|
48
|
-
readonly ACTION_INPUT_INVALID: "ACTION_INPUT_INVALID";
|
|
49
|
-
readonly ACTION_OUTPUT_INVALID: "ACTION_OUTPUT_INVALID";
|
|
50
|
-
readonly ACTION_RATE_LIMITED: "ACTION_RATE_LIMITED";
|
|
51
|
-
readonly ACTION_EXECUTION_FAILED: "ACTION_EXECUTION_FAILED";
|
|
52
|
-
readonly ROW_NOT_FOUND: "ROW_NOT_FOUND";
|
|
53
|
-
readonly TABLE_NOT_FOUND: "TABLE_NOT_FOUND";
|
|
54
|
-
readonly VALIDATION_FAILED: "VALIDATION_FAILED";
|
|
55
|
-
readonly ROW_LIMIT_EXCEEDED: "ROW_LIMIT_EXCEEDED";
|
|
56
|
-
readonly STORAGE_LIMIT_EXCEEDED: "STORAGE_LIMIT_EXCEEDED";
|
|
57
|
-
readonly OUTBOUND_NOT_ALLOWED: "OUTBOUND_NOT_ALLOWED";
|
|
58
|
-
readonly OUTBOUND_CONCURRENCY_LIMITED: "OUTBOUND_CONCURRENCY_LIMITED";
|
|
59
|
-
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
60
|
-
readonly TIMEOUT: "TIMEOUT";
|
|
61
|
-
readonly INTERNAL_ERROR: "INTERNAL_ERROR";
|
|
62
|
-
};
|
|
63
|
-
type DashwiseErrorCode = (typeof DashwiseErrorCode)[keyof typeof DashwiseErrorCode];
|
|
64
|
-
/**
|
|
65
|
-
* Base class for every error thrown by @dashai/sdk. Carries the
|
|
66
|
-
* structured envelope from the backend (or a synthetic one for
|
|
67
|
-
* client-side failures like network errors / timeouts).
|
|
68
|
-
*
|
|
69
|
-
* Always thrown — never returned as a value. The async client methods
|
|
70
|
-
* `.list()` / `.get()` etc. reject with one of these on failure.
|
|
71
|
-
*/
|
|
72
|
-
declare class DashwiseError extends Error {
|
|
73
|
-
readonly code: string;
|
|
74
|
-
readonly status: number;
|
|
75
|
-
readonly retriable: boolean;
|
|
76
|
-
readonly context: Record<string, unknown> | undefined;
|
|
77
|
-
/**
|
|
78
|
-
* Backend-issued trace anchor (Phase 6 slice 6.2, 2026-05-19).
|
|
79
|
-
* Mirrors the `x-request-id` response header. Populated whenever
|
|
80
|
-
* the backend's `RuntimeQueryController` (or any future endpoint
|
|
81
|
-
* that echoes the header) is the origin of this error. `null` when
|
|
82
|
-
* the failure is client-side (NetworkError / TimeoutError — the
|
|
83
|
-
* request never reached the server, so there's no server-issued
|
|
84
|
-
* ID to surface).
|
|
85
|
-
*
|
|
86
|
-
* Use this as a debugging trace anchor when filing a bug against a
|
|
87
|
-
* specific failed query — the matching row in `module_query_log`
|
|
88
|
-
* carries the full backend context (ast_hash, error_code,
|
|
89
|
-
* duration_ms, etc.).
|
|
90
|
-
*/
|
|
91
|
-
readonly requestId: string | null;
|
|
92
|
-
constructor(code: string, message: string, options?: {
|
|
93
|
-
status?: number;
|
|
94
|
-
retriable?: boolean;
|
|
95
|
-
context?: Record<string, unknown>;
|
|
96
|
-
cause?: unknown;
|
|
97
|
-
/**
|
|
98
|
-
* Phase 6 slice 6.2: backend `x-request-id` response header,
|
|
99
|
-
* captured by the transport before throwing. Pass `undefined`
|
|
100
|
-
* (or omit) for client-side errors that never hit the server.
|
|
101
|
-
*/
|
|
102
|
-
requestId?: string | null;
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
/** Network-level failure (DNS, connection refused, TLS, fetch threw). */
|
|
106
|
-
declare class NetworkError extends DashwiseError {
|
|
107
|
-
constructor(message: string, cause?: unknown);
|
|
108
|
-
}
|
|
109
|
-
/** Request exceeded the configured timeout (`timeoutMs`). */
|
|
110
|
-
declare class TimeoutError extends DashwiseError {
|
|
111
|
-
constructor(message: string, timeoutMs: number);
|
|
112
|
-
}
|
|
113
|
-
/** 401 — token missing/invalid. */
|
|
114
|
-
declare class UnauthenticatedError extends DashwiseError {
|
|
115
|
-
constructor(message?: string, requestId?: string | null);
|
|
116
|
-
}
|
|
117
|
-
/** 403 — caller lacks the role/scope for this operation. */
|
|
118
|
-
declare class ForbiddenError extends DashwiseError {
|
|
119
|
-
constructor(message: string, context?: Record<string, unknown>, requestId?: string | null);
|
|
120
|
-
}
|
|
121
|
-
/** 404 — `GET /db/<table>/<id>` for a missing or soft-deleted row. */
|
|
122
|
-
declare class RowNotFoundError extends DashwiseError {
|
|
123
|
-
constructor(message: string, context?: Record<string, unknown>, requestId?: string | null);
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Schema-level rejection: dep contract violated, table not exposed, field
|
|
127
|
-
* not readable, filter op not allowed. Covers the family of "you asked for
|
|
128
|
-
* something the manifest doesn't permit" errors.
|
|
129
|
-
*/
|
|
130
|
-
declare class ContractViolationError extends DashwiseError {
|
|
131
|
-
constructor(message: string, context?: Record<string, unknown>, requestId?: string | null);
|
|
132
|
-
}
|
|
133
|
-
/** 422-ish — request body failed validation. Inspect `context` for field-level errors. */
|
|
134
|
-
declare class ValidationError extends DashwiseError {
|
|
135
|
-
constructor(message: string, context?: Record<string, unknown>, requestId?: string | null);
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* 404 — the caller's `module.json#dependencies` doesn't declare a
|
|
139
|
-
* dependency on the requested provider module, OR the declared dep
|
|
140
|
-
* exists but its `reads[]` block doesn't include the requested table.
|
|
141
|
-
*
|
|
142
|
-
* Either way the fix is on the **caller's** side: update the manifest
|
|
143
|
-
* to declare the dep / add the table to `reads[]`, then re-publish.
|
|
144
|
-
*
|
|
145
|
-
* Covers both `DEPENDENCY_NOT_DECLARED` and `TABLE_NOT_IN_CONTRACT` —
|
|
146
|
-
* inspect `err.code` to distinguish if you need different UX per case.
|
|
147
|
-
*/
|
|
148
|
-
declare class DependencyContractError extends DashwiseError {
|
|
149
|
-
constructor(code: string, message: string, context?: Record<string, unknown>, requestId?: string | null);
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* 404 — the provider's physical table no longer exists. This means the
|
|
153
|
-
* provider published a version that dropped the table while the
|
|
154
|
-
* consumer's installation still pinned the old contract. Recovery is
|
|
155
|
-
* on the **provider's** side (republish with the table) or the
|
|
156
|
-
* workspace admin's (downgrade the provider, or upgrade the consumer
|
|
157
|
-
* to a version whose `dependencies[]` no longer references it).
|
|
158
|
-
*/
|
|
159
|
-
declare class ProviderTableMissingError extends DashwiseError {
|
|
160
|
-
constructor(message: string, context?: Record<string, unknown>, requestId?: string | null);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* 403 — the provider's `exposes.operations.<verb>.allowed` is explicitly
|
|
164
|
-
* `false` for the requested verb. Distinct from `OPERATION_NOT_ALLOWED`
|
|
165
|
-
* (which is the **owner's** own-table policy) — `OPERATION_NOT_ALLOWED_BY_PROVIDER`
|
|
166
|
-
* is the provider opting out of letting depending modules use a verb.
|
|
167
|
-
*
|
|
168
|
-
* No caller-side fix; either the provider needs to flip the gate, or
|
|
169
|
-
* the consumer needs to find a different way to satisfy the use case.
|
|
170
|
-
*/
|
|
171
|
-
declare class OperationNotAllowedByProviderError extends DashwiseError {
|
|
172
|
-
constructor(message: string, context?: Record<string, unknown>, requestId?: string | null);
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* 400 — `.join()` / `.leftJoin()` referenced a target the manifest
|
|
176
|
-
* can't resolve. Covers the three sub-cases above; inspect
|
|
177
|
-
* `err.code` to distinguish.
|
|
178
|
-
*
|
|
179
|
-
* Author-side fix in every case: either correct the slug, or pass
|
|
180
|
-
* an explicit `on` argument to bypass link-field inference.
|
|
181
|
-
*/
|
|
182
|
-
declare class JoinError extends DashwiseError {
|
|
183
|
-
constructor(code: string, message: string, context?: Record<string, unknown>, requestId?: string | null);
|
|
184
|
-
}
|
|
185
|
-
interface BackendErrorEnvelope {
|
|
186
|
-
code?: string;
|
|
187
|
-
message?: string;
|
|
188
|
-
retriable?: boolean;
|
|
189
|
-
context?: Record<string, unknown>;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Build a typed `DashwiseError` from the backend's response envelope.
|
|
193
|
-
*
|
|
194
|
-
* The mapping picks the most specific subclass for codes we have a class
|
|
195
|
-
* for; everything else becomes a plain `DashwiseError` carrying the raw
|
|
196
|
-
* code. This keeps the type set small while still letting consumers
|
|
197
|
-
* branch on string codes when they want to.
|
|
198
|
-
*
|
|
199
|
-
* Phase 6 slice 6.2 (2026-05-19): accepts an optional `requestId` —
|
|
200
|
-
* the backend's `x-request-id` response header — and threads it onto
|
|
201
|
-
* every produced error so consumers can use it as a debugging trace
|
|
202
|
-
* anchor. Default `null` for backward compat; the transport always
|
|
203
|
-
* passes it through.
|
|
204
|
-
*/
|
|
205
|
-
declare function fromBackendEnvelope(status: number, envelope: BackendErrorEnvelope | null, requestId?: string | null): DashwiseError;
|
|
206
|
-
|
|
207
|
-
export { ContractViolationError as C, DependencyContractError as D, ForbiddenError as F, JoinError as J, NetworkError as N, OperationNotAllowedByProviderError as O, ProviderTableMissingError as P, RowNotFoundError as R, TimeoutError as T, UnauthenticatedError as U, ValidationError as V, DashwiseError as a, DashwiseErrorCode as b, fromBackendEnvelope as f };
|