import { CodingProjectServiceError, type CodingProjectService, } from '../coding-projects/project-service'; import { getValidWorksSquareAccessToken, } from './works-square-session'; import { WORKS_SQUARE_CONFIG } from '../api/works-config'; import { proxyAwareFetch } from '../utils/proxy-fetch'; import { type DataServiceCollectionRemoval, type DataServiceCollectionTargetInput, type DataServiceConfirmationInput, type DataServiceDocument, type DataServiceDocumentList, type DataServiceDocumentTargetInput, type DataServiceErrorContext, type DataServiceHostResult, type DataServiceInstanceList, type DataServiceInstanceRemoval, type DataServiceInstanceState, type DataServicePutDocumentInput, } from '../../shared/data-service'; const MAX_REQUEST_BYTES = 98_304; const MAX_RESPONSE_BYTES = 1_310_720; const MAX_CURSOR_LENGTH = 1_024; const MAX_RETRY_AFTER_SECONDS = 86_400; const PROJECT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/; const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/; const KNOWN_ERROR_CODES = new Set([ 'invalid_project_id', 'invalid_collection_name', 'invalid_document_id', 'invalid_document_data', 'invalid_revision', 'invalid_cursor', 'cursor_expired', 'instance_not_found', 'collection_not_found', 'document_not_found', 'revision_conflict', 'document_too_large', 'quota_exceeded', 'rate_limited', ]); const CONTEXT_KEYS = new Set([ 'resource', 'limit', 'current', 'attempted', 'actual', 'allowed', 'current_revision', 'retry_after_seconds', ]); const CONTEXT_RESOURCES = new Set(['instances', 'collections', 'documents', 'bytes']); const ERROR_STATUS_BY_CODE: Readonly> = { invalid_cursor: 400, instance_not_found: 404, collection_not_found: 404, document_not_found: 404, cursor_expired: 410, revision_conflict: 409, quota_exceeded: 409, document_too_large: 413, invalid_project_id: 422, invalid_collection_name: 422, invalid_document_id: 422, invalid_document_data: 422, invalid_revision: 422, rate_limited: 429, }; type FetchImplementation = typeof fetch; type AccessTokenGetter = typeof getValidWorksSquareAccessToken; export type DataServiceOperations = { configure(input: { collections: string[] }, trustedProjectPath?: string): Promise>; inspect(trustedProjectPath?: string): Promise>; listProjects(): Promise>; getDocument(input: { collection: string; document_id: string }, trustedProjectPath?: string): Promise>; listDocuments(input: { collection: string; limit?: number; cursor?: string }, trustedProjectPath?: string): Promise>; putDocument(input: DataServicePutDocumentInput, trustedProjectPath?: string): Promise>; deleteDocument(input: DataServiceDocumentTargetInput & { confirmed: true }, trustedProjectPath?: string): Promise>; removeCollection(input: DataServiceCollectionTargetInput, trustedProjectPath?: string): Promise>; reset(input: DataServiceConfirmationInput, trustedProjectPath?: string): Promise>; removeProject(input: DataServiceConfirmationInput, trustedProjectPath?: string): Promise>; }; export type DataServiceCloudClientDependencies = { fetchImpl?: FetchImplementation; getAccessToken?: AccessTokenGetter; apiBaseUrl?: string; }; type RequestSpec = { method: 'GET' | 'PUT' | 'POST' | 'DELETE'; path: string; body?: unknown; ifRevision?: number; expectedStatus: number | readonly number[]; project(payload: unknown): T; }; class InvalidRemoteResponseError extends Error {} class OversizedRemoteResponseError extends Error {} function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function boundedString(value: unknown, maximum: number): string | null { if (typeof value !== 'string') return null; return value && value.length <= maximum ? value : null; } function nonNegativeInteger(value: unknown): number | null { return Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : null; } function positiveInteger(value: unknown): number | null { return Number.isSafeInteger(value) && (value as number) > 0 ? value as number : null; } function projectUuid(value: unknown): string { const uuid = boundedString(value, 36); if (!uuid || !PROJECT_ID_PATTERN.test(uuid)) throw new InvalidRemoteResponseError(); return uuid; } function projectTimestamp(value: unknown): string { const timestamp = boundedString(value, 64); if (!timestamp || !timestamp.endsWith('Z') || !Number.isFinite(Date.parse(timestamp))) { throw new InvalidRemoteResponseError(); } return timestamp; } function projectCollectionName(value: unknown): string { const name = boundedString(value, 48); if (!name || !COLLECTION_PATTERN.test(name)) throw new InvalidRemoteResponseError(); return name; } function projectDocumentId(value: unknown): string { const id = boundedString(value, 128); if (!id || id === '.' || id === '..' || !DOCUMENT_ID_PATTERN.test(id)) { throw new InvalidRemoteResponseError(); } return id; } function projectData(value: unknown): Record { if (!isRecord(value)) throw new InvalidRemoteResponseError(); return value; } function projectUsage(value: unknown): { document_count: number; total_bytes: number } { if (!isRecord(value)) throw new InvalidRemoteResponseError(); const documentCount = nonNegativeInteger(value.document_count); const totalBytes = nonNegativeInteger(value.total_bytes); if (documentCount === null || totalBytes === null) throw new InvalidRemoteResponseError(); return { document_count: documentCount, total_bytes: totalBytes }; } function projectLimits(value: unknown) { if (!isRecord(value)) throw new InvalidRemoteResponseError(); const fields = [ 'max_collections', 'max_documents', 'max_total_bytes', 'max_document_bytes', 'list_default_limit', 'list_max_limit', 'list_max_data_bytes', 'mutations_per_minute', ] as const; const projected = {} as Record<(typeof fields)[number], number>; for (const field of fields) { const item = positiveInteger(value[field]); if (item === null) throw new InvalidRemoteResponseError(); projected[field] = item; } return projected; } function projectCollection(value: unknown) { if (!isRecord(value)) throw new InvalidRemoteResponseError(); const documentCount = nonNegativeInteger(value.document_count); const totalBytes = nonNegativeInteger(value.total_bytes); if (documentCount === null || totalBytes === null) throw new InvalidRemoteResponseError(); return { name: projectCollectionName(value.name), document_count: documentCount, total_bytes: totalBytes, created_at: projectTimestamp(value.created_at), }; } function projectInstance(value: unknown): DataServiceInstanceState { if (!isRecord(value) || !Array.isArray(value.collections) || value.collections.length > 20) { throw new InvalidRemoteResponseError(); } const projectId = projectUuid(value.project_id); const instanceId = projectUuid(value.instance_id); const collections = value.collections.map(projectCollection); if (new Set(collections.map(({ name }) => name)).size !== collections.length) { throw new InvalidRemoteResponseError(); } const usage = projectUsage(value.usage); return { instance_id: instanceId, project_id: projectId, collections, usage, limits: projectLimits(value.limits), created_at: projectTimestamp(value.created_at), updated_at: projectTimestamp(value.updated_at), }; } function projectInstanceForProject(projectId: string) { return (value: unknown): DataServiceInstanceState => { const instance = projectInstance(value); if (instance.project_id !== projectId) throw new InvalidRemoteResponseError(); return instance; }; } function projectInstanceList(value: unknown): DataServiceInstanceList { if (!isRecord(value) || !Array.isArray(value.items) || value.items.length > 20) { throw new InvalidRemoteResponseError(); } const total = nonNegativeInteger(value.total); const instanceLimit = positiveInteger(value.instance_limit); if (total === null || instanceLimit === null) throw new InvalidRemoteResponseError(); const items = value.items.map(projectInstance); if (new Set(items.map(({ project_id }) => project_id)).size !== items.length || total !== items.length) { throw new InvalidRemoteResponseError(); } return { items, total, instance_limit: instanceLimit }; } function projectCollectionRemoval(value: unknown): DataServiceCollectionRemoval { if (!isRecord(value) || typeof value.removed !== 'boolean') throw new InvalidRemoteResponseError(); return { removed: value.removed, usage: projectUsage(value.usage) }; } function projectInstanceRemoval(value: unknown): DataServiceInstanceRemoval { if (!isRecord(value) || typeof value.removed !== 'boolean') throw new InvalidRemoteResponseError(); return { removed: value.removed }; } function projectDocument(value: unknown): DataServiceDocument { if (!isRecord(value)) throw new InvalidRemoteResponseError(); const revision = positiveInteger(value.revision); if (revision === null) throw new InvalidRemoteResponseError(); return { id: projectDocumentId(value.id), data: projectData(value.data), revision, created_at: projectTimestamp(value.created_at), updated_at: projectTimestamp(value.updated_at), }; } function projectDocumentList(value: unknown): DataServiceDocumentList { if (!isRecord(value) || !Array.isArray(value.items)) throw new InvalidRemoteResponseError(); if (value.items.length > 100) throw new InvalidRemoteResponseError(); const limit = positiveInteger(value.limit); if (limit === null || limit > 100) throw new InvalidRemoteResponseError(); if (value.next_cursor !== null && boundedString(value.next_cursor, MAX_CURSOR_LENGTH) === null) { throw new InvalidRemoteResponseError(); } const items = value.items.map(projectDocument); if (new Set(items.map(({ id }) => id)).size !== items.length) { throw new InvalidRemoteResponseError(); } return { items, next_cursor: value.next_cursor === null ? null : value.next_cursor as string, limit, }; } function resultSuccess(status: number, data: T | null): DataServiceHostResult { return { success: true, status, code: null, error: null, retryable: false, data, }; } function resultFailure( status: number, code: string, error: string, retryable: boolean, extra: Pick, 'retry_after_seconds' | 'context'> = {}, ): DataServiceHostResult { return { success: false, status, code, error, retryable, ...(extra.retry_after_seconds === undefined ? {} : { retry_after_seconds: extra.retry_after_seconds }), ...(extra.context === undefined ? {} : { context: extra.context }), data: null, }; } function authRequired(): DataServiceHostResult { return resultFailure(401, 'authentication_required', 'Works Square sign-in is required', false); } function unavailable(): DataServiceHostResult { return resultFailure(503, 'data_service_unavailable', 'Data Service is temporarily unavailable', true); } function invalidResponse(): DataServiceHostResult { return resultFailure(502, 'upstream_invalid_response', 'Data Service returned an invalid response', false); } function invalidRequest(): DataServiceHostResult { return resultFailure(422, 'invalid_request', 'Data Service request is invalid', false); } function confirmationRequired(): DataServiceHostResult { return resultFailure( 400, 'confirmation_required', 'Explicit confirmation is required for this Data Service operation', false, ); } function errorMessageForCode(code: string): string { switch (code) { case 'invalid_cursor': return 'Data Service cursor is invalid'; case 'cursor_expired': return 'Data Service cursor has expired'; case 'instance_not_found': return 'Data Service project instance was not found'; case 'collection_not_found': return 'Data Service collection was not found'; case 'document_not_found': return 'Data Service document was not found'; case 'revision_conflict': return 'Data Service document revision conflicts'; case 'quota_exceeded': return 'Data Service quota exceeded'; case 'document_too_large': return 'Data Service document is too large'; case 'invalid_revision': return 'Data Service document revision is invalid'; case 'invalid_project_id': return 'Data Service project ID is invalid'; case 'invalid_collection_name': return 'Data Service collection is invalid'; case 'invalid_document_id': return 'Data Service document ID is invalid'; case 'invalid_document_data': return 'Data Service document data is invalid'; case 'rate_limited': return 'Data Service request rate limit exceeded'; case 'data_service_unavailable': return 'Data Service is temporarily unavailable'; default: return 'Data Service request was rejected'; } } function projectContext(value: unknown): DataServiceErrorContext | undefined { if (!isRecord(value)) return undefined; const context: DataServiceErrorContext = {}; for (const [key, item] of Object.entries(value)) { if (!CONTEXT_KEYS.has(key)) continue; if (key === 'resource') { const resource = boundedString(item, 64); if (!resource || !CONTEXT_RESOURCES.has(resource)) throw new InvalidRemoteResponseError(); context[key] = resource; } else if (Number.isSafeInteger(item) && (item as number) >= 0) { context[key] = item as number; } else { throw new InvalidRemoteResponseError(); } } return Object.keys(context).length > 0 ? context : undefined; } function retryAfterSeconds(response: Response): number | undefined { const raw = response.headers.get('retry-after')?.trim(); if (!raw || !/^\d+$/.test(raw)) return undefined; const seconds = Number(raw); return Number.isSafeInteger(seconds) && seconds <= MAX_RETRY_AFTER_SECONDS ? seconds : undefined; } function projectError(payload: unknown, response: Response): DataServiceHostResult { if (!isRecord(payload) || !isRecord(payload.detail)) return invalidResponse(); const detail = payload.detail; const rawCode = boundedString(detail.code, 64); if (!rawCode || !KNOWN_ERROR_CODES.has(rawCode)) return invalidResponse(); if (ERROR_STATUS_BY_CODE[rawCode] !== response.status) return invalidResponse(); if (detail.retryable !== undefined && typeof detail.retryable !== 'boolean') return invalidResponse(); if (detail.context !== undefined && !isRecord(detail.context)) return invalidResponse(); const code = rawCode; // The upstream message is intentionally not forwarded: even bounded error // text is not a safe boundary for exception details or account data. const message = errorMessageForCode(code); const retryable = typeof detail.retryable === 'boolean' ? detail.retryable : code === 'rate_limited' || code === 'data_service_unavailable'; const retryAfter = retryAfterSeconds(response); const context = projectContext(detail.context); return resultFailure( response.status >= 400 && response.status <= 599 ? response.status : 502, code, message, retryable, { ...(retryAfter === undefined ? {} : { retry_after_seconds: retryAfter }), ...(context === undefined ? {} : { context }), }, ); } async function readBoundedJson(response: Response): Promise { const declaredLength = response.headers.get('content-length'); if (declaredLength && /^\d+$/.test(declaredLength) && Number(declaredLength) > MAX_RESPONSE_BYTES) { await response.body?.cancel().catch(() => undefined); throw new OversizedRemoteResponseError(); } if (!response.body) { const text = await response.text(); if (Buffer.byteLength(text, 'utf8') > MAX_RESPONSE_BYTES) throw new OversizedRemoteResponseError(); if (!text.trim()) return null; try { return JSON.parse(text) as unknown; } catch { throw new InvalidRemoteResponseError(); } } const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let size = 0; try { while (true) { const { done, value } = await reader.read(); if (done) break; if (!value) continue; size += value.byteLength; if (size > MAX_RESPONSE_BYTES) { await reader.cancel().catch(() => undefined); throw new OversizedRemoteResponseError(); } chunks.push(value); } } finally { reader.releaseLock(); } const text = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString('utf8'); if (!text.trim()) return null; try { return JSON.parse(text) as unknown; } catch { throw new InvalidRemoteResponseError(); } } function assertProjectId(projectId: string): boolean { return typeof projectId === 'string' && PROJECT_ID_PATTERN.test(projectId); } function assertCollection(collection: string): boolean { return typeof collection === 'string' && COLLECTION_PATTERN.test(collection); } function assertDocumentId(documentId: string): boolean { return typeof documentId === 'string' && documentId !== '.' && documentId !== '..' && DOCUMENT_ID_PATTERN.test(documentId); } function assertRevision(value: number | undefined): boolean { return value === undefined || (Number.isSafeInteger(value) && value > 0); } function requestBytes(body: unknown): string | null { try { const encoded = JSON.stringify(body); if (typeof encoded !== 'string') return null; if (Buffer.byteLength(encoded, 'utf8') > MAX_REQUEST_BYTES) return null; return encoded; } catch { return null; } } export class DataServiceCloudClient { private readonly fetchImpl: FetchImplementation; private readonly getAccessToken: AccessTokenGetter; private readonly apiBaseUrl: string; constructor(dependencies: DataServiceCloudClientDependencies = {}) { this.fetchImpl = dependencies.fetchImpl ?? proxyAwareFetch; this.getAccessToken = dependencies.getAccessToken ?? getValidWorksSquareAccessToken; this.apiBaseUrl = (dependencies.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/, ''); } listProjects(): Promise> { return this.request({ method: 'GET', path: '/api/data-service/v1/projects', expectedStatus: 200, project: projectInstanceList, }); } configure(projectId: string, collections: string[]): Promise> { if (!assertProjectId(projectId) || !Array.isArray(collections) || collections.length > 20 || collections.some((name) => !assertCollection(name))) return Promise.resolve(invalidRequest()); return this.request({ method: 'PUT', path: `/api/data-service/v1/projects/${encodeURIComponent(projectId)}`, body: { collections }, expectedStatus: 200, project: projectInstanceForProject(projectId), }); } inspect(projectId: string): Promise> { if (!assertProjectId(projectId)) return Promise.resolve(invalidRequest()); return this.request({ method: 'GET', path: `/api/data-service/v1/projects/${encodeURIComponent(projectId)}`, expectedStatus: 200, project: projectInstanceForProject(projectId), }); } removeCollection(projectId: string, collection: string): Promise> { if (!assertProjectId(projectId) || !assertCollection(collection)) return Promise.resolve(invalidRequest()); return this.request({ method: 'DELETE', path: `/api/data-service/v1/projects/${encodeURIComponent(projectId)}/collections/${encodeURIComponent(collection)}`, expectedStatus: 200, project: projectCollectionRemoval, }); } reset(projectId: string): Promise> { if (!assertProjectId(projectId)) return Promise.resolve(invalidRequest()); return this.request({ method: 'POST', path: `/api/data-service/v1/projects/${encodeURIComponent(projectId)}/reset`, expectedStatus: 200, project: projectInstanceForProject(projectId), }); } removeProject(projectId: string): Promise> { if (!assertProjectId(projectId)) return Promise.resolve(invalidRequest()); return this.request({ method: 'DELETE', path: `/api/data-service/v1/projects/${encodeURIComponent(projectId)}`, expectedStatus: 200, project: projectInstanceRemoval, }); } getDocument(projectId: string, collection: string, documentId: string): Promise> { if (!assertProjectId(projectId) || !assertCollection(collection) || !assertDocumentId(documentId)) { return Promise.resolve(invalidRequest()); } return this.request({ method: 'GET', path: `/api/data/v1/projects/${encodeURIComponent(projectId)}/collections/${encodeURIComponent(collection)}/documents/${encodeURIComponent(documentId)}`, expectedStatus: 200, project: projectDocument, }); } listDocuments( projectId: string, collection: string, limit?: number, cursor?: string, ): Promise> { if (!assertProjectId(projectId) || !assertCollection(collection) || (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)) || (cursor !== undefined && (typeof cursor !== 'string' || !cursor || cursor.length > MAX_CURSOR_LENGTH))) { return Promise.resolve(invalidRequest()); } const query = new URLSearchParams(); if (limit !== undefined) query.set('limit', String(limit)); if (cursor !== undefined) query.set('cursor', cursor); const suffix = query.toString() ? `?${query.toString()}` : ''; return this.request({ method: 'GET', path: `/api/data/v1/projects/${encodeURIComponent(projectId)}/collections/${encodeURIComponent(collection)}/documents${suffix}`, expectedStatus: 200, project: projectDocumentList, }); } putDocument( projectId: string, input: DataServicePutDocumentInput, ): Promise> { if (!isRecord(input)) return Promise.resolve(invalidRequest()); const encodedBody = requestBytes({ data: input.data }); if (!assertProjectId(projectId) || !assertCollection(input.collection) || !assertDocumentId(input.document_id) || !isRecord(input.data) || encodedBody === null || !assertRevision(input.if_revision)) return Promise.resolve(invalidRequest()); return this.request({ method: 'PUT', path: `/api/data/v1/projects/${encodeURIComponent(projectId)}/collections/${encodeURIComponent(input.collection)}/documents/${encodeURIComponent(input.document_id)}`, body: { data: input.data }, ifRevision: input.if_revision, expectedStatus: 200, project: projectDocument, }); } deleteDocument( projectId: string, input: DataServiceDocumentTargetInput, ): Promise> { if (!isRecord(input)) return Promise.resolve(invalidRequest()); if (!assertProjectId(projectId) || !assertCollection(input.collection) || !assertDocumentId(input.document_id) || !assertRevision(input.if_revision)) { return Promise.resolve(invalidRequest()); } return this.request({ method: 'DELETE', path: `/api/data/v1/projects/${encodeURIComponent(projectId)}/collections/${encodeURIComponent(input.collection)}/documents/${encodeURIComponent(input.document_id)}`, ifRevision: input.if_revision, expectedStatus: 204, project: () => null, }); } private async request(spec: RequestSpec): Promise> { const body = spec.body === undefined ? undefined : requestBytes(spec.body); if (spec.body !== undefined && body === null) return invalidRequest(); let token: string | null; try { token = await this.getAccessToken({ fetchImpl: this.fetchImpl }); } catch { return authRequired(); } if (!token) return authRequired(); const send = async (accessToken: string): Promise => await this.fetchImpl( `${this.apiBaseUrl}${spec.path}`, { method: spec.method, headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), ...(spec.ifRevision === undefined ? {} : { 'If-Match': `"${spec.ifRevision}"` }), }, ...(body === undefined ? {} : { body }), redirect: 'manual', }, ); let response: Response; try { response = await send(token); } catch { return unavailable(); } if (response.status === 401) { await response.body?.cancel().catch(() => undefined); let refreshed: string | null; try { refreshed = await this.getAccessToken({ fetchImpl: this.fetchImpl, forceRefresh: true }); } catch { refreshed = null; } if (!refreshed) return authRequired(); try { response = await send(refreshed); } catch { return unavailable(); } if (response.status === 401) { await response.body?.cancel().catch(() => undefined); return authRequired(); } } if (!response.ok) { if (response.status >= 500) { await response.body?.cancel().catch(() => undefined); return unavailable(); } let payload: unknown; try { payload = await readBoundedJson(response); } catch { await response.body?.cancel().catch(() => undefined); return invalidResponse(); } return projectError(payload, response); } const expected = Array.isArray(spec.expectedStatus) ? spec.expectedStatus : [spec.expectedStatus]; if (!expected.includes(response.status)) { await response.body?.cancel().catch(() => undefined); return invalidResponse(); } if (response.status === 204) return resultSuccess(response.status, null); let payload: unknown; try { payload = await readBoundedJson(response); return resultSuccess(response.status, spec.project(payload)); } catch { return invalidResponse(); } } } function projectLocalProjectError(error: unknown): DataServiceHostResult { if (!(error instanceof CodingProjectServiceError)) return unavailable(); switch (error.code) { case 'CODING_PROJECT_IDENTITY_REQUIRED': return resultFailure(409, 'project_identity_required', 'A durable coding project identity is required', false); case 'CODING_ACTIVE_PROJECT_REQUIRED': return resultFailure(409, 'active_project_required', 'An active coding project is required', false); case 'CODING_ACTIVE_PROJECT_PATH_MISMATCH': return resultFailure(409, 'active_project_path_mismatch', 'The active coding project path is invalid', false); case 'CODING_ACTIVE_PROJECT_INVALID': case 'CODING_PROJECT_CONFIG_INVALID': return resultFailure(409, 'active_project_invalid', 'The active coding project is unavailable', false); default: return error.status >= 500 ? unavailable() : resultFailure(error.status, 'active_project_invalid', 'The active coding project is unavailable', false); } } export type DataServiceOperationsDependencies = { projects: Pick; client?: DataServiceCloudClient; }; export function createDataServiceOperations( dependencies: DataServiceOperationsDependencies, ): DataServiceOperations { const client = dependencies.client ?? new DataServiceCloudClient(); async function withActive( trustedProjectPath: string | undefined, operation: (projectId: string) => Promise>, ): Promise> { try { const active = await dependencies.projects.requireActiveRealProjectWithIdentity(trustedProjectPath); return await operation(active.projectId); } catch (error) { return projectLocalProjectError(error); } } return { listProjects: () => client.listProjects(), configure: ({ collections }, trustedProjectPath) => withActive(trustedProjectPath, (projectId) => client.configure(projectId, collections)), inspect: (trustedProjectPath) => withActive(trustedProjectPath, (projectId) => client.inspect(projectId)), getDocument: ({ collection, document_id }, trustedProjectPath) => withActive(trustedProjectPath, (projectId) => client.getDocument(projectId, collection, document_id), ), listDocuments: ({ collection, limit, cursor }, trustedProjectPath) => withActive(trustedProjectPath, (projectId) => client.listDocuments(projectId, collection, limit, cursor), ), putDocument: (input, trustedProjectPath) => withActive(trustedProjectPath, (projectId) => client.putDocument(projectId, input)), deleteDocument: ({ collection, document_id, if_revision, confirmed }, trustedProjectPath) => confirmed === true ? withActive(trustedProjectPath, (projectId) => client.deleteDocument(projectId, { collection, document_id, if_revision })) : Promise.resolve(confirmationRequired()), removeCollection: ({ collection, confirmed }, trustedProjectPath) => confirmed === true ? withActive(trustedProjectPath, (projectId) => client.removeCollection(projectId, collection)) : Promise.resolve(confirmationRequired()), reset: ({ confirmed }, trustedProjectPath) => confirmed === true ? withActive(trustedProjectPath, (projectId) => client.reset(projectId)) : Promise.resolve(confirmationRequired()), removeProject: ({ confirmed }, trustedProjectPath) => confirmed === true ? withActive(trustedProjectPath, (projectId) => client.removeProject(projectId)) : Promise.resolve(confirmationRequired()), }; }