import { ObjectStackClient } from '@objectstack/client'; import { DroppedFieldsEvent } from '@objectstack/spec/data'; export { DroppedFieldsEvent } from '@objectstack/spec/data'; import { GetMetaItemLayeredResponse, RuntimeAuthoringIssue } from '@objectstack/spec/api'; export { RuntimeAuthoringIssue } from '@objectstack/spec/api'; import { DatasetSelection, AnalyticsResult } from '@objectstack/spec/contracts'; import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, DeleteViewResult, FileUploadResult } from '@object-ui/types'; export { DeleteViewResult, FileUploadResult, ViewHomeDeleteOutcome } from '@object-ui/types'; import { DatasetDrillRange } from '@object-ui/core'; /** * ObjectUI * Copyright (c) 2024-present ObjectStack Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** * MetadataClient * * Thin, framework-agnostic HTTP client for the ObjectStack metadata * API (`/api/v1/meta/*`). Used by the platform Setup app surfaces in * `@object-ui/plugin-designer` (Object Manager, Field Designer, etc.) * so they can read and write protocol metadata without depending on * `ObjectStackAdapter` (which is a generic data-source binding and * carries a lot of view/dashboard-specific behaviour we don't need * here). * * Endpoints (see `packages/rest/src/rest-server.ts`): * GET /api/v1/meta — list all metadata types * GET /api/v1/meta/:type — list items of a type * GET /api/v1/meta/:type/:name — get one item (returns the * unwrapped item content) * PUT /api/v1/meta/:type/:name — save (overlay) one item * honours `If-Match` for OCC * DELETE /api/v1/meta/:type/:name — reset overlay to artifact * GET /api/v1/meta/:type/:name/history — durable change log * * The client deliberately keeps the response shape opaque (typed as * `unknown`/generic `T`) so callers explicitly narrow per-metadata- * type, mirroring the framework's "single Zod source per type" rule. */ /** * Emitted after a {@link MetadataClient.save} whose response carried a * non-empty `advisories` array. The save SUCCEEDED — the row persisted and the * server returned 200 — so this is advisory, never a failure. * * Deliberately the same shape of seam as `ObjectStackAdapter.onWriteWarning` * (#3431/#3455): a successful write whose response carries something the author * needs to be told, surfaced to the shell as an event so the data layer never * imports a toaster. The difference is only which door produced it — that one * is record CRUD, this one is the metadata save door. */ interface MetadataSaveAdvisoryEvent { /** Metadata type saved (e.g. `'flow'`). */ type: string; /** Item name saved. */ name: string; /** * The save mode the call used. `'draft'` writes are **never gated** — the * framework returns at `runtime-authoring-gate.ts`'s D1 early-return * (`if (args.state !== 'active') return null`), so a draft save produces no * findings at all and this event never fires for one. Carried anyway so a * consumer reading the event can tell which door it came through. */ mode: 'draft' | 'publish'; /** The findings. Never empty — the event is not emitted otherwise. */ advisories: RuntimeAuthoringIssue[]; } /** Event listener type for save-advisory events. */ type MetadataSaveAdvisoryListener = (event: MetadataSaveAdvisoryEvent) => void; /** * Read the `advisories` array off a save response, defensively. * * The server omits the key entirely on a clean save, so `undefined` is the * common case and means "nothing to say". Anything that is not an array of * objects carrying the six required keys is dropped rather than rendered: a * half-shaped finding would print blanks at the author, and this channel must * never turn a successful save into noise. */ declare function readSaveAdvisories(body: unknown): RuntimeAuthoringIssue[]; interface MetadataClientConfig { /** Base URL of the ObjectStack server (no trailing slash needed). */ baseUrl: string; /** * Optional environment ID to scope reads/writes to a tenant. When * provided, requests use the scoped path * `/api/v1/environments/:environmentId/meta/...` and overlays are * persisted in the environment's own metadata store. */ environmentId?: string; /** Optional custom fetch implementation (defaults to `globalThis.fetch`). */ fetch?: typeof fetch; /** Additional headers (e.g. auth) included on every request. */ headers?: Record; /** * ADR-0037 Live Canvas — render the world **as-if-published**. When true, * `list()` and `get()` append `?preview=draft`, which the framework * dispatcher maps to `getMetaItems/getMetaItem({ previewDrafts: true })`: * pending ADR-0033 drafts overlay the active registry (draft wins by name, * draft-only items surface). Reads only — writes are unaffected, and * nothing the preview shows is live until Publish. */ previewDrafts?: boolean; /** * Called after a {@link MetadataClient.save} whose 2xx response carried a * non-empty `advisories` array (objectstack#7435). The save already * succeeded; this is how the shell learns there is something to tell the * author instead of the findings being discarded client-side. * * Set on the CONFIG rather than exposed as a `subscribe()` method on purpose: * console metadata clients are minted per-component by `useMetadataClient`, * so there is no long-lived instance to subscribe to — but every one of them * is built by the single `createConsoleMetadataClient` factory, which is * where this gets wired exactly once for every save call site in the app. * * A throw from this callback is swallowed — an advisory channel must never * be able to fail a save that the server already committed. */ onSaveAdvisory?: MetadataSaveAdvisoryListener; } interface MetadataListOptions { /** Filter by source package id (matches the `package` query param). */ packageId?: string; } /** * A pending DRAFT metadata item (ADR-0033), as returned by {@link MetadataClient.listDrafts}. * Light header — no body — carrying the owning package so the console can group * pending changes by app package. */ interface MetadataDraftHeader { type: string; name: string; packageId: string | null; updatedAt: string | null; updatedBy: string | null; } /** * Options for {@link MetadataClient.save} — a WRITE OVER HTTP to * `/api/v1/meta/:type/:name`. * * NOT the spec's `MetadataSaveOptions` (`@objectstack/spec/system` and * `/kernel`), whose name this interface wore until objectui#3160 * (objectstack#4115 ledger batch 6). Both spec copies describe writing a * metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`, * `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys * exists here, and not one of these exists there: this is the REST client's * request envelope — optimistic concurrency (`ifMatch` → `If-Match`), the * destructive-change override, the ADR-0033 draft/publish mode, and the * owning package. Same words, different layer. */ interface MetadataClientSaveOptions { /** * Optimistic concurrency token (the `checksum` returned by the last * read). When present, sent as the `If-Match` header so concurrent * edits get a 409 instead of overwriting each other. */ ifMatch?: string; /** * Bypass destructive-change protection (Phase 3a). The server returns * `409 destructive_change` with `issues[]` if a write would drop or * narrow data; setting `force: true` adds `?force=true` to the request * so the operator can confirm and proceed. */ force?: boolean; /** * Save mode — `'draft'` writes a pending draft row (invisible to the * runtime until `publish()` is called); `'publish'` writes directly * to the active overlay. Omit (or set `'publish'`) for the legacy * "save = live" behaviour. */ mode?: 'draft' | 'publish'; /** * Software-package id to bind the saved row to (sent as the `package` * query param → `sys_metadata.package_id`). Set when authoring inside a * Studio package workspace. Omit for an env-local overlay. */ packageId?: string; } interface MetadataGetOptions { /** * Read a specific overlay state. `'draft'` returns the pending draft * body (no fallback to the published overlay or the registry); omit * to read the active (published) value. */ state?: 'active' | 'draft'; /** * Software-package id to scope resolution (sent as the `package` query * param → server prefer-local, ADR-0048). Set when reading one item that * may collide by name across installed packages (e.g. the Studio editor * passes the edited item's owning package). Omit for context-free reads. */ packageId?: string; } interface MetadataDeleteOptions extends MetadataClientSaveOptions { /** * Target state. `'draft'` discards the pending draft (keeps the * published overlay intact). Omit to reset the active overlay back * to the artifact default (the legacy behaviour). */ state?: 'active' | 'draft'; } /** * Which layer an overlay was saved at — `'org'` for a tenant overlay, `'env'` * for an environment-level one, `null` exactly when `overlay` is null. * * DERIVED from `@objectstack/spec`, not restated: the vocabulary lives once, in * `GetMetaItemLayeredResponseSchema`'s `overlayScope` * (`z.enum(['org', 'env']).nullable()`), and this alias indexes the published * response type so a scope the spec adds arrives here with no edit. Restating * the union locally is the fork `scripts/check-spec-symbol-derivation.mjs` * exists to reject, and the mirror-image mistake — a consumer re-spelling a * producer's enum — is what this field carried until objectui#4982. * * What it carried: `string | null`, under a comment naming the vocabulary as * `organization | environment | package`. Not one of those three spellings is a * value the producer emits (`metadata-protocol`'s two assignment sites write * `'org'` / `'env'`); the schema rejects all three by name, and `package` is * not a scope this field has ever had. Because the declared type was `string` * the compiler had no opinion, so the wrong comment was the only description of * the vocabulary — a planted premise rather than stale prose, and the reason * the Studio's layer badge shipped the raw value straight to screen. */ type MetadataOverlayScope = GetMetaItemLayeredResponse['overlayScope']; /** * ADR-0010 §3.6 — the four-state metadata protection lock * (`none` / `no-overlay` / `no-delete` / `full`), read from * `GetMetaItemLayeredResponseSchema`'s own `z.enum` rather than spelled out * again here (objectui#5024). * * It used to be written out twice in this file, 42 lines apart: once as * {@link MetadataLayered.lock} (optional) and once as * {@link MetadataAuditEntry.lockState} (nullable), identical in every other * respect and compared by no gate. A fifth state added to one would have left * the other compiling — the same "declared N times, diffed by nothing" failure * objectui#4972 and objectui#4984 record for other vocabularies. * * Deriving beats a local alias the two merely share. The producer of these * values is the framework, and `packages/spec` already declares the vocabulary, * so the copies were restating a schema that existed rather than filling a gap. * The card that reported this recorded the opposite — "`@objectstack/spec` 也没 * 有对应的 `z.enum` 可派生" — and so did the audit panel's neighbouring comment; * both predate the enum, which ships in `@objectstack/spec` 17.1.0. This is the * same treatment {@link MetadataOverlayScope} above already gets, and it closes * the cross-repo half of the drift, not just the in-repo half. * * ⚠️ This types what this repo may WRITE. It does NOT constrain what a server * may SEND: {@link MetadataClient.layered} casts the wire value through * unchecked, so every reader must still handle a value outside these four. The * lock banner in `ResourceEditPage` is the worked example. */ type MetadataLockState = GetMetaItemLayeredResponse['lock']; /** * Layered view of a metadata item — the body of * `GET /meta/:type/:name/layers` (`GetMetaItemLayeredResponseSchema`). * * A subset by design: the response also carries `type` / `name`, which the * caller already knows because it passed them in, so * {@link MetadataClient.layered} does not hand them back. */ interface MetadataLayered { /** Code-level (artifact) item; null if the item only exists as an overlay. */ code: T | null; /** Org/environment overlay (just the saved delta or full overlay row). */ overlay: T | null; /** * Which layer {@link MetadataLayered.overlay} came from, or null when there * is no overlay. Spec-derived — see {@link MetadataOverlayScope}. */ overlayScope: MetadataOverlayScope; /** Merged effective view — what the runtime actually sees. */ effective: T | null; /** * Load-time validation result for `effective` (server-computed via * the same Zod registry used at save time). Undefined for types * without a registered Zod schema. Surfaced by the Studio as a * banner + inline field errors so operators can spot bad metadata * without having to hit Save. */ _diagnostics?: MetadataDiagnostics; /** 4-state lock: `none` / `no-overlay` / `no-delete` / `full`. */ lock?: MetadataLockState; /** Human-readable reason for the lock (tooltip text). */ lockReason?: string; /** Which layer set the lock: artifact / package / overlay / env-forced. */ lockSource?: 'artifact' | 'package' | 'overlay' | 'env-forced'; /** Optional docs URL for the lock reason (rendered as "View docs →"). */ lockDocsUrl?: string; /** Origin of the item: `package` (loader) | `org` (tenant) | `env-forced`. */ provenance?: 'package' | 'org' | 'env-forced'; /** Owning package id (denormalised from the loader tag). */ packageId?: string; /** Owning package version. */ packageVersion?: string; /** True when the editor should allow Save (PUT). */ editable?: boolean; /** True when the editor should allow Delete. */ deletable?: boolean; /** True when "Reset to package default" applies (has overlay + artifact). */ resettable?: boolean; } /** * One row in the metadata protection-audit trail * (ADR-0010 §3.6 / Phase 4.1). Mirrors `sys_metadata_audit` columns * the API exposes. */ interface MetadataAuditEntry { /** Stable audit-row id (uuid). */ id: unknown; /** ISO timestamp when the attempt happened. */ occurredAt: string; /** * Who attempted the operation — the identity the request was authorized * as, or `'system'` for internal machine writes (objectstack#7941). */ actor: string; /** Code path that recorded the row (e.g. `protocol.saveMetaItem`). */ source: string | null; /** Which lifecycle op was attempted. */ operation: 'save' | 'publish' | 'rollback' | 'delete' | 'reset'; /** Decision: allowed / denied / forced (admin override). */ outcome: 'allowed' | 'denied' | 'forced'; /** Machine-readable reason code (`item_locked`, `ok`, …). */ code: string; /** Effective lock at the moment of the attempt. */ lockState: MetadataLockState | null; /** True when admin forced the write through despite the lock. */ lockOverridden: boolean; /** Request-id for trace correlation (if propagated). */ requestId: string | null; /** Free-text note (often the lock reason). */ note: string | null; } /** Response shape for `MetadataClient.audit()`. */ interface MetadataAuditResponse { events: MetadataAuditEntry[]; } /** * Load-time validation envelope attached to metadata items by the * framework. Mirrors `MetadataValidationResult` in the kernel spec. */ interface MetadataDiagnostics { valid: boolean; errors?: Array<{ path: string; message: string; code?: string; }>; warnings?: Array<{ path: string; message: string; }>; } /** Options for the cross-type `/meta/diagnostics` sweep call. */ interface MetadataDiagnosticsOptions { /** Restrict the sweep to a single metadata type (e.g. `'view'`). */ type?: string; /** * `'error'` (default) returns only items that fail validation. * `'warning'` also includes items whose only diagnostics are warnings. */ severity?: 'error' | 'warning'; /** Restrict to items owned by this package id. */ packageId?: string; } /** One row in the `/meta/diagnostics` response. */ interface MetadataDiagnosticsEntry { type: string; name: string; diagnostics: MetadataDiagnostics; } /** Top-level envelope returned by `/meta/diagnostics`. */ interface MetadataDiagnosticsSummary { entries: MetadataDiagnosticsEntry[]; /** Number of `entries` returned (post-filter). */ total: number; /** How many metadata types the sweep visited. */ scannedTypes: number; /** How many individual items were validated. */ scannedItems: number; /** * Per-type aggregate stats — count of items and the list of * packages contributing to each type. Computed in the same sweep * so a single call serves both the diagnostics governance page and * the directory tile counts / package filter. * * Optional for backward compatibility with older framework * versions that do not yet emit it; clients should fall back to * an empty record. */ stats?: Record; } /** Reference back-pointer — Phase 3a `/references`. */ interface MetadataReference { /** Referencing item's metadata type. */ fromType: string; /** Referencing item's name. */ fromName: string; /** JSON path within the referencing item that holds the reference. */ path: string; /** The actual value seen at `path` (the referenced name). */ value: string; } interface MetadataHistoryOptions { /** Only return events after this sequence number. */ sinceSeq?: number; /** Limit the number of events returned. */ limit?: number; } /** A single field-anchored spec-validation issue (server `error.details.issues`). */ interface MetadataValidationIssue { /** Dot-path to the offending field, e.g. `fields.amount.type` (`''` = root). */ path: string; message: string; code?: string; } interface MetadataError extends Error { status: number; code?: string; body?: unknown; /** Structured spec-validation issues, when the failure was a validation error. */ issues?: MetadataValidationIssue[]; } /** * MetadataClient — read/write protocol metadata via the framework REST API. * * @example * ```ts * const client = new MetadataClient({ baseUrl: 'http://localhost:3000' }); * const objects = await client.list<{ name: string; label?: string }>('object'); * const account = await client.get<{ fields: Record }>('object', 'account'); * await client.save('object', 'account', { ...account, label: 'Customer' }); * ``` */ declare class MetadataClient { private readonly base; private readonly fetchImpl; private readonly headers; /** ADR-0037: when true, reads render the draft-overlaid world. */ readonly previewDrafts: boolean; /** #4133 — sink for post-save advisory findings; see the config field. */ private readonly onSaveAdvisory; constructor(config: MetadataClientConfig); /** Update the client's environment scope at runtime. */ withEnvironment(environmentId: string | undefined): MetadataClient; /** * Derive a client whose reads render the draft-overlaid world (or not). * Same base/fetch/headers; used by the app-shell to switch the whole * renderer tree into ADR-0037 preview mode off one URL flag. */ withPreviewDrafts(previewDrafts: boolean): MetadataClient; /** List all registered metadata types (returns the registry rows). */ listTypes(): Promise; /** List items of a metadata type (e.g. `object`, `field`, `view`). */ list(type: string, options?: MetadataListOptions): Promise; /** * List pending DRAFT items (ADR-0033) — what an AI authored but nobody * published yet. `list()` only sees published/active metadata, so a * just-built app package looks empty there; this surfaces the drafts so the * console can show a "pending changes" view and draft-aware package contents. * Optionally narrow by `packageId` and/or `type`. Returns light headers * (no body) carrying `packageId` for grouping. */ listDrafts(options?: { packageId?: string; type?: string; }): Promise; /** * Fetch a single metadata item and return the response body EXACTLY as the * server sent it — envelope and all. * * The one transport both {@link get} (which unwraps) and {@link getDraft} * (which does not) sit on, so the two can differ in what they hand back * without differing in how they ask. Private on purpose: the envelope is a * wire detail, and the two published contracts above are the supported ways * to read an item. */ private readItemResponse; /** * Get a single metadata item. Returns the unwrapped item CONTENT — the * metadata document itself, so `obj.fields` / `obj.label` are reachable * directly. * * The framework answers `GET /meta/:type/:name` with the spec-declared * envelope `{ type, name, item, …protection fields }` * (`GetMetaItemResponseSchema`; objectstack#5563 collapsed this read to that * ONE shape), so the envelope is unwrapped here — at the client boundary, * once — rather than by each caller. * * **This method used to return the envelope while promising the body** * (objectui#4271). Nothing detected the disagreement, because the test * doubles across the repo were written against this docblock: every consumer * reading `obj.fields` got `undefined` in production and a field list in * unit tests. The visible cost was the entire field half of the permission * matrix reporting "No fields registered for this object." for every object, * plus dead RLS CEL autocomplete, an inert report drill-down and a designer * that saved the envelope back over the object body. * * A response that is NOT the envelope (an older server answering the bare * document, or a body of its own shape) passes through untouched, and `null` * still comes back on 404 to keep the call site ergonomic. */ get(type: string, name: string, options?: MetadataGetOptions): Promise; /** * Read the pending draft body for an item (`?state=draft`). Returns * `null` when there is no draft pending. Draft reads do NOT fall * back to the published overlay or the artifact registry — a `null` * unambiguously means "nothing to publish". * * Note: this method hands back the `{ type, name, item }` envelope the * framework sends, NOT the body — callers read `.item`. That asymmetry with * {@link get} is deliberate and long-standing (the draft envelope's identity * and protection carriers are part of what a draft reader inspects), so it * is preserved by reading the transport directly instead of going through * `get()`'s unwrap. `unwrapDraftBody` (app-shell) and `unwrapViewDraft` * (this package) are the shared helpers for taking the body out. */ getDraft(type: string, name: string, options?: { packageId?: string; }): Promise; /** * Save (PUT) a metadata item. The framework accepts both the bare * item payload and the `{ item: ... }` / `{ metadata: ... }` * envelopes; we send bare for consistency. Pass `mode: 'draft'` to * stage the change without publishing (Studio's "Save" button). */ save(type: string, name: string, item: unknown, options?: MetadataClientSaveOptions): Promise; /** * Publish a single pending draft BY REFERENCE — promotes the draft row for * `(type, name)` into the active overlay and drops the draft. Works for ANY * draft, including ones with no `packageId` binding (which the package-scoped * `/packages/:id/publish-drafts` flow cannot reach). Use this to publish the * exact set returned by {@link listDrafts} without needing a package. * * Returns the server result. For `seed` drafts the protocol also materializes * the rows and reports under `seedApplied` — a data problem never fails the * publish, so callers should check `seedApplied?.success` and warn the user * rather than assume the data went live. */ publishDraft(type: string, name: string): Promise<{ success?: boolean; seedApplied?: { success: boolean; inserted?: number; updated?: number; error?: string; errors?: unknown[]; }; } & Record>; /** * Get the 3-state layered view of a metadata item: `code` (the packaged * artifact baseline), `overlay` (the tenant customisation row alone) and * `effective` (the merged value the runtime sees). * * Reads **`GET /meta/:type/:name/layers`** — the path the framework declares * for this projection, with a response schema of its own * (`GetMetaItemLayeredResponseSchema`, objectstack#5882 ruling B). It used to * be reached by hanging a `layers` flag on the ordinary item read, which made * one route answer two unrelated representations while `packages/spec` * declared only one of them. That spelling still answers this same body * inside its deprecation window (the response carries RFC 9745 * `Deprecation: true` and an RFC 8288 `Link: rel="successor-version"` back to * this path) and is scheduled for removal upstream, so nothing here may * depend on it — the repo-wide ratchet is * `scripts/__tests__/layered-read-declared-path-4016.test.ts`. * * The request is built here rather than delegated to `@objectstack/client` * because the SDK expresses no layered read in EITHER spelling: the * framework's REST route ledger records this route as `server-only`, * "consumed by objectui over plain HTTP", and whether the SDK should express * it is an open upstream product call. * * One behaviour delta rides along with the path, and it is the server's * choice rather than ours: the retired flag FELL THROUGH to the plain item * read on a backend whose protocol implementation had no layered support, * answering the `{ type, name, item }` envelope. A dedicated path refuses to * answer a different resource under this one's declared shape, so it returns * 501 `NOT_IMPLEMENTED` instead — which surfaces here as a thrown error * rather than a view with `code` and `overlay` silently blank. */ layered(type: string, name: string, options?: { packageId?: string; }): Promise>; /** * Cross-type sweep of load-time validation results — calls * `GET /meta/diagnostics`. Returns every entry the framework * considers invalid (or, when `severity: 'warning'`, also entries * with only warnings). * * Used by the Studio's governance overview page and by the * directory page to show "N invalid" badges per metadata type. */ diagnostics(options?: MetadataDiagnosticsOptions): Promise; /** * Find every metadata item that references this one (Phase 3a). Useful * for pre-delete impact analysis: "Are any views pointing at this * object before I drop it?". */ references(type: string, name: string): Promise; /** * Reset a metadata customization overlay back to the artifact default. * Idempotent: returns the result even when no overlay row existed. * Pass `state: 'draft'` to discard the pending draft only (keeps the * published overlay intact) — useful for a Studio "Discard draft" button. */ reset(type: string, name: string, options?: MetadataDeleteOptions): Promise; /** * Promote the pending draft of an item to the active overlay * (POST `/meta/:type/:name/publish`). Returns * `{ success, version, seq, message }`. Throws a `404 no_draft` if * nothing is pending and `409 metadata_conflict` if the published * overlay moved while the draft was sitting. */ publish(type: string, name: string, options?: { message?: string; }): Promise; /** * Restore an item to a previous version (POST `/meta/:type/:name/rollback`). * The server reads the history row at `toVersion`, writes its body back * as the active overlay with `operation_type='revert'`. Returns * `{ success, version, seq, restoredFromVersion, message }`. Throws * `404 version_not_found` for unknown versions and `409 version_not_restorable` * when the target version is a delete tombstone. */ rollback(type: string, name: string, toVersion: number, options?: { message?: string; }): Promise; /** * Compute a structured top-level key diff between two history versions * (GET `/meta/:type/:name/diff?from=&to=`). Returns * `{ added, removed, changed }` where each entry carries a `path` and * the relevant value(s). Useful for "what changed in this draft?" and * pre-rollback previews. */ diff(type: string, name: string, fromVersion?: number, toVersion?: number): Promise; /** Fetch the durable history (change log) for one metadata item. */ history(type: string, name: string, options?: MetadataHistoryOptions): Promise; /** * Fetch the protection-audit trail for one metadata item * (ADR-0010 §3.6 / Phase 4.1). Shows every save/publish/rollback/ * delete/reset attempt — allowed, denied, or forced — so the * Studio "审计日志 / Audit log" tab can render who tried what * and whether a lock blocked it. */ audit(type: string, name: string, options?: { limit?: number; }): Promise; } /** * ObjectUI * Copyright (c) 2024-present ObjectStack Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** * Base error class for all ObjectStack adapter errors */ declare class ObjectStackError extends Error { code: string; statusCode?: number | undefined; details?: Record | undefined; /** * Create a new ObjectStackError * * @param message - Human-readable error message * @param code - Unique error code for programmatic handling * @param statusCode - Optional HTTP status code * @param details - Optional additional error details for debugging */ constructor(message: string, code: string, statusCode?: number | undefined, details?: Record | undefined); /** * Convert error to JSON for logging/debugging */ toJSON(): { name: string; message: string; code: string; statusCode: number | undefined; details: Record | undefined; stack: string | undefined; }; } /** * Error thrown when requested metadata/schema is not found */ declare class MetadataNotFoundError extends ObjectStackError { constructor(objectName: string, details?: Record); } /** * Error thrown when a bulk operation fails */ declare class BulkOperationError extends ObjectStackError { successCount: number; failureCount: number; errors: Array<{ index: number; error: unknown; }>; /** * Create a new BulkOperationError * * @param operation - The bulk operation that failed (create, update, delete) * @param successCount - Number of successful operations * @param failureCount - Number of failed operations * @param errors - Array of individual errors * @param details - Additional error details */ constructor(operation: 'create' | 'update' | 'delete', successCount: number, failureCount: number, errors: Array<{ index: number; error: unknown; }>, details?: Record); /** * Get a summary of the bulk operation failure */ getSummary(): { operation: string; total: number; successful: number; failed: number; failureRate: number; errors: { index: number; error: unknown; }[]; }; } /** * Error thrown when connection to ObjectStack server fails */ declare class ConnectionError extends ObjectStackError { url?: string | undefined; constructor(message: string, url?: string | undefined, details?: Record, statusCode?: number); } /** * Error thrown when authentication fails */ declare class AuthenticationError extends ObjectStackError { constructor(message?: string, details?: Record, statusCode?: number); } /** * Error thrown when the ObjectStack data API rejects a write as invalid. * * NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this * class wore until objectui#3160 (objectstack#4115 ledger batch 6). That one is * a plain DATA SHAPE — `{ field, message, code? }`, one entry in a plugin * manifest's validation report — and `@object-ui/types` re-exports it under that * name. This is a runtime `Error` subclass carrying an HTTP status plus a list * of such entries, so the two are not even the same KIND of thing. * * The name follows the convention registered on objectstack#4115 for this * family — `Validation`. `@object-ui/core` * took `SchemaNodeValidationError` for its SDUI-tree walk; this one belongs to * the data API. */ declare class DataApiValidationError extends ObjectStackError { field?: string | undefined; validationErrors?: Array<{ field: string; message: string; }> | undefined; /** * Create a new DataApiValidationError * * @param message - Human-readable error message * @param field - The field that failed validation (optional) * @param validationErrors - Array of validation error details * @param details - Additional error details */ constructor(message: string, field?: string | undefined, validationErrors?: Array<{ field: string; message: string; }> | undefined, details?: Record); /** * Get all validation errors as a formatted list */ getValidationErrors(): { field: string; message: string; }[]; } /** * Helper function to create an error from an HTTP response * * @param response - Response object or error from fetch/axios * @param context - Additional context for debugging * @returns Appropriate error instance */ declare function createErrorFromResponse(response: Record, context?: string): ObjectStackError; /** * Type guard to check if an error is an ObjectStackError */ declare function isObjectStackError(error: unknown): error is ObjectStackError; /** * Type guard to check if an error is a specific ObjectStack error type */ declare function isErrorType(error: unknown, errorClass: new (...args: any[]) => T): error is T; /** * ObjectUI * Copyright (c) 2024-present ObjectStack Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** * Statistics reported by {@link MetadataCache.getStats} for monitoring. * * NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this * interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one * describes the platform's `ICacheService` — a server-side KV cache measured by * `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front * of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces * concurrent fetches onto one in-flight promise, and it reports a `hitRate`. * Neither type has a key the other has, so this is a name collision, not a * dialect — deriving would have replaced every field. */ interface MetadataCacheStats { size: number; maxSize: number; hits: number; misses: number; evictions: number; /** Number of concurrent fetches that were coalesced onto an in-flight request. */ coalesced: number; hitRate: number; } /** * ObjectStack-backed `UserDataAdapter` factory. * * Persists arbitrary per-user UI state (favorites, recently-accessed items, …) * as a single JSON blob per `(user_id, key)` row in the **unified per-user * KV store** — the `sys_user_preference` object shipped by every * `@objectstack/plugin-auth`-enabled environment. * * Using the existing `sys_user_preference` table (rather than a parallel * `user_app_state` table) keeps things consistent with the platform's * "one KV store per scope" pattern: * * - `sys_setting` ← tenant / env scope * - `sys_user_preference` ← per-user scope ← we live here * * Callers are encouraged to namespace their keys (e.g. `ui.favorites`, * `ui.recent`, `ui.grid.account.state`) so explicit settings (`theme`, * `locale`) and machine-written UI traces stay easy to tell apart. * * ## Schema this adapter expects * * The canonical `sys_user_preference` schema (from * `@objectstack/platform-objects`): * * ```yaml * object: sys_user_preference * fields: * user_id: lookup(sys_user) # indexed * key: string # indexed (e.g. "ui.favorites") * value: json # the serialised list * updated_at: datetime # auto-managed * unique: [user_id, key] * ``` * * ## Failure modes * * The adapter is designed to **degrade silently**. Any error — missing * schema, 4xx/5xx, network — is caught: * * - `load()` returns `[]`, so the hosting provider keeps its localStorage state. * - `save()` resolves without throwing, so UI mutations never surface a toast. * * As soon as the backend supports `sys_user_preference`, persistence * "lights up" with no code change. * * @module */ interface ObjectStackUserStateAdapterOptions { /** Connected data source (usually the one provided by ``). */ dataSource: DataSource; /** Authenticated user id. */ userId: string; /** * Storage key. Should be a dotted, namespaced string so UI traces * don't collide with user-facing preferences. Examples: * `ui.favorites`, `ui.recent`, `ui.grid.account.state`. */ key: string; /** Override the storage object name. Defaults to `"sys_user_preference"`. */ resource?: string; /** * Optional console logger for development diagnostics. Defaults to noop so * production builds stay quiet. */ onError?: (where: 'load' | 'save', error: unknown) => void; } interface UserDataAdapter { load(): Promise; save(items: T[]): Promise; } /** * Build a `UserDataAdapter` backed by ObjectStack. * * Each adapter instance is bound to a single `(user, key)` pair; create * one per slot you want to persist. */ declare function createObjectStackUserStateAdapter(options: ObjectStackUserStateAdapterOptions): UserDataAdapter; /** * Map human-readable filter operator names produced by SDUI view configs * (e.g. `lead.view.ts`) to the canonical operator symbols expected by the * ObjectStack server's filter AST. Unknown operators fall through unchanged * so existing AST-style entries keep working. * * Every VALUE here must be a member of the spec's `VALID_AST_OPERATORS` * (`@objectstack/spec/data`) — that set gates `isFilterAST()`, and a filter it * rejects is not converted, not validated, and then silently DROPPED by * driver-sql (objectstack#3948). Pinned by `filter-operator-ast-parity.test.ts`. * * Exported for that test. @internal */ declare const FILTER_OPERATOR_ALIASES: Record; /** * A filter entry this adapter cannot translate into an AST tuple. * * Thrown rather than skipped. Dropping one entry out of an `and` WIDENS the * result set, and dropping the last one emits no `filter=` at all — every row, * no error, from a query that asked for a subset. That is the same silent * over-fetch the server-side drivers stopped doing in objectstack#3948, and * skipping it here just moves it one layer up. * * Carries the code and status the data API uses for its own version of this * refusal (objectstack#4121) so a failed list renders "this view's filter is * malformed" rather than "check your connection" (#3066). */ declare class MalformedFilterError extends Error { readonly code = "INVALID_FILTER"; readonly httpStatus = 400; readonly entry: unknown; readonly index: number; constructor(entry: unknown, index: number); } /** Detect the malformed-filter refusal, whether raised here or by the server. */ declare function isMalformedFilterError(error: unknown): boolean; /** * Serialize a `$orderby` to the server's `sort` shorthand * (`field,-other_field`), for every shape `QueryParams['$orderby']` declares. * * The type declares four — `string`, `string[]`, `SortNode[]`, * `Record` — and the two `find()` routes each open-coded a * fold that handled three of them. The missing one was the bare string, and it * did not degrade quietly: `Object.entries('name asc')` enumerates a string's * character indices, so the request went out as `sort=0,1,2,3,4,5,6,7`. Against * a server that rejects an unreadable sort rather than ignoring it * (objectstack#4226), that is a `400 INVALID_SORT` and an empty list — so a * standalone `ObjectGrid` with a `sort` in its metadata, which is exactly the * shape it builds (`ObjectGrid.tsx`: `` `${field} ${order}` ``), failed to load * at all. * * One serializer for both routes, for the reason the filter path already has * one: two copies of a fold can only agree by inspection, and these two did not. * * Returns `undefined` when nothing is sortable, so callers skip the parameter * entirely rather than sending an empty one. */ declare function serializeOrderBy(orderby: QueryParams['$orderby']): string | undefined; /** * Fetch the server `discovery` document once per (baseUrl) and reuse the * resulting Promise. Used by `ObjectStackAdapter.connect()` (and any caller * that wants the discovery payload without spinning up a new client). */ declare function getSharedDiscovery(baseUrl: string, fetcher: () => Promise): Promise; /** Test/dev helper to drop the cache (e.g. on logout or origin change). */ declare function clearSharedDiscoveryCache(): void; /** * Read the cross-object atomic-batch capability from a `discovery` document * (framework #3298 / objectui #2693). The server advertises it hierarchically * under `capabilities.transactionalBatch.enabled`; the published * `@objectstack/client` also accepts the flat `capabilities.transactionalBatch: * boolean` form and normalizes the two — mirror that here so the adapter reads * the same bit regardless of which shape reaches it. * * Returns: * - `true` — the backend GUARANTEES an atomic `/batch` (declared === enforced, * i.e. the route is mounted AND the runtime can honour a transaction): the * client may drop its non-atomic fallback and treat any batch failure as a * real error. * - `false` — the backend explicitly does NOT (route absent, or a runtime that * can't open a transaction). * - `undefined` — the capability is absent, i.e. the backend predates #3298; * the caller must keep the legacy runtime-probe fallback (we can't tell * whether `/batch` exists without trying it). */ declare function readTransactionalBatchCapability(discovery: unknown): boolean | undefined; /** * Detect "missing resource" errors regardless of where they originate. * * The ObjectStack client decorates thrown errors with `httpStatus` (and a * machine-readable `code` such as `object_not_found`/`record_not_found`), * while raw `fetch()` callers may surface `status` or `statusCode`. Treat * any of these as a 404 so callers can degrade gracefully instead of * tripping on the property-name mismatch. */ declare function is404Error(error: unknown): boolean; /** * The two denials the server derives from an object's `enable` block — * `apiAccessDenialFromEnable` (objectstack `packages/rest/src/rest-server.ts`). * * - `OBJECT_API_DISABLED` (404) — `enable.apiEnabled: false`; the object is * not exposed over the data API at all. * - `OBJECT_API_METHOD_NOT_ALLOWED` (405) — the operation is absent from the * `enable.apiMethods` whitelist. * * Both are **pure functions of the object's metadata**: no user, no permission, * no context, no request body. So neither is transient and neither is * per-user — when one happens it is a permanent property of that object, and * every retry of every persona gets the identical answer. * * That is exactly why they must not be degraded into "no data". A 404 from a * missing collection means *this backend doesn't have that table*, which the * optional-collection probes below legitimately read as "feature unavailable"; * a 404 from `OBJECT_API_DISABLED` means *this page can never work*. Answering * the second with an empty result set renders "you have no records" over a * surface that is not allowed to have any (objectui#4408 — it also hid the * upstream defect objectstack#7544 for its entire life, because a merely * unpopulated page invites nobody to click through). */ declare const API_ACCESS_DENIED_CODES: readonly ["OBJECT_API_DISABLED", "OBJECT_API_METHOD_NOT_ALLOWED"]; /** * True when `error` is an `enable`-block API denial (see * {@link API_ACCESS_DENIED_CODES}). * * Discriminates on the ADR-0112 `code`, never on the status: 404 alone cannot * separate a disabled object from a missing collection, and 405 alone cannot * separate a withheld method from any other method rejection. The code survives * the transport — `@objectstack/client`'s fetch wrapper stamps `error.code` * from the response envelope, and both spellings are declared members of the * spec's `StandardErrorCode` — so no heuristic on status is needed or wanted. */ declare function isApiAccessDeniedError(error: unknown): boolean; /** * What the by-name meta app route said about THIS session's access to an app * (objectui#4252 / objectstack#8013). * * - `granted` — the route served the app document. * - `denied` — the app EXISTS and the session lacks its `requiredPermissions`. * The only verdict a caller may render as an authorization refusal. * - `unknown` — anything else: an absent app, an unpublished one, an app * withheld by an absent optional service, an unreachable server, an adapter * that cannot ask. All of these are cases where the server declined to say * that a permission of the caller's is missing, so no caller may claim it. * * Three values rather than a boolean because the third is not a shade of the * other two: "the app is missing" and "I could not find out" both have to leave * the caller's existing copy alone, and collapsing them into `false` invites a * consumer to read a failed probe as a positive absence. */ type AppAccessVerdict = 'granted' | 'denied' | 'unknown'; /** * The ADR-0112 standard catalog code the by-name meta app route answers with * when an app exists and the session lacks its `requiredPermissions` * (objectstack#8013, `sendError(res, 403, 'PERMISSION_DENIED', …)` in * `packages/rest/src/rest-server.ts`). * * Deliberately NOT a member of {@link API_ACCESS_DENIED_CODES}: those two are * pure functions of an object's `enable` metadata — permanent, identical for * every persona — whereas this one is a statement about the CALLER, and the same * request by a different session succeeds. Same word "denied", different * question, so a consumer that wants one must never match the other. */ declare const APP_PERMISSION_DENIED_CODE = "PERMISSION_DENIED"; /** * True when `error` is the by-name app route's permission denial. * * Discriminates on the ADR-0112 `code`, never on the status (objectui#4408): the * route answers 403 for this and 404 for absence today, but a status is a * transport fact many conditions share, while the code is the contract. The * console's whole reason to call this is to tell two REFUSALS apart, and both * are errors. */ declare function isAppPermissionDeniedError(error: unknown): boolean; /** * Thrown when the deployment has no analytics capability installed * (framework#3891 / #4019). * * The framework retired its degraded in-kernel analytics fallback — it dropped * the caller's RLS/tenant scope and ignored the contract filter, so it answered * 200 with over-broad numbers. `@objectstack/service-analytics` is now the * domain's only implementation, and a deployment without it answers: * * - `POST /api/v1/analytics/query` → **404** (the routes aren't even mounted); * - `POST /api/v1/analytics/dataset/query` → **501 NOT_IMPLEMENTED**. * * Neither is a bug to report as a stack trace: it is a deployment that hasn't * installed the capability. This error carries a message a UI can show as-is. */ declare class AnalyticsNotInstalledError extends Error { readonly code = "ANALYTICS_NOT_INSTALLED"; /** The surface that was unavailable, for the message a host renders. */ readonly surface: string; /** * The server's own ADR-0112 code — the field this branch was chosen BY, when * a producer named one (`NOT_IMPLEMENTED` from the mounted route with no * service behind it, `ROUTE_NOT_FOUND` from the dispatcher when the route is * not mounted at all). Absent for the no-code residual, where a bare * transport 404/501 is all there was to read. * * Additive (objectui#5663): carried so a reader can audit that the headline * and the quoted `detail` below came off the same answer. */ readonly serverCode?: string; constructor(surface: string, detail?: string, serverCode?: string); } /** True when `error` is an {@link AnalyticsNotInstalledError} (or its wire twin). */ declare function isAnalyticsNotInstalledError(error: unknown): boolean; /** * Thrown when the dataset route ANSWERED and said the dataset this query named * does not exist in this environment — `404` + ADR-0112 `NOT_FOUND`, the * `body.datasetName` lookup miss in `@objectstack/rest`'s * `registerAnalyticsEndpoints` (`Dataset "" not found.`). * * A SIBLING of {@link AnalyticsNotInstalledError}, never a shade of it * (objectui#5663). Both conditions answer **404** — routes-not-mounted through * the runtime dispatcher's `ROUTE_NOT_FOUND`, an unknown dataset through this * route's own `NOT_FOUND` — so a `res.status === 404` test cannot separate * them, and the mapping that could not tell them apart reported EVERY unknown * dataset as a missing server capability. * * Measured live on a prod tenant: four HotCRM Executive Overview widgets told * the operator to install `@objectstack/service-analytics` and mount * `AnalyticsServicePlugin`, while the analytics service was installed and * answering the whole time. The real condition was an installed * `app.objectstack.hotcrm` pinned at 1.3.0 whose datasets ship in 2.2.2. The * remedy the banner named — install a server plugin — is the opposite corner of * the system from the remedy that works: upgrade the installed app. A wrong * diagnosis is not a smaller version of no diagnosis; it spends the operator's * time in the wrong subsystem. * * The `code` is the ONLY field that separates the two, which is why this branch * reads it and nothing else. See `readAnalyticsErrorEnvelope` below — the * module-private reader that pulls the code out of either declared envelope. */ declare class AnalyticsDatasetNotFoundError extends Error { readonly code = "ANALYTICS_DATASET_NOT_FOUND"; /** * The dataset this query asked for, taken from the REQUEST rather than parsed * back out of the server's prose: the request is what we know for certain, * and re-reading the name out of a message would make the headline depend on * that message's WORDING — the coupling framework#5367 spent a whole issue * removing from the producing route. */ readonly datasetName?: string; /** The server's own ADR-0112 code — the field this branch was chosen BY. */ readonly serverCode?: string; /** The server's own message, quoted verbatim in the parenthetical. */ readonly serverMessage?: string; constructor(opts: { datasetName?: string; serverCode?: string; serverMessage?: string; }); } /** * Thrown when the dataset query was refused before it ran because the session * is not authenticated — `401` + ADR-0112 `UNAUTHENTICATED`, the REST seam's * `ANONYMOUS_DENY_BODY` (`@objectstack/core`'s `security/anonymous-deny.ts`, * written verbatim by `enforceAuth`). * * The THIRD branch, and the one the reported card under-states (objectui#5663): * it recorded a live `POST /api/v1/analytics/dataset/query -> 401 * UNAUTHENTICATED` and read it as evidence for the dataset-unknown branch. It * is neither of the other two. An expired session reported as "the deployment * is missing a capability" is the SAME defect wearing a different mask — an * operator sent to install a server plugin because their token lapsed — so the * separation is the point, not a nicety. * * Deliberately not left to the generic `Dataset query failed: 401 …` either: * that string names a transport status where a person needs an action, and * "sign in again" is an action. */ declare class AnalyticsUnauthenticatedError extends Error { readonly code = "ANALYTICS_UNAUTHENTICATED"; /** The server's own ADR-0112 code — the field this branch was chosen BY. */ readonly serverCode?: string; /** The server's own message, quoted verbatim in the parenthetical. */ readonly serverMessage?: string; constructor(opts?: { serverCode?: string; serverMessage?: string; }); } /** * Thrown when the server REJECTED the analytics query body (HTTP 400 — * `VALIDATION_FAILED` since framework#4010 validates `/analytics/query` at the * entry against the canonical bare `AnalyticsQuery` shape). * * Distinct from {@link AnalyticsNotInstalledError} on purpose: this one is a * defect in what WE sent, so it must never be answered with the client-side * fallback. Numbers produced by a different code path would look plausible and * bury the contract violation — the misdirection framework#3878 documented. */ declare class AnalyticsQueryRejectedError extends Error { readonly code = "ANALYTICS_QUERY_REJECTED"; /** The server's own error code (e.g. `VALIDATION_FAILED`), when it sent one. */ readonly serverCode?: string; constructor(detail?: string, serverCode?: string); } /** * Classify a FAILED analytics call so the caller knows whether to degrade or * to surface the failure. * * `@objectstack/client`'s fetch wrapper throws on a non-2xx, decorating the * error with the semantic `code` string and the numeric `httpStatus` (the * ADR-0112 / framework#3842 shape this repo already reads elsewhere). * * ## The `code` is the contract; the status is a transport fact (objectui#5721) * * What stood here tested `status === 404 || status === 501` BEFORE the code * operands that followed them, so the status short-circuited every one of * them: any 404 on this face was `not-installed` whatever code it carried, and * `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` were unreachable for the very * conditions they name. Three unrelated conditions answer 404 on this url — * * route absent 404 `ROUTE_NOT_FOUND` (runtime dispatcher, framework#4019) * cube unknown 404 `CUBE_NOT_FOUND` (service-analytics' inference gate) * object unknown 404 `OBJECT_NOT_FOUND` (the `/data` fallback's own answer) * * — so a misspelled cube read as "this deployment has no analytics", told the * operator to install a server plugin, and re-answered the chart from a * different code path. Same defect as objectui#5663, arrived at from the other * direction: the mapping read a field it did not quote. Branch on the `code`; * consult the status only as a residual, when the answer declared no code at * all. Comparisons go through `errorCodeIs`/`errorCodeIsAnyOf` — the pre- and * post-ADR-0112 spellings both have to match (`@object-ui/types`). * * ### Both envelope families reach this function already decorated (MEASURED) * * `/analytics/query` exits through `@objectstack/runtime`'s * `dispatcher-plugin.errorResponseBase`, i.e. the **wrapped** `{ success: * false, error: { code, message } }` shape — not the flat one the dataset * route writes — and its 401 is `enforceAuth`'s **flat** `ANONYMOUS_DENY_BODY`. * The client's fetch wrapper flattens BOTH before throwing: * `errorBody?.code ?? errorBody?.error?.code` → `error.code`, plus * `error.httpStatus = res.status`. So no envelope reading belongs here; unlike * `queryDataset` (which owns its `fetch` and must read the body itself), this * function is handed the already-flattened error and reads one field. * * ## Each branch names ONE of the three outcomes the caller can take * * - **`not-installed`** — *degrade LOUDLY*. The deployment has no analytics * service: 501 `NOT_IMPLEMENTED` (route mounted, nothing behind it) or 404 * `ROUTE_NOT_FOUND` (framework#4019 stops mounting the routes at all). * A client-side aggregate over a scoped `find()` answers the chart * correctly, and the operator is told once that the semantic layer is off. * - **`rejected`** — *THROW*. The server refused OUR body (400 * `VALIDATION_FAILED`; framework#4010 validates `/analytics/query` at the * entry). Degrading would answer our own contract violation with plausible * numbers from a different code path and bury it — the misdirection * framework#3878 documented. * - **`unauthenticated`** — *THROW*. 401 `UNAUTHENTICATED`: the request was * refused before it ran, so it is evidence about the SESSION and none at * all about the capability. Degrading is not merely misleading here, it is * futile: the fallback's `find()` carries the same lapsed token and is * refused the same way, so the chart cannot be answered either. "Sign in * again" is an action; "install @objectstack/service-analytics" is not. * - **`cube-not-found`** — *THROW*. 404 `CUBE_NOT_FOUND` * (`analytics-service.assertInferableCube`, framework#3867): analytics IS * installed and answering — the NAME does not exist. That gate throws only * when the name is neither a registered cube nor a registered object, and * the fallback asks `/data/`, which objectql's * `assertObjectRegistered` (framework#3770) answers 404 `OBJECT_NOT_FOUND`. * So degrading cannot produce numbers; it can only swap a message that * names the fix ("Define a Cube in your stack, or check the object name") * for a distant one, behind a warning that instructs the wrong repair. * - **`unknown`** — *degrade SILENTLY*. Anything else (5xx, network, a code * this consumer does not know): a transient failure, not a deployment * missing a capability and not a request that named something absent. */ declare function classifyAnalyticsFailure(error: unknown): { kind: 'not-installed' | 'rejected' | 'unauthenticated' | 'cube-not-found' | 'unknown'; code?: string; message?: string; }; /** * Thrown by `update()` / `delete()` when the server returns * `409 CONCURRENT_UPDATE` — i.e. the record was modified by someone else * between when the caller last read it and when they attempted to write. * * The error carries the current server-side `updated_at` version and the * full latest record so the UI can render an informed conflict-resolution * dialog (typically "Reload latest" / "Overwrite anyway" / "Cancel"). * * Mirrors the {@link ConcurrentUpdateError} thrown by * `@objectstack/objectql`'s protocol; the wire shape is: * ```json * { "code": "CONCURRENT_UPDATE", * "error": "", * "currentVersion": "", * "currentRecord": { ...latest... } } * ``` */ declare class ConcurrentUpdateError extends Error { readonly code = "CONCURRENT_UPDATE"; readonly httpStatus = 409; readonly currentVersion: string | null; readonly currentRecord: unknown; constructor(opts: { currentVersion: string | null; currentRecord: unknown; message?: string; }); } /** * Detect "concurrent update" errors raised by the platform. The wire * shape is `409` + `code: 'CONCURRENT_UPDATE'`. The client surfaces * extra details on `error.details` (full response body). */ declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpdateError; /** * Convert any error thrown by the upstream client into a typed error when we * recognise its shape. Returns the original error untouched otherwise, so * callers can simply `throw normaliseClientError(err)` from their catch blocks. * * Two shapes are recognised: * - `409` + `CONCURRENT_UPDATE` → {@link ConcurrentUpdateError}; * - `400` + `VALIDATION_FAILED` → {@link DataApiValidationError}, carrying the * server's per-field entries so a form can mark the offending inputs * instead of showing one undirected toast. */ declare function normaliseClientError(error: unknown): unknown; /** * Fold an @objectstack/client HTTP-failure `meta` bag into the log MESSAGE. * * The client already hands us everything worth knowing — * `logger.error("HTTP request failed", undefined, { method, url, status, error })` * — but it hands it as the THIRD argument. Everything that flattens a console * record to text (a headless/CDP console capture, a log shipper, a copied * DevTools line) keeps only the message and renders the rest as `[object * Object]` / `Object`, so a wall of failures carried no method, no URL and no * status: the reporter of objectui#4042 had to diff the network panel by hand * to find out that 30 red lines were all one benign pre-login burst. * * So the identifying fields go into the string itself, and the structured bag * is STILL passed alongside for DevTools to expand — text for the flatteners, * object for the inspectors, neither at the other's expense. * * Exported for tests. Returns `null` when `meta` carries none of the three * fields, so callers keep the original message rather than printing a husk. */ declare function formatHttpFailureMessage(message: string, meta?: Record): string | null; /** * Build a Logger compatible with @objectstack/client that (a) spells every * request failure out in the message — see {@link formatHttpFailureMessage} — * and (b) demotes expected 404 noise to console.debug. The client logs every * non-2xx response with * `logger.error("HTTP request failed", undefined, { method, url, status, error })`, * but 404s on optional collections (sys_presence, sys_activity, …) are part of * normal degraded operation when those plugins aren't installed on the * server — they should not surface as errors in the browser DevTools. * * NOTE the asymmetry, and keep it: 404-on-an-optional-collection is demoted * because it is an EXPECTED outcome of a request we still mean to make. No * other status is demoted — a 401 that survives the console's session gate * (objectui#4042: a mid-session expiry, say) is a real event and must stay a * visible, fully-identified error. The cure for doomed requests is not issuing * them, never hiding them once issued. * * Returned object is loosely typed because the spec's Logger interface lives * in a transitive package; using `any` keeps us decoupled. * * Exported so the console's log contract is testable, and so an app wiring its * own `ObjectStackClient` gets the same identified failures. */ declare function createQuietHttpLogger(): any; /** * Connection state for monitoring */ type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error'; /** * Connection state change event */ interface ConnectionStateEvent { state: ConnectionState; timestamp: number; error?: Error; } /** * Batch operation progress event */ interface BatchProgressEvent { operation: 'create' | 'update' | 'delete'; total: number; completed: number; failed: number; percentage: number; } /** * Event listener type for connection state changes */ type ConnectionStateListener = (event: ConnectionStateEvent) => void; /** * Event listener type for batch operation progress */ type BatchProgressListener = (event: BatchProgressEvent) => void; /** * Emitted after a create/update whose response carried `droppedFields` * (framework #3431/#3455). The write SUCCEEDED — this is a warning that some * supplied fields never landed, so the UI can tell the user rather than let it * pass silently. Subscribe via {@link ObjectStackAdapter.onWriteWarning}. */ interface WriteWarningEvent { operation: 'create' | 'update'; resource: string; id?: string | number; droppedFields: DroppedFieldsEvent[]; } /** Event listener type for write-warning (dropped-fields) events. */ type WriteWarningListener = (event: WriteWarningEvent) => void; /** * Resolve which object a `type='view'` metadata item belongs to. * * The metadata index is name-only, not field-typed: `GET /api/v1/meta/view` * accepts `?package=` and `?preview=draft` and nothing else (measured on * framework `packages/rest/src/rest-server.ts` — the `GET /meta/:type` * handler — and on `client.meta.getItems(type, { packageId })`). So every * reader of the view namespace enumerates `type='view'` once and narrows to * one object HERE, client-side. * * ONE spelling, one place, deliberately: {@link ObjectStackAdapter.listViews} * and {@link ObjectStackAdapter.listViewOverrides} read the same rows out of * the same namespace, and two private copies of "which object is this?" is a * drift waiting to happen — the switcher showing a view whose override the * grid cannot find, or the reverse. * * `object` is the identity field the write path stamps (and that the * framework's overlay heals onto identity-less personalization rows — * objectstack#2555); `data.object` is the config's data-provider target and * `objectName` the legacy artifact spelling. * * **Exported** since objectui#4373, for the same one-spelling reason: a writer * outside this module that holds a view BODY but not its object name (app-shell's * `MetadataService`) needs the object to name the keys * {@link ObjectStackAdapter.invalidateViewKeys} drops, and a fourth private copy * of "which object is this?" is exactly the drift this accessor exists to * prevent. Identity only — it names no cache key. */ declare function viewItemObjectName(item: any): string | undefined; /** * The keys a personalization overlay row legitimately OWNS (objectui#5233). * * One per `persistViewPatch` call site in app-shell's `ObjectView` — the ONLY * production writer of these rows — read off the tree rather than recalled: * `rowHeight` (the density toggle, spec-canonical since #2890), `sort`, * `hiddenFields`, `columnState` and `inlineEdit`. Nothing else in such a row * is an opinion the user expressed; anything else it carries is a COPY of the * source view as it stood at write time, because `persistViewPatch` USED TO * send `{ ...baseViewDef, ...patch }` and this adapter persists what it is * given. * * That copy was the defect the maintainer ruled on (objectstack#7494, comment * 5261754173): an overlay written by a mere column drag froze the view's * effective `filter` — and its `columns`, `label`, `type`, `isDefault` … — as * of that moment, and because the display merge is `{ ...source, ...override }` * the frozen copy SHADOWED the source view forever. An admin then edited the * view's filter and every user who once resized a column kept the old one, * with nothing anywhere reporting it. * * Both halves of that ruling have now landed, and this adapter's behaviour is * unchanged by either — it still persists what it is given: * * - **read** (PR #5272, {@link narrowPersonalizationOverlay}): the consumer * that MERGES an overlay over a source view contributes only these keys, so * every already-stored fat row stops shadowing its source. * - **write** (objectui#5233, `buildPersistedViewBody` in app-shell's * `ObjectView`, unblocked by `columnState`'s admission to the view-metadata * surface as a runtime-only overlay key — objectstack#9933, released in * `@objectstack/spec` 17.1.0): a *system view's* overlay is now written as * the patch alone, so no new row freezes anything, and because the write is * a whole-document PUT the next toggle also strips an old fat row. A *saved * view's* own row is deliberately still written whole — for it the body IS * the view, not a copy of one. * * A fat row is therefore a legacy shape, not a shape this product still * produces; the list below is still what a reader is allowed to trust from one. * * ⛔ Do not grow this list to make some other key "stick" through an overlay. * A key that belongs to the view belongs in the view; the overlay is a patch, * and a patch that carries the whole document is what this list exists to * stop. Adding a sixth entry is only correct alongside a sixth * `persistViewPatch` call site — {@link narrowPersonalizationOverlay} is what * a reader checks that against. */ declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFields", "columnState", "inlineEdit"]; /** * Reduce a personalization overlay row to the keys it owns — the read-side * half of objectui#5233, and the half that reaches rows ALREADY STORED. * * Rows written before this shipped carry the whole source view (see * {@link VIEW_OVERLAY_OWNED_KEYS}). Of the three dispositions the issue names * for them — strip on next write, migrate, tolerate on read — this is the * third, chosen deliberately and stated here rather than left implicit, * because it is the only one that is already true for every existing row the * moment it ships: strip-on-next-write heals a row only when its user happens * to touch that view again (and leaves the frozen filter live until then), * and a migration needs a runner this product does not have for `sys_metadata` * rows an operator may not even know exist. What the issue forbids is SILENT * tolerance; this is the explicit, pinned kind (`viewOverlayPatchOnly.test.ts` * in this package, `ObjectView.overlayPatchOnly.test.tsx` in app-shell). * * Applied by the consumer that MERGES an override over a source view * (app-shell's `sanitizeViewOverride`, the one seam both of * `loadViewOverrides`' read branches pass through), NOT by * {@link ObjectStackAdapter.listViewOverrides} / {@link ObjectStackAdapter.getView}: * those two answer with the stored DOCUMENT, their equality is itself pinned * ("same key space, same document" — `listViewOverrides.test.ts`), and * `InterfaceListPage` hydrates a hollow view out of that document. Narrowing * belongs where a row is read AS A PATCH, not where it is read as a row. * * Non-overlay rows — a genuine saved view's own body, which the same batch * read enumerates — are returned by REFERENCE, untouched: for those the row * IS the view, and every key on it is an opinion its author expressed. * Classification is {@link isPersonalizationOverlayRow}, the same predicate * {@link ObjectStackAdapter.listViews} excludes rows by, so a row cannot be a * saved view for one reader and an overlay for the other. */ declare function narrowPersonalizationOverlay(row: T): T; /** * ObjectStack Data Source Adapter * * Bridges the ObjectStack Client SDK with the ObjectUI DataSource interface. * This allows Object UI applications to seamlessly integrate with ObjectStack * backends while maintaining the universal DataSource abstraction. * * @example * ```typescript * import { ObjectStackAdapter } from '@object-ui/data-objectstack'; * * const dataSource = new ObjectStackAdapter({ * baseUrl: 'https://api.example.com', * token: 'your-api-token', * autoReconnect: true, * maxReconnectAttempts: 5 * }); * * // Monitor connection state * dataSource.onConnectionStateChange((event) => { * console.log('Connection state:', event.state); * }); * * const users = await dataSource.find('users', { * $filter: { status: 'active' }, * $top: 10 * }); * ``` */ declare class ObjectStackAdapter implements DataSource { private client; private connected; private connectPromise; private metadataCache; private connectionState; private connectionStateListeners; private batchProgressListeners; private autoReconnect; private maxReconnectAttempts; private reconnectDelay; private reconnectAttempts; private baseUrl; private token?; /** One "analytics capability is missing" console line per adapter, not per widget. */ private analyticsCapabilityWarned; private fetchImpl; private inflightFinds; private missingResources; private batchUnsupported; private atomicBatchCapability; private mutationListeners; private writeWarningListeners; private saveAdvisoryListeners; constructor(config: { baseUrl: string; token?: string; fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise; cache?: { maxSize?: number; ttl?: number; }; autoReconnect?: boolean; maxReconnectAttempts?: number; reconnectDelay?: number; }); /** * Ensure the client is connected to the server. * Call this before making requests or it will auto-connect on first request. */ connect(): Promise; /** * Attempt to reconnect to the server with exponential backoff */ private attemptReconnect; /** * Get the current connection state */ getConnectionState(): ConnectionState; /** * Check if the adapter is currently connected */ isConnected(): boolean; /** * Register a listener for connection state changes */ onConnectionStateChange(listener: ConnectionStateListener): () => void; /** * Register a listener for batch operation progress */ onBatchProgress(listener: BatchProgressListener): () => void; /** * Set connection state and notify listeners */ private setConnectionState; /** * Emit batch progress event to listeners */ private emitBatchProgress; /** * Find multiple records with query parameters. * Converts OData-style params to ObjectStack query options. */ find(resource: string, params?: QueryParams): Promise>; /** * Full-text search across every searchable object in a single round-trip. * * Hits `GET /api/v1/search?q=`, the platform's global search endpoint served * by the registered search service (the pinyin full-text plugin) and backed * by `metadata-protocol`'s `searchAll`. Unlike `find(resource, { $search })` * — a per-object metadata-driven search (ADR-0061) — this consults the search * index and ranks hits across objects, so it surfaces records the per-object * fanout misses. Global affordances (⌘K command palette, search page) prefer * this path (framework #3371). * * Returns `{ query, hits }`. A backend without the search plugin installed * answers `404`; we treat that as "no global search here" and return an empty * hit set so callers can fall back to a per-object fanout rather than surface * an error. */ searchAll(query: string, options?: { limit?: number; objects?: string[]; }): Promise; /** * Find a single record by ID. */ findOne(resource: string, id: string | number, params?: QueryParams): Promise; /** * Create a new record. */ /** * Notify all mutation subscribers. A throwing listener must not break the * mutation or starve the other subscribers, so each is isolated. */ private emitMutation; /** * Subscribe to create/update/delete events on any resource. Returns an * unsubscribe function. Data-bound views use this to auto-refresh after a * mutation (e.g. inline-edit "Save All", which writes through `update` and * must repaint the list without a manual reload). */ onMutation(callback: (event: DataSourceMutationEvent) => void): () => void; /** * Notify all write-warning subscribers. Isolated like {@link emitMutation}: a * throwing listener must not break the write or starve the others. */ private emitWriteWarning; /** * Read `droppedFields` off a create/update response (framework #3431/#3455) * and, when present, notify write-warning subscribers. Tolerant of a client * whose response type predates `droppedFields`: the field is read structurally * and validated, so an older client (or a backend that never drops) is a no-op. */ private notifyDroppedFields; /** * Same, for the cross-object transactional batch (framework #3794). Its * response hangs the events off a top-level `droppedFields` list, each tagged * with the `index` of the operation it came from — `results` entries are bare * record echoes with nowhere to hang a per-row list. * * This is the path that matters most for the warning: `batchTransaction` is * how the console's record form saves a master-detail record, so a * `readonlyWhen`-locked field edited in that form was stripped server-side * while the UI reported a plain success. The operation kind is taken from the * originating op so the toast doesn't call an update a create. */ private notifyBatchDroppedFields; /** * Subscribe to write-warning events (a create/update dropped caller-supplied * fields — #3431/#3455). Returns an unsubscribe function. The app shell uses * this to toast the user; the write itself already succeeded. */ onWriteWarning(callback: WriteWarningListener): () => void; /** * Subscribe to metadata save-advisory events — the runtime authoring gate's * advisory findings on a save that SUCCEEDED (#4237; backend * objectstack#7435). Returns an unsubscribe function. * * Deliberately the same seam as {@link onWriteWarning} (#3431/#3455), which * is what {@link MetadataSaveAdvisoryEvent}'s own declaration already said it * was modelled on. It is a SIBLING of that channel rather than a second * payload pushed down it: `WriteWarningEvent` is a closed shape whose * `droppedFields` is required and means "fields the write legally stripped", * so carrying advisories on it would either force every existing * `onWriteWarning` consumer to grow a branch or make the event lie about what * happened. The seam's SHAPE is what is reused here — a long-lived instance * with a `subscribe → unsubscribe` registration that `AdapterProvider` wires * once — not its event type. * * Why here and not on the config, which is how the other client class does it * (#4133/#4236): `MetadataClient` is minted per component by * `useMetadataClient`, so it has no instance to subscribe to and its sink * rides the factory. `ObjectStackAdapter` is the opposite — one long-lived * instance per app, already carrying this exact subscription pattern. */ onSaveAdvisory(callback: MetadataSaveAdvisoryListener): () => void; /** * Notify all save-advisory subscribers. Isolated exactly like * {@link emitWriteWarning}: a throwing listener must neither break the save * nor starve the others. */ private emitSaveAdvisory; /** * Install the ONE emitter for the metadata save door (#4237). * * ## Why this seam, and what it covers * * `ObjectStackClient.meta.saveItem` is the second client class that writes * through `PUT /api/v1/meta/:type/:name`, and every one of its callers reaches * it through an adapter this class constructed — the four inside this file * (`updateViewConfig`, the two view paths, `updateDashboard`) via * `this.client`, and every caller outside it via {@link getClient}, which * hands back this same instance: `MetadataService` (app-shell, five saves), * `useNavigationSync`, and plugin-designer's Create/EditAppPage. Wrapping the * method once here therefore covers all of them WITHOUT a per-site edit, which * is the whole point — a toast copied into a dozen call sites is the shape * #4133 rejected for the other client class and it is no better here. * * `meta` is an own, writable property assigned per instance in the SDK's * constructor (`this.meta = { … }`), and the client this adapter builds is * never shared, so the wrap is bounded to an object this adapter owns for its * whole lifetime. It is not a prototype or global patch. * * ## Response shape — measured, not assumed * * The two client classes' envelopes coincide at the top level, which is what * makes `readSaveAdvisories` reusable unchanged across both. `SaveMetaItem- * ResponseSchema` puts `advisories` at the body's top level next to * `success` / `version` / `seq` / `state`, and the SDK's `unwrapResponse` * strips its `{ success, data }` envelope only when the body actually HAS a * `data` key — this body does not, so it is returned verbatim. So the same * reader that `MetadataClient.save` uses reads this response correctly, and * the pins in `onSaveAdvisory.test.ts` drive a real SDK client through a fake * `fetch` rather than stubbing `meta`, so that continues to be measured. * * ## Draft-door honesty (D1) * * Drafts are NEVER gated: the framework returns at its D1 early-return * (`if (args.state !== 'active') return null`) before running a rule, so a * draft save produces no findings to withhold. This client class has no draft * door at all to worry about — the SDK's `saveItem(type, name, item)` takes no * mode and always writes the active door, which is exactly why the gate DOES * run for its callers. `mode` on the emitted event is therefore derived from * the response's own `state` rather than from a request-side flag that does * not exist here: `'draft'` when the server says the row landed as a draft, * `'publish'` otherwise. That keeps the event truthful about which door it * came through instead of hard-coding one. */ private installSaveAdvisoryInterceptor; create(resource: string, data: Partial): Promise; /** * Update an existing record. * * Optional `opts.ifMatch` enables Optimistic Concurrency Control: the * server compares the supplied token (typically the `updated_at` value * the caller previously read) against the record's current version * and throws a {@link ConcurrentUpdateError} on mismatch (HTTP 409). * * Requires `@objectstack/client@>=4.2.0`, which forwards `opts.ifMatch` * as an `If-Match` HTTP header. */ update(resource: string, id: string | number, data: Partial, opts?: { ifMatch?: string; }): Promise; /** * Delete a record. * * Optional `opts.ifMatch` enables Optimistic Concurrency Control — * see {@link update} for details. On 409 the call rejects with * a {@link ConcurrentUpdateError}. */ delete(resource: string, id: string | number, opts?: { ifMatch?: string; }): Promise; /** * Apply the same patch to many records in a single round-trip. * * Sends one `POST /api/v1/data/:object/updateMany` request whose body * is `{ records: ids.map(id => ({id, data: patch})), options: { continueOnError: true }}`. * The server iterates server-side (still N engine writes) but the * client only pays for ONE HTTP/auth/RLS round-trip — the relevant * perf win for inbox / list-toolbar "mark all read" / "archive * selected" interactions where N can easily be in the hundreds. * * Falls back to a sequential per-id loop when the connected client * does not expose `updateMany` (older clients / offline adapters). * In that case `continueOnError` semantics are emulated locally so * callers see the same return shape. */ bulkUpdate(resource: string, ids: ReadonlyArray, patch: Partial): Promise; /** * Single-call bulk delete. Mirrors the bulkUpdate contract: prefers * the server's `deleteMany` primitive when the client supports it; * otherwise emulates `continueOnError` by looping `delete` per id and * swallowing per-row failures. Returns the count of rows reported * deleted by the server (or successfully deleted in fallback mode). */ bulkDelete(resource: string, ids: ReadonlyArray): Promise; /** * Bulk operations with optimized batch processing and error handling. * Emits progress events for tracking operation status. * * @param resource - Resource name * @param operation - Operation type (create, update, delete) * @param data - Array of records to process * @returns Promise resolving to array of results */ /** * Cross-object transactional batch (ObjectStack #1604 / ADR-0034 item 4). * Runs the operations in ONE server transaction — commit all or roll back * all. A field value of `{ $ref: }` resolves to that op's * created id, so a child can reference its parent created earlier in the same * batch (master-detail). * * Transport: the published `@objectstack/client` SDK method * `data.batchTransaction` (framework #3271; shipped since client v16, our * dependency floor). Per AGENTS.md §7 data always flows through the client — * never a hand-rolled `fetch('/api/v1/batch')`. * * Fallback decision — declarative capability negotiation (framework #3298 / * objectui #2693). At connect() we read `capabilities.transactionalBatch` * from discovery: * - Declared `true` → the backend GUARANTEES atomicity (declared === * enforced). We TRUST it: any batch failure — including 404/405/501 — * surfaces as a real error. No non-atomic client-side compensation. This * is the path modern backends take. * - Declared `false`, or ABSENT (backend predates #3298) → we can't rely on * server atomicity, so we keep the legacy behaviour: on 404/405 (no * endpoint) or 501 (runtime without transactions) degrade to the * client-side, NON-atomic {@link emulateBatchTransaction} so a save is * still possible. Removing that here would regress older backends from * "saves, less safe" to "no save path" (#2679 compatibility constraint). * The non-atomic fallback stays isolated to THIS adapter. */ batchTransaction(operations: BatchTransactionOperation[]): Promise<{ results: any[]; }>; /** True for statuses that mean "this backend can't do a transactional batch". */ private batchStatusUnsupported; /** Best-effort HTTP status extraction from a thrown SDK/client error. */ private errorStatusOf; /** Mark the endpoint unsupported (warn once) and serve via emulation. */ private fallbackToEmulation; /** * Emit one DataSourceMutationEvent per committed operation so the invalidation bus * (#2269) sees writes that went through /batch exactly like single * create/update/delete calls — master-detail ModalForm saves otherwise leave * related lists and count badges stale (#2582). `results` is index-aligned * with `operations`; creates take id/record from the server echo. * * Only called on the server-committed paths. The emulation branch drives the * adapter's own create/update/delete primitives, which already emit — so it * must NOT be routed through here, or events would double-fire. */ private emitBatchMutations; bulk(resource: string, operation: 'create' | 'update' | 'delete', data: Partial[]): Promise; /** * Bulk-import raw spreadsheet rows in a single server round-trip via * `POST /api/v1/data/:object/import`. The server performs all value coercion * (booleans, numbers, dates→ISO, select label→code, lookup name→id) from the * object's field metadata, so this method forwards the request verbatim and * returns the aggregate + per-row result untouched. * * Requires `@objectstack/client` with `data.import` (server `/import` route). * Callers should feature-detect (`typeof dataSource.importRecords`) and fall * back to a per-row `create` loop when unavailable. */ importRecords(resource: string, request: ImportRequestOptions): Promise; /** * Feature-detect the async import-job API on the connected client. Older * clients/servers lack these routes; callers fall back to {@link importRecords}. */ private importJobApi; /** * Start an asynchronous import job — the large-file counterpart to * {@link importRecords}. Posts the whole payload once; the server processes * rows in the background. Requires an `@objectstack/client` new enough to * expose `data.createImportJob` (server `/import/jobs` route). Callers should * feature-detect (`typeof dataSource.createImportJob`) and fall back to the * synchronous path when unavailable. */ createImportJob(resource: string, request: ImportRequestOptions): Promise; /** Poll an import job's progress. Requires {@link createImportJob} support. */ getImportJobProgress(jobId: string): Promise; /** Fetch an import job's capped per-row results. */ getImportJobResults(jobId: string): Promise; /** List recent import jobs (history), newest first. */ listImportJobs(options?: ListImportJobsOptions): Promise; /** Cancel a pending/running import job (cooperative). */ cancelImportJob(jobId: string): Promise; /** * Logically roll back a finished import job — delete the records it created * and restore the records it updated to their pre-import values. Requires an * `@objectstack/client` new enough to expose `data.undoImportJob`, and a job * the server captured an undo log for (see {@link ImportJobProgressInfo.undoable}). */ undoImportJob(jobId: string): Promise; /** * Normalize the result from data.find() or data.query() into a consistent QueryResult. */ private normalizeQueryResult; /** * Make a raw GET request to the data API with `populate` as a URL query param. * Used when $expand is needed, since the client SDK's data.find() does not * support populate/expand. The server's REST API routes GET /data/:object * to findData({ object, query: req.query }) which processes `populate`. */ private rawFindWithPopulate; /** * Synchronously download a server-streamed export (csv / json / xlsx). * * Hits `GET /api/v1/data/:object/export`, which streams matching rows in the * requested format, formats values for readability (lookup → name, select → * label, boolean → 是/否, dates formatted) and enforces permissions. The * filter / sort are translated the same way as `rawFindWithPopulate` so the * exported file mirrors the active list view. Returns the file as a Blob; * the caller triggers the browser download. */ exportDownload(resource: string, request?: ExportDownloadRequest): Promise; /** * Convert ObjectUI QueryParams to ObjectStack QueryOptions. * Maps OData-style conventions to ObjectStack conventions. */ private convertQueryParams; /** * Get object schema/metadata from ObjectStack. * Uses caching to improve performance for repeated requests. * * @param objectName - Object name * @returns Promise resolving to the object schema */ getObjectSchema(objectName: string): Promise; /** * ADR-0056 P2 (epic #2398) — stamp structured-widget hints onto platform * fields whose framework type is a storage primitive (e.g. `textarea`) but * whose authoring UX should be a structured editor. Only the render `widget` * is added; the field's `type` (the storage contract) is untouched. Applied * idempotently to the cached schema so form + detail both honor it. Widget * components are registered as `field:` in `@object-ui/fields`. */ private applyFieldWidgetOverrides; /** * Fetch a single object's schema while always revalidating the browser cache. * * The server serves `GET /api/v1/meta/object/:name` with * `Cache-Control: public, max-age=3600`, so the default `fetch` the SDK uses * keeps returning the same response from the browser HTTP cache for up to an * hour without contacting the origin. Because the create/edit form reads the * object schema through {@link getObjectSchema}, a field added + published in * the same session never appears in the form even though it is live (the LIST * endpoint, `/meta/object`, is uncached — which is why list views update). * * Issuing the read with `cache: 'no-cache'` forces a conditional revalidation * (`If-None-Match`): a changed ETag returns the fresh schema, an unchanged one * still gets a cheap `304`. We go through `fetchImpl` (the adapter's * authenticated fetch) rather than `client.meta.getItem` because the SDK does * not expose the request cache mode. */ private fetchObjectSchemaFresh; /** * List every registered object (code- and DB-defined) from the metadata * registry — `GET /api/v1/meta/object`. Returns lightweight `{ name, label }` * headers for object-picker widgets (e.g. the sharing-rule `object-ref` * field). The list endpoint is uncached server-side, so no cache-busting * dance is needed. Returns `[]` on any failure so callers degrade gracefully. */ getObjects(): Promise>; /** * Get access to the underlying ObjectStack client for advanced operations. */ getClient(): ObjectStackClient; /** * Get the discovery information from the connected server. * Returns the capabilities and service status of the ObjectStack server. * * Note: This accesses an internal property of the ObjectStackClient. * The discovery data is populated during client.connect() and cached. * * @returns Promise resolving to discovery data, or null if not connected */ getDiscovery(): Promise; /** * **The one place that knows which cache keys a view-row write invalidates.** * * Two reads are cached under view-shaped keys, and both go stale when any * writer touches a view row for `objectName`: * * | reader | key | * |----------------------------|----------------------------| * | {@link getView} | `view:{object}:{viewName}` | * | {@link listViewOverrides} | `view-overrides:{object}` | * * Every writer routes here rather than restating that pair. The repo has now * paid twice for restatement: objectui#3778 removed five copies of a key * (`views:{object}`) that no reader had ever populated, and objectui#4363 * fixed four copies that named only half the live set — and objectui#4373 is * the measured proof that the failure recurs by default, because the console's * real create/publish flow writes these same rows through the ADR-0034 * metadata seam and never learned the key list at all. A pin suite can only * guard the writers that exist; a mandatory seam makes the next writer * structurally unable to forget. * * The rule is uniform per WRITE, not per branch: a write to a view row drops * both keys, in that order. Draft-addressed writes over-invalidate on purpose * — both readers enumerate PUBLISHED rows, so a draft write stales neither — * because the costs are not symmetric. An unnecessary invalidation costs one * refetch; a missed one costs up to the cache's 5-minute TTL of stale * overrides, and "which half am I in?" is not a question a future writer * should have to re-answer to stay correct. * * Public because the writers are not all in this class: app-shell's ADR-0034 * seam (`runtime-metadata-persistence.ts`) and `MetadataService` write the * same rows through `client.meta` / the `/meta` draft-publish API, and they * reach this adapter as the `dataSource` they were already handed. * * @param objectName - Object the view belongs to (the `{object}` in both keys) * @param viewName - The view's canonical `name` — the identity * {@link getView} reads back by and {@link listViewOverrides} keys its map * with. Callers that hold a qualified `.` name pass it as-is: * it is the row key, not a path to be split. */ invalidateViewKeys(objectName: string, viewName: string): void; /** * Batch-fetch all persisted view overrides for an object. * * Per-view runtime overrides (density, column widths, sort, hidden * columns, inlineEdit …) live in the SAME metadata namespace the * write path uses: `type='view'`, `name=` (see * {@link updateViewConfig}). Loading them per-view fires N HTTP GETs * that 404 for every view the user never customized — console noise on * every page load. This batch method performs a single * `GET /api/v1/meta/view` (returns `{type, items}`) and narrows the * result to `objectName` client-side, exactly as {@link listViews} * does over the same rows (shared accessor: {@link viewItemObjectName}). * * objectui#3774 — this used to enumerate `GET /api/v1/meta/`, * putting the OBJECT name in the metadata TYPE slot. That key space is * disjoint from the one the write path lands in, so the batch map came * back empty for every object, forever, and every saved personalization * read back as "setting didn't save". * * FAILURES REJECT — they are not answered as `{}`. An empty map is an * authoritative "this object has no overrides" (callers may trust it and * skip the per-view reads); a transport/permission failure is "I could * not tell", and reporting the two as the same value is what made * ObjectView's per-view {@link getView} fallback unreachable code. When * we cannot tell, we do not pretend we can. Rejections are not cached * (the cache stores on success only), so a transient failure does not * pin an empty answer for the TTL. * * Result is cached identically to {@link getView}, and EVERY view write — * on this adapter and above it — invalidates it through the one seam, * {@link invalidateViewKeys}. For a long time only {@link updateViewConfig} * did (objectui#4363), which left the other three adapter paths stale * for the cache's 5-minute TTL. That gap does not self-heal: the consumer * (`loadViewOverrides`, app-shell `ObjectView`) treats a RESOLVED map as * authoritative and deliberately does not re-probe per view — objectui#3774, * and correct, since re-probing reinstates the 404 flurry the batch read * exists to remove. So a stale map here is served in full, and the per-view * {@link getView} fallback that would have masked it never runs. * * @param objectName - Object name (e.g. 'lead') * @returns Map keyed by view name with the persisted override config * @throws whatever the metadata transport throws — callers that have a * per-view fallback should catch and use it. */ listViewOverrides(objectName: string): Promise>; /** * Get a view definition for an object. * Attempts to fetch from the server metadata API. * Falls back to null if the server doesn't provide view definitions, * allowing the consumer to use static config. * * @param objectName - Object name * @param viewId - View identifier * @returns Promise resolving to the view definition or null */ getView(objectName: string, viewId: string): Promise; /** * Persist a toolbar-driven view config patch — density, column widths, * sort, hidden columns, inline edit. Symmetric counterpart to * {@link getView}: writes the row to the server metadata store via * `client.meta.saveItem`, then invalidates the matching cache entry so the * next {@link getView} reflects the new payload. Returns the persisted item * when the server echoes it, otherwise undefined. * * Called from exactly ONE production site — `ObjectView`'s * `persistViewPatch`, for the toolbar toggle — but that ONE call site * fires for BOTH kinds of active tab: a code-defined **system** view (no * row of its own yet) and a genuinely user-created **saved** view (already * has a row — the toggle is editing ITS OWN definition, not laying an * overlay on top of it). Which one a given call means is NOT re-derived * here from the write's shape (objectui#4227's own lesson: shape inference * on this namespace is exactly what let a system view masquerade as * saved) — the caller already knows, via the same `isSavedViewId` * classification that gates the switcher's readonly flag and its five * mutating handlers, and passes it as {@link opts.isSavedView}. * * - `isSavedView` false/omitted (system-view target, the common case and * the default for backward compatibility): stamps * {@link VIEW_OVERLAY_MARKER} so {@link listViews} excludes the row — * the original objectui#4227 fix. * - `isSavedView` true: the marker is withheld. Stamping it here would * flag the saved view's OWN row as a personalization overlay, and * `listViews()` would exclude it on the very next read — the user's own * view would vanish from the switcher the moment they toggled its * density (objectui#4227 follow-up, PM review on PR #4713, measured: * `persistViewPatch` has no gate on which kind of tab is active, and * this method writes to the exact same `(type='view', name=viewId)` key * {@link createView}/the ADR-0034 `viewEnvelope` seam already used for * that view, so the write is an upsert onto the saved view's row, not a * new one). * * Per objectstack#7494's ruling, the overlay this writes (system-view * case) is ORG-WIDE shared view settings, not a per-user preference — a * true per-user scope is a parked v18 direction on the platform side. * * @param objectName - Object name (e.g. 'lead') * @param viewId - View identifier (e.g. 'all_leads') * @param config - Full view definition to persist * @param opts.isSavedView - Whether `viewId` already names a saved view * (vs. a system view being personalized for the first time). Omit / * `false` for the default overlay-marking behavior. */ updateViewConfig(objectName: string, viewId: string, config: Record, opts?: { isSavedView?: boolean; }): Promise | void>; /** * List user-created views for a given object via the metadata overlay * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path * that wrote to a physical `sys_view` table whose columns no longer * match the view spec shape. * * Returns view spec objects with their canonical `name` as identifier. * Narrows to one object client-side via {@link viewItemObjectName} — * the metadata index is name-only, not field-typed, so the route has no * `?object=` to push the filter down into. {@link listViewOverrides} * reads the same rows through the same accessor. */ listViews(objectName: string, options?: { previewDrafts?: boolean; }): Promise; /** * Build a {@link MetadataClient} bound to this adapter's server + auth. Used * by draft-aware reads (`listViews({ previewDrafts })`) so the `/meta` route, * `?preview=draft` flag, and environment scoping live in the SDK rather than * being hand-assembled at each call site (#2767 P3). */ private metadataClient; /** * List registered import `mapping` artifacts targeting a given object * (framework #2611). Reads the `mapping` metadata kind via the overlay API * and filters by `targetObject` client-side (the metadata index is * name-only). Feeds the import wizard's "saved mapping" selector; a failure * (older server without the `mapping` kind) degrades to an empty list, so * the selector simply doesn't appear. */ listImportMappings(objectName: string): Promise; /** * Create a new overlay view for an object. The view's `name` is the * stable identifier — must be unique within the project scope. Returns * the persisted view spec (or undefined when the server doesn't echo). * * Generates a snake_case name if `spec.name` is not provided by appending * a short timestamp suffix to the source-name hint. * * Invalidates through {@link invalidateViewKeys}, like every other writer — * see {@link listViewOverrides} for why the batch map is the one that cannot * heal itself (objectui#4363). */ createView(objectName: string, spec: Record): Promise | void>; /** * Apply a partial update to an existing overlay view. Reads the current * document, merges, and writes it back. ADR-0005 overlay rows store the * *full* view document, so partial updates require a read-merge-write cycle. * * **Both halves address the same row (#4139).** A view has two possible * homes and the read must resolve the one the write will target: * * - a pending per-item **draft** (`?state=draft` / `?mode=draft`) — where * ADR-0034 stages every runtime-created view, so a view made from the `+` * tab lives ONLY here until an explicit Publish; * - the **published** overlay (`client.meta.getItem` / `saveItem`). * * The draft is probed FIRST, and a hit is merged and written straight back * as a draft. Two things that ordering buys, both load-bearing: * * 1. A draft-only view is no longer invisible to the read. It used to 404, * and a `catch {}` labelled "treat missing as create-equivalent" * substituted `current = {}` — so a rename merged onto nothing and went * out as a `{label, name, object}` partial the server rejects (422), * while the draft row the UI reads back through `?preview=draft` kept the * old label. The edit was lost with no error surfaced to the user. * 2. A draft is never bypassed. Writing the published row while a draft is * pending would put the edit somewhere the draft shadows — and Publish * would then overwrite it with the pre-edit body, losing the change a * second time, later, where nothing connects it to this call. * * A draft edit stays a draft: `mode: 'draft'` keeps ADR-0037's guarantee * that nothing the preview shows goes live until Publish. Renaming a * *published* view (no draft pending) is unchanged — it writes the * published overlay, as before. * * **Both halves invalidate through {@link invalidateViewKeys}** (objectui#4363, * #4373) — same call, same key set, so the draft half's deliberate * over-invalidation is the seam's uniform rule rather than a per-branch * decision this method has to keep re-making. * * @throws when the view resolves in neither home, or when either read fails * for any other reason (network, permission). Both used to be swallowed * and converted into the bad partial write above; a caller that wants a * view created should call {@link createView}, which is the operation that * actually means "create". */ updateView(objectName: string, viewName: string, partial: Record): Promise | void>; /** * Delete an overlay view — from **every home it has**. * * A view has two possible homes, the same two {@link updateView} addresses: * the pending per-item **draft** (`DELETE /meta/view/:name?state=draft`) and * the **published** overlay (`DELETE /meta/view/:name`). This method used to * issue only the second, and the three cases came out like this (#4479): * * | case | before | * |--------------------|-----------------------------------------------------| * | draft-only view | BUG — the published delete answered `reset:false` / | * | | "nothing to delete", the draft survived, and the tab | * | | was still there after reload | * | published-only | correct — `reset:true`, tab gone | * | published + draft | ACCIDENTALLY correct — the published row went, so | * | | the tab went; the orphan draft stayed behind | * * **Why this is not the mechanical mirror of #4139.** `updateView` probes * the draft first and writes back to whichever home the read resolved, and * that is right for an update in all three cases. Copying it here would be * wrong in the third: `persistRuntimeMetadata` (app-shell) stages EVERY * runtime edit as a draft, so "publish a view, then edit it" routinely * produces a pair — and a draft-first-only delete on a pair discards the * draft and leaves the published row serving the view. That is not Delete * view, it is **Discard draft**, a deliberately different operation that * already exists (`discardRuntimeDraft`, documented as "the published * overlay is untouched"). The clean statement of the asymmetry: for an * update, one home is the right home; for a delete, "remove this view" is * satisfied only when NO home is left serving it. * * So both homes are deleted, **draft first**. The order is load-bearing on * the failure path: a fault between the two calls leaves the PUBLISHED * overlay intact, so the view is still served and the operation is cleanly * retryable. The reverse order would strand a draft-only view — which is * precisely the bug shape above. * * **Two blind calls, no probe.** The framework's `deleteMetaItem` answers a * missing home with a **200** carrying `reset:false` (`"No pending draft * for view/x."` / `"No view 'x' found — nothing to delete."`), never a 404, * so there is nothing for a probe to protect against. `updateView` needs its * probe for a different reason — its read must resolve the row the merge * writes back to — and that reason has no counterpart for a delete. * * **One transport, one error contract.** Both halves go through * {@link MetadataClient} (`reset`), the transport that can express the * `?state=` qualifier and the one `updateView`'s draft half already uses. * The published half used to go through `client.meta.deleteItem`; measured, * that issues the byte-identical request (`DELETE * {baseUrl}/api/v1/meta/view/:name`, no environment scoping is configured on * this adapter), so routing it here costs no addressing change and buys a * single `MetadataError` shape across both calls instead of two. * * @returns `deleted` is true only when no home is left serving the view AND * at least one actually held a row. A view that existed in neither home * still answers `false`, unchanged. The per-home outcomes are additive: * a partial result is observable rather than rounded up to `true`. * @throws when either delete fails, matching {@link updateView}'s * convention of surfacing the fault rather than degrading. A failure of * the PUBLISHED half after the draft was discarded carries the partial * state on the error's `outcome` — "draft gone, overlay left" is exactly * what the old `{ deleted: boolean }` could not express. * * Invalidates through {@link invalidateViewKeys}: the deleted row leaves the * batch override map too, and a ghost entry there is what the object page * would keep applying (objectui#4363). Fired in a `finally`, so it happens * once per call on EVERY outcome including the throw — after a half-failure * the draft row really is gone, and #4363's asymmetry decides it: an * unnecessary invalidation costs one refetch, a missed one costs the cache's * full 5-minute TTL of stale overrides. */ deleteView(objectName: string, viewName: string): Promise; /** * Get an application definition by name or ID. * Attempts to fetch from the server metadata API. * Falls back to null if the server doesn't provide app definitions, * allowing the consumer to use static config. * * @param appId - Application identifier * @returns Promise resolving to the app definition or null */ getApp(appId: string): Promise; /** * Ask the by-name meta app route WHY an app is not in this session's app list * (objectui#4252). * * The app LIST is the generic metadata list route `GET /api/v1/meta/:type`, * requested with the singular type segment `app` — the same address this * method appends a name to below, and the one `MetadataProvider` reads its * items from. The server filters that list per session in `filterAppForUser` * (`packages/rest/src/rest-server.ts`, applied inside the `:type` list handler * once the type segment resolves to `app`), so an app withheld by * `requiredPermissions` and an app * that does not exist are byte-identical there: both are simply absent. A * console reading only that list has one fact and two conditions, and it * renders its copy for the wrong one — "it may still be publishing" over a * permanent authorization decision, which cost a downstream acceptance round * two test batches spent chasing a platform defect that was a missing * permission-set binding. * * The maintainer ruling (2026-08-12) put the answer on the BY-NAME route * rather than in the list, so the enumeration surface is not widened past what * a by-name probe already implies (objectstack#8013 / PR #8135): an app that * exists and whose `requiredPermissions` the session lacks answers `403` with * `PERMISSION_DENIED` in the declared envelope, and absence — a nonexistent * name, an unpublished app, an app gated by an absent optional service — * keeps answering `404 RESOURCE_NOT_FOUND`. * * ## Why this is a separate method and not a flavour of {@link getApp} * * - `getApp` degrades EVERY failure to `null`, which is exactly the * conflation this exists to undo; changing it would silently re-point its * own callers' fallback-to-static-config path. * - `getApp` memoises in `metadataCache`. A verdict about the CALLER must not * be cached beside a document about the APP — one grant, and a cached * denial outlives the session it described. * * Nothing here throws: a probe that cannot reach an answer returns `unknown` * and the caller keeps whatever it was already showing. Only the measured * `code` produces `denied` — never a status, never a message (objectui#4408). * * @param appName - the app name as it appears in the URL segment */ probeAppAccess(appName: string): Promise; /** * Get a page definition from ObjectStack. * Uses the metadata API to fetch page layouts. * Returns null if the server doesn't support page metadata. */ getPage(pageId: string): Promise; /** * Update (upsert) a dashboard definition. * * Dashboards are control-plane metadata, not data records. Persist via * `client.meta.saveItem('dashboard', name, schema)` which routes to * `PUT /api/v1/meta/dashboard/:name`. After save, invalidates the * relevant metadata cache entry so the next dashboard read reflects * the new payload. * * @param dashboardName - Dashboard identifier (e.g. 'crm_overview_dashboard') * @param schema - Full dashboard schema (widgets, layout, etc.) */ updateDashboard(dashboardName: string, schema: Record): Promise | void>; /** * Perform server-side aggregation via the ObjectStack analytics API. * Uses `this.client.analytics.query()` from @objectstack/client to leverage * the SDK's built-in auth, headers, and fetch configuration. * Falls back to client-side aggregation via find() if the analytics endpoint * is not available. */ aggregate(resource: string, params: any): Promise; /** * Client-side aggregation over a server-scoped `find()` — the fallback used * whenever the analytics endpoint cannot answer (capability absent, network * failure, or a result whose measure came back missing). * * Forwarding `params.filter` is load-bearing: without it the fallback * aggregates the whole table while the caller believes it applied a filter, * which is the "KPI silently sums everything" failure this adapter has * guarded against since the widget filter was threaded through. */ private aggregateViaFind; /** * Say "the analytics capability isn't installed" ONCE per adapter, not once * per widget: a dashboard fans out one aggregate() per KPI, and N identical * console lines read like N different failures. */ private warnAnalyticsCapabilityOnce; /** * Run a semantic-layer `dataset` (ADR-0021) and return chart-ready rows. * * Posts to `POST /api/v1/analytics/dataset/query` (see `@objectstack/rest` * `registerAnalyticsEndpoints`). Accepts either a saved dataset name or an * inline draft definition — the inline form is what the Studio dataset * editor sends to preview an unsaved draft. The adapter's bearer token is * forwarded so tenant/RLS scoping (ADR-0021 D-C) is enforced server-side. * * Unlike {@link aggregate}, this does NOT fall back to client-side * aggregation: cross-object joins can only run on the server, so a failure * is surfaced to the caller (the preview panel shows the error) rather than * silently returning wrong numbers. * * @param dataset - An inline dataset definition (draft) OR a saved dataset name. * @param selection - The spec's {@link DatasetSelection} — dimension/measure * names to project plus runtime directives. This parameter IS the spec type * by reference, never a local restatement of it (objectui#3613): a hand * copy of a contract is a second dialect of it, and the copy this replaced * had already drifted three ways from `@objectstack/spec` — it required * `compareTo.dimension` (optional since objectstack#5011, and resolved by * the EXECUTOR, so requiring it pushed callers into exactly the * consumer-side dimension guess AGENTS.md #0.1 forbids), it widened * `timeDimensions` to `unknown[]` and `runtimeFilter` to * `Record`, and it had never grown `dateGranularity` at * all. Pinned in `queryDataset.test.ts`. */ queryDataset(dataset: Record | string, selection: DatasetSelection): Promise<{ rows: Array>; /** * Column metadata — the spec's `AnalyticsResult.fields[]` element BY * REFERENCE, never a local restatement of it (objectui#3752). Read * `@objectstack/spec` for what a column carries; this comment deliberately * does not re-list the keys, because the enumeration it replaced was the * bug: it named five (`name`/`type`/`label`/`format`/`currency`) and stopped * at the contract of the day it was written, so it never grew * `percentScale` — the server's answer to whether a percentage column is a * 0–1 fraction or already percentage points. The spec says a renderer that * receives it "must scale by it instead of guessing from the value" * (objectui#3136), so a declaration that hides the key steers a typed * consumer into exactly the guess-by-magnitude the issue banned. Pinned in * `queryDataset.test.ts`. * * Only this element is spec-owned: the envelope around it (`object` / * `dimensionFields` / `drillRawRows`) is ADR-0021 D2 drill metadata the REST * route adds on top of `AnalyticsResult`, and this method never returns the * result's `sql`, so the whole envelope is NOT an `AnalyticsResult`. */ fields: Array; /** ADR-0021 D2 drill-through: the dataset's base object (records to drill into). */ object?: string; /** Drillable dimension NAME → underlying object FIELD name. */ dimensionFields?: Record; /** Raw grouped values per row (aligned to `rows` by index) for drill filters. */ drillRawRows?: Array>; /** * Half-open date-range drill scope per row (framework#1752), aligned to * `rows` by index: dimension NAME → the field and `[gte, lt)` bounds of that * row's time bucket. The RANGE companion to `drillRawRows`, which handles * equality dims only — a `dateGranularity` dimension groups a SPAN of * records into one bucket, so the server excludes date dims from * `dimensionFields`/`drillRawRows` and sends this sidecar instead. * * The entry type is `@object-ui/core`'s `DatasetDrillRange` BY REFERENCE, * not a local restatement of it (objectui#3613/#3752 discipline): the same * declaration is what `buildDatasetDrillFilter` — the single consumer that * turns these bounds into an ObjectQL `{ $gte, $lt }` — accepts, and what * `DatasetWidget` / `DatasetReportRenderer` type their state with. Nothing * in `@objectstack/spec` owns this shape yet (the server's own * `AnalyticsResultWithDrill` is local to `service-analytics`), so the shared * in-repo interface is the one contract available; restating it here would * make a third dialect of it. Like `drillRawRows`, only the ARRAY is * validated below — the bounds are unvalidated payload, which is exactly why * `DatasetDrillRange` declares them `unknown`. */ drillRanges?: Array>; /** Server-computed marginal aggregates, one entry per requested grouping. */ totals?: Array<{ dimensions: string[]; rows: Array>; }>; }>; /** Client-side aggregation fallback */ /** * The result column an object-bound `aggregate` projects its value under * (framework#3701, `chartAggregateValueKey` in `@objectstack/spec/ui`): the * raw `field` name — no `sum_`-style decoration, unlike a dataset measure — * or the literal `count` when a count names no field. */ private aggregateValueKey; private aggregateClientSide; /** * Get multiple metadata items from ObjectStack. * Uses v3.0.0 metadata API pattern: getItems for batch retrieval. */ getItems(category: string, names: string[]): Promise; /** * Get cached metadata if available, without triggering a fetch. * Uses v3.0.0 metadata API pattern: getCached for synchronous cache access. */ getCached(key: string): unknown | undefined; /** * Get cache statistics for monitoring performance. */ getCacheStats(): MetadataCacheStats; /** * Invalidate metadata cache entries. * * @param key - Optional key to invalidate. If omitted, invalidates all entries. */ invalidateCache(key?: string): void; /** * Clear all cache entries and statistics. */ clearCache(): void; /** * Upload a single file to a resource. * Posts the file as multipart/form-data to the ObjectStack server. * * @param resource - The resource/object name to attach the file to * @param file - File object or Blob to upload * @param options - Additional upload options (recordId, fieldName, metadata) * @returns Promise resolving to the upload result (file URL, metadata) */ uploadFile(resource: string, file: File | Blob, options?: { recordId?: string; fieldName?: string; metadata?: Record; onProgress?: (percent: number) => void; }): Promise; /** * Upload multiple files to a resource. * Posts all files as a single multipart/form-data request. * * @param resource - The resource/object name to attach the files to * @param files - Array of File objects or Blobs to upload * @param options - Additional upload options * @returns Promise resolving to array of upload results */ uploadFiles(resource: string, files: (File | Blob)[], options?: { recordId?: string; fieldName?: string; metadata?: Record; onProgress?: (percent: number) => void; }): Promise; /** * Cancel (recall) the active pending approval request for a given record. * * Looks up the most recent `sys_approval_request` for the (object, record) * pair whose status is `pending` or `in_approval`, then issues a POST to * `/api/v1/approvals/requests/:id/recall`. The submitter is the only role * permitted to recall on the server — non-submitters will receive a 403. * * On success, the backend mirrors `approval_status = 'recalled'` onto the * source record so the lock badge disappears on next fetch. */ cancelPendingApproval(objectName: string, recordId: string): Promise<{ requestId: string; status: string; }>; /** * Get authorization headers from the adapter config. */ private getAuthHeaders; } /** * Factory function to create an ObjectStack data source. * * @example * ```typescript * const dataSource = createObjectStackAdapter({ * baseUrl: process.env.API_URL, * token: process.env.API_TOKEN, * cache: { maxSize: 100, ttl: 300000 }, * autoReconnect: true, * maxReconnectAttempts: 5 * }); * ``` */ declare function createObjectStackAdapter(config: { baseUrl: string; token?: string; fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise; cache?: { maxSize?: number; ttl?: number; }; autoReconnect?: boolean; maxReconnectAttempts?: number; reconnectDelay?: number; }): DataSource; export { API_ACCESS_DENIED_CODES, APP_PERMISSION_DENIED_CODE, AnalyticsDatasetNotFoundError, AnalyticsNotInstalledError, AnalyticsQueryRejectedError, AnalyticsUnauthenticatedError, type AppAccessVerdict, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, DataApiValidationError, FILTER_OPERATOR_ALIASES, MalformedFilterError, type MetadataAuditEntry, type MetadataAuditResponse, type MetadataCacheStats, MetadataClient, type MetadataClientConfig, type MetadataClientSaveOptions, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, type MetadataLockState, MetadataNotFoundError, type MetadataOverlayScope, type MetadataReference, type MetadataSaveAdvisoryEvent, type MetadataSaveAdvisoryListener, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type UserDataAdapter, VIEW_OVERLAY_OWNED_KEYS, type WriteWarningEvent, type WriteWarningListener, classifyAnalyticsFailure, clearSharedDiscoveryCache, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, createQuietHttpLogger, formatHttpFailureMessage, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isApiAccessDeniedError, isAppPermissionDeniedError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isObjectStackError, narrowPersonalizationOverlay, normaliseClientError, readSaveAdvisories, readTransactionalBatchCapability, serializeOrderBy, viewItemObjectName };