Skip to main content
Glama
outcomes.ts13.8 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { OperationOutcome, OperationOutcomeIssue } from '@medplum/fhirtypes'; import type { Constraint } from './typeschema/types'; const OK_ID = 'ok'; const CREATED_ID = 'created'; const GONE_ID = 'gone'; const NOT_MODIFIED_ID = 'not-modified'; const FOUND_ID = 'found'; const NOT_FOUND_ID = 'not-found'; const CONFLICT_ID = 'conflict'; const UNAUTHORIZED_ID = 'unauthorized'; const FORBIDDEN_ID = 'forbidden'; const PRECONDITION_FAILED_ID = 'precondition-failed'; const UNSUPPORTED_MEDIA_TYPE_ID = 'unsupported-media-type'; const MULTIPLE_MATCHES_ID = 'multiple-matches'; const TOO_MANY_REQUESTS_ID = 'too-many-requests'; const ACCEPTED_ID = 'accepted'; const SERVER_TIMEOUT_ID = 'server-timeout'; const BUSINESS_RULE = 'business-rule'; export const allOk: OperationOutcome = { resourceType: 'OperationOutcome', id: OK_ID, issue: [ { severity: 'information', code: 'informational', details: { text: 'All OK', }, }, ], }; export const created: OperationOutcome = { resourceType: 'OperationOutcome', id: CREATED_ID, issue: [ { severity: 'information', code: 'informational', details: { text: 'Created', }, }, ], }; export const notModified: OperationOutcome = { resourceType: 'OperationOutcome', id: NOT_MODIFIED_ID, issue: [ { severity: 'information', code: 'informational', details: { text: 'Not Modified', }, }, ], }; export const notFound: OperationOutcome = { resourceType: 'OperationOutcome', id: NOT_FOUND_ID, issue: [ { severity: 'error', code: 'not-found', details: { text: 'Not found', }, }, ], }; export const unauthorized: OperationOutcome = { resourceType: 'OperationOutcome', id: UNAUTHORIZED_ID, issue: [ { severity: 'error', code: 'login', details: { text: 'Unauthorized', }, }, ], }; export const unauthorizedTokenExpired: OperationOutcome = { ...unauthorized, issue: [ ...unauthorized.issue, { severity: 'error', code: 'expired', details: { text: 'Token expired', }, }, ], }; export const unauthorizedTokenAudience: OperationOutcome = { ...unauthorized, issue: [ ...unauthorized.issue, { severity: 'error', code: 'invalid', details: { text: 'Token not issued for this audience', }, }, ], }; export const forbidden: OperationOutcome = { resourceType: 'OperationOutcome', id: FORBIDDEN_ID, issue: [ { severity: 'error', code: 'forbidden', details: { text: 'Forbidden', }, }, ], }; export const gone: OperationOutcome = { resourceType: 'OperationOutcome', id: GONE_ID, issue: [ { severity: 'error', code: 'deleted', details: { text: 'Gone', }, }, ], }; export const preconditionFailed: OperationOutcome = { resourceType: 'OperationOutcome', id: PRECONDITION_FAILED_ID, issue: [ { severity: 'error', code: 'processing', details: { text: 'Precondition Failed', }, }, ], }; export const unsupportedMediaType: OperationOutcome = { resourceType: 'OperationOutcome', id: UNSUPPORTED_MEDIA_TYPE_ID, issue: [ { severity: 'error', code: 'not-supported', details: { text: 'Unsupported media type', }, }, ], }; export const multipleMatches: OperationOutcome = { resourceType: 'OperationOutcome', id: MULTIPLE_MATCHES_ID, issue: [ { severity: 'error', code: 'multiple-matches', details: { text: 'Multiple resources found matching condition', }, }, ], }; export const tooManyRequests: OperationOutcome = { resourceType: 'OperationOutcome', id: TOO_MANY_REQUESTS_ID, issue: [ { severity: 'error', code: 'throttled', details: { text: 'Too Many Requests', }, }, ], }; export function accepted(location: string): OperationOutcome { return { resourceType: 'OperationOutcome', id: ACCEPTED_ID, issue: [ { severity: 'information', code: 'informational', details: { text: 'Accepted', }, diagnostics: location, }, ], }; } export function badRequest(details: string, expression?: string): OperationOutcome { return { resourceType: 'OperationOutcome', issue: [ { severity: 'error', code: 'invalid', details: { text: details, }, ...(expression ? { expression: [expression] } : undefined), }, ], }; } export function conflict(details: string, code?: string): OperationOutcome { return { resourceType: 'OperationOutcome', id: CONFLICT_ID, issue: [ { severity: 'error', code: 'conflict', details: { coding: code ? [{ code }] : undefined, text: details, }, }, ], }; } export function validationError(details: string): OperationOutcome { return { resourceType: 'OperationOutcome', issue: [ { severity: 'error', code: 'structure', details: { text: details, }, }, ], }; } export function serverError(err: Error): OperationOutcome { return { resourceType: 'OperationOutcome', issue: [ { severity: 'error', code: 'exception', details: { text: 'Internal server error', }, diagnostics: err.toString(), }, ], }; } export function serverTimeout(msg?: string): OperationOutcome { return { resourceType: 'OperationOutcome', id: SERVER_TIMEOUT_ID, issue: [ { severity: 'error', code: 'timeout', details: { text: msg ?? 'Server timeout', }, }, ], }; } export function redirect(url: URL): OperationOutcome { const urlStr = url.toString(); return { resourceType: 'OperationOutcome', id: FOUND_ID, issue: [ { severity: 'information', code: 'informational', details: { coding: [{ system: 'urn:ietf:rfc:3986', code: urlStr }], text: 'Redirect to ' + urlStr, }, }, ], }; } export function businessRule(key: string, message: string): OperationOutcome { return { resourceType: 'OperationOutcome', id: BUSINESS_RULE, issue: [ { severity: 'error', code: 'business-rule', details: { id: key, text: message }, }, ], }; } /** * Returns true if the input is an Error object. * This should be replaced with `Error.isError` when it is more widely supported. * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/isError * @param value - The candidate value. * @returns True if the input is an Error object. */ export function isError(value: unknown): value is Error { // Quick type check if (!value || typeof value !== 'object') { return false; } // Fast path for same-realm errors using instanceof if (value instanceof Error) { return true; } // Handle DOMException case if (typeof DOMException !== 'undefined' && value instanceof DOMException) { return true; } // Cross-realm check using toString (most reliable method) return Object.prototype.toString.call(value) === '[object Error]'; } export function isOperationOutcome(value: unknown): value is OperationOutcome { return typeof value === 'object' && value !== null && (value as any).resourceType === 'OperationOutcome'; } export function isOk(outcome: OperationOutcome): boolean { return ( outcome.id === OK_ID || outcome.id === CREATED_ID || outcome.id === NOT_MODIFIED_ID || outcome.id === ACCEPTED_ID ); } export function isCreated(outcome: OperationOutcome): boolean { return outcome.id === CREATED_ID; } export function isAccepted(outcome: OperationOutcome): boolean { return outcome.id === ACCEPTED_ID; } export function isRedirect(outcome: OperationOutcome): boolean { return outcome.id === FOUND_ID; } export function isNotFound(outcome: OperationOutcome): boolean { return outcome.id === NOT_FOUND_ID; } export function isConflict(outcome: OperationOutcome): boolean { return outcome.id === CONFLICT_ID; } export function isGone(outcome: OperationOutcome): boolean { return outcome.id === GONE_ID; } export function isUnauthenticated(outcome: OperationOutcome): boolean { return outcome.id === UNAUTHORIZED_ID; } export function getStatus(outcome: OperationOutcome): number { switch (outcome.id) { case OK_ID: return 200; case CREATED_ID: return 201; case ACCEPTED_ID: return 202; case FOUND_ID: return 302; case NOT_MODIFIED_ID: return 304; case UNAUTHORIZED_ID: return 401; case FORBIDDEN_ID: return 403; case NOT_FOUND_ID: return 404; case CONFLICT_ID: return 409; case GONE_ID: return 410; case PRECONDITION_FAILED_ID: case MULTIPLE_MATCHES_ID: return 412; case UNSUPPORTED_MEDIA_TYPE_ID: return 415; case BUSINESS_RULE: return 422; case TOO_MANY_REQUESTS_ID: return 429; case SERVER_TIMEOUT_ID: return 504; default: return outcome.issue?.[0]?.code === 'exception' ? 500 : 400; } } /** * Asserts that the operation completed successfully and that the resource is defined. * @param outcome - The operation outcome. * @param resource - The resource that may or may not have been returned. */ export function assertOk<T>(outcome: OperationOutcome, resource: T | undefined): asserts resource is T { if (!isOk(outcome) || resource === undefined) { throw new OperationOutcomeError(outcome); } } export class OperationOutcomeError extends Error { readonly outcome: OperationOutcome; constructor(outcome: OperationOutcome, options?: ErrorOptions) { super(operationOutcomeToString(outcome), options); this.name = 'OperationOutcomeError'; this.outcome = outcome; } } /** * Normalizes an error object into an OperationOutcome. * @param error - The error value which could be a string, Error, OperationOutcome, or other unknown type. * @returns The normalized OperationOutcome. */ export function normalizeOperationOutcome(error: unknown): OperationOutcome { if (error instanceof OperationOutcomeError) { return error.outcome; } if (isOperationOutcome(error)) { return error; } return badRequest(normalizeErrorString(error)); } /** * Normalizes an error object into a displayable error string. * @param error - The error value which could be a string, Error, OperationOutcome, or other unknown type. * @returns A display string for the error. */ export function normalizeErrorString(error: unknown): string { if (!error) { return 'Unknown error'; } if (typeof error === 'string') { return error; } if (isError(error)) { return error.message; } if (isOperationOutcome(error)) { return operationOutcomeToString(error); } if (typeof error === 'object' && 'code' in error && typeof error.code === 'string') { return error.code; } return JSON.stringify(error); } /** * Returns a string represenation of the operation outcome. * @param outcome - The operation outcome. * @returns The string representation of the operation outcome. */ export function operationOutcomeToString(outcome: OperationOutcome): string { const strs = outcome.issue?.map(operationOutcomeIssueToString) ?? []; return strs.length > 0 ? strs.join('; ') : 'Unknown error'; } /** * Returns a string represenation of the operation outcome issue. * @param issue - The operation outcome issue. * @returns The string representation of the operation outcome issue. */ export function operationOutcomeIssueToString(issue: OperationOutcomeIssue): string { let issueStr; if (issue.details?.text) { if (issue.diagnostics) { issueStr = `${issue.details.text} (${issue.diagnostics})`; } else { issueStr = issue.details.text; } } else if (issue.diagnostics) { issueStr = issue.diagnostics; } else { issueStr = 'Unknown error'; } if (issue.expression?.length) { issueStr += ` (${issue.expression.join(', ')})`; } return issueStr; } export type IssueSeverity = 'error' | 'fatal' | 'warning' | 'information'; export type IssueType = 'structure' | 'invariant' | 'processing'; export function createOperationOutcomeIssue( severity: IssueSeverity, code: IssueType, message: string, path: string, data?: Record<string, any> ): OperationOutcomeIssue { const issue: OperationOutcomeIssue = { severity, code, details: { text: message, }, expression: [path], }; if (data) { issue.diagnostics = JSON.stringify(data); } return issue; } export function createStructureIssue(expression: string, details: string): OperationOutcomeIssue { return createOperationOutcomeIssue('error', 'structure', details, expression); } export function createConstraintIssue(expression: string, constraint: Constraint): OperationOutcomeIssue { return createOperationOutcomeIssue( 'error', 'invariant', `Constraint ${constraint.key} not met: ${constraint.description}`, expression, { fhirpath: constraint.expression, } ); } export function createProcessingIssue( expression: string, message: string, err: Error, data?: Record<string, any> ): OperationOutcomeIssue { return createOperationOutcomeIssue('error', 'processing', message, expression, { ...data, error: err }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server