Get create operation logic from old arch.refactor/new-arch
@@ -1,4 +1,11 @@ | |||
import {App as BaseApp, AppOperations, BaseAppState, Endpoint, Response} from '../common'; | |||
import { | |||
App as BaseApp, | |||
AppOperations, | |||
BaseAppState, | |||
Endpoint, | |||
Language, | |||
Response, | |||
} from '../common'; | |||
import {DataSource} from './data-source'; | |||
interface BackendParams<App extends BaseApp> { | |||
@@ -12,6 +19,8 @@ export interface ImplementationContext { | |||
params: Record<string, unknown>; | |||
query?: URLSearchParams; | |||
dataSource?: DataSource; | |||
language: Language; | |||
setLocation(location: string): void; | |||
} | |||
export type ImplementationFunction = (params: ImplementationContext) => Promise<Response | void>; | |||
@@ -92,6 +92,7 @@ export interface Endpoint< | |||
schema: Schema; | |||
params: Set<string>; | |||
operations: Set<string>; | |||
metadata: Map<string, unknown>; | |||
can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | |||
op: OpName, | |||
value?: OpValue | |||
@@ -110,7 +111,8 @@ export interface Endpoint< | |||
operations: State['operations']; | |||
params: State['params'] extends string[] ? readonly [...State['params'], ParamName]: [ParamName]; | |||
} | |||
> | |||
>; | |||
id<IdAttr extends string = string>(id: IdAttr): this; | |||
} | |||
export type GetEndpointParams<T extends Endpoint> = T extends Endpoint<any, infer R> ? ( | |||
@@ -132,6 +134,7 @@ class EndpointInstance< | |||
readonly operations: Set<string>; | |||
readonly params: Set<string>; | |||
readonly schema: Params['schema']; | |||
readonly metadata: Map<string, unknown>; | |||
constructor(params: Params) { | |||
this.name = params.name; | |||
@@ -139,6 +142,7 @@ class EndpointInstance< | |||
this.operations = new Set<string>(); | |||
this.params = new Set<string>(); | |||
this.dataSource = params.dataSource; | |||
this.metadata = new Map<string, unknown>(); | |||
} | |||
can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | |||
@@ -159,7 +163,11 @@ class EndpointInstance< | |||
name: ParamName | |||
) { | |||
this.params.add(name); | |||
return this; | |||
} | |||
id<IdAttr extends string = string>(id: IdAttr) { | |||
this.metadata.set('idAttr', id); | |||
return this; | |||
} | |||
} | |||
@@ -15,7 +15,7 @@ export const LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS = [ | |||
'badRequest', | |||
'deleteNonExistingResource', | |||
'unableToCreateResource', | |||
'unableToBindResourceDataSource', | |||
'dataSourceMethodNotImplemented', | |||
'unableToGenerateIdFromResourceDataSource', | |||
'unableToAssignIdFromResourceDataSource', | |||
'unableToEmplaceResource', | |||
@@ -67,41 +67,41 @@ export const FALLBACK_LANGUAGE = { | |||
statusMessages: { | |||
unableToSerializeResponse: 'Unable To Serialize Response', | |||
unableToEncodeResponse: 'Unable To Encode Response', | |||
unableToBindResourceDataSource: 'Unable To Bind $RESOURCE Data Source', | |||
unableToInitializeResourceDataSource: 'Unable To Initialize $RESOURCE Data Source', | |||
unableToFetchResourceCollection: 'Unable To Fetch $RESOURCE Collection', | |||
unableToFetchResource: 'Unable To Fetch $RESOURCE', | |||
unableToDeleteResource: 'Unable To Delete $RESOURCE', | |||
dataSourceMethodNotImplemented: 'Unable To Bind Resource Data Source', | |||
unableToInitializeResourceDataSource: 'Unable To Initialize Resource Data Source', | |||
unableToFetchResourceCollection: 'Unable To Fetch Resource Collection', | |||
unableToFetchResource: 'Unable To Fetch Resource', | |||
unableToDeleteResource: 'Unable To Delete Resource', | |||
languageNotAcceptable: 'Language Not Acceptable', | |||
characterSetNotAcceptable: 'Character Set Not Acceptable', | |||
unableToDeserializeResource: 'Unable To Deserialize $RESOURCE', | |||
unableToDecodeResource: 'Unable To Decode $RESOURCE', | |||
unableToDeserializeResource: 'Unable To Deserialize Resource', | |||
unableToDecodeResource: 'Unable To Decode Resource', | |||
mediaTypeNotAcceptable: 'Media Type Not Acceptable', | |||
methodNotAllowed: 'Method Not Allowed', | |||
urlNotFound: 'URL Not Found', | |||
badRequest: 'Bad Request', | |||
ok: 'OK', | |||
provideOptions: 'Provide Options', | |||
resourceCollectionFetched: '$RESOURCE Collection Fetched', | |||
resourceCollectionQueried: '$RESOURCE Collection Queried', | |||
resourceFetched: '$RESOURCE Fetched', | |||
resourceNotFound: '$RESOURCE Not Found', | |||
deleteNonExistingResource: 'Delete Non-Existing $RESOURCE', | |||
resourceDeleted: '$RESOURCE Deleted', | |||
resourceCollectionFetched: 'Resource Collection Fetched', | |||
resourceCollectionQueried: 'Resource Collection Queried', | |||
resourceFetched: 'Resource Fetched', | |||
resourceNotFound: 'Resource Not Found', | |||
deleteNonExistingResource: 'Delete Non-Existing Resource', | |||
resourceDeleted: 'Resource Deleted', | |||
unableToDeserializeRequest: 'Unable To Deserialize Request', | |||
patchNonExistingResource: 'Patch Non-Existing $RESOURCE', | |||
unableToPatchResource: 'Unable To Patch $RESOURCE', | |||
invalidResourcePatch: 'Invalid $RESOURCE Patch', | |||
invalidResourcePatchType: 'Invalid $RESOURCE Patch Type', | |||
invalidResource: 'Invalid $RESOURCE', | |||
resourcePatched: '$RESOURCE Patched', | |||
resourceCreated: '$RESOURCE Created', | |||
resourceReplaced: '$RESOURCE Replaced', | |||
unableToGenerateIdFromResourceDataSource: 'Unable To Generate ID From $RESOURCE Data Source', | |||
unableToAssignIdFromResourceDataSource: 'Unable To Assign ID From $RESOURCE Data Source', | |||
unableToEmplaceResource: 'Unable To Emplace $RESOURCE', | |||
resourceIdNotGiven: '$RESOURCE ID Not Given', | |||
unableToCreateResource: 'Unable To Create $RESOURCE', | |||
patchNonExistingResource: 'Patch Non-Existing Resource', | |||
unableToPatchResource: 'Unable To Patch Resource', | |||
invalidResourcePatch: 'Invalid Resource Patch', | |||
invalidResourcePatchType: 'Invalid Resource Patch Type', | |||
invalidResource: 'Invalid Resource', | |||
resourcePatched: 'Resource Patched', | |||
resourceCreated: 'Resource Created', | |||
resourceReplaced: 'Resource Replaced', | |||
unableToGenerateIdFromResourceDataSource: 'Unable To Generate ID From Resource Data Source', | |||
unableToAssignIdFromResourceDataSource: 'Unable To Assign ID From Resource Data Source', | |||
unableToEmplaceResource: 'Unable To Emplace Resource', | |||
resourceIdNotGiven: 'Resource ID Not Given', | |||
unableToCreateResource: 'Unable To Create Resource', | |||
notImplemented: 'Not Implemented', | |||
internalServerError: 'Internal Server Error', | |||
}, | |||
@@ -211,7 +211,7 @@ export const FALLBACK_LANGUAGE = { | |||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | |||
], | |||
], | |||
unableToBindResourceDataSource: [ | |||
dataSourceMethodNotImplemented: [ | |||
'The resource could not be associated from the data source.', | |||
[ | |||
'Try the request again at a later time.', | |||
@@ -1 +1,2 @@ | |||
export * from './common'; | |||
export * from './parsing'; |
@@ -0,0 +1,235 @@ | |||
import { | |||
QueryAndGrouping, | |||
QueryAnyExpression, | |||
QueryOrGrouping, | |||
} from './common'; | |||
export class QueryParsingError extends Error {} | |||
export class DeserializeError extends QueryParsingError {} | |||
export class SerializeError extends QueryParsingError {} | |||
interface ProcessEntryBase { | |||
type: string; | |||
} | |||
const AVAILABLE_MATCH_TYPES = [ | |||
'startsWith', | |||
'endsWith', | |||
'includes', | |||
'regexp', | |||
] as const; | |||
type MatchType = typeof AVAILABLE_MATCH_TYPES[number]; | |||
interface ProcessEntryString extends ProcessEntryBase { | |||
type: 'string'; | |||
matchType?: MatchType; | |||
caseInsensitiveMatch?: boolean; | |||
} | |||
interface ProcessEntryNumber extends ProcessEntryBase { | |||
type: 'number'; | |||
decimal?: boolean; | |||
} | |||
interface ProcessEntryBoolean extends ProcessEntryBase { | |||
type: 'boolean'; | |||
truthyStrings?: string[]; | |||
} | |||
type ProcessEntry = ProcessEntryString | ProcessEntryNumber | ProcessEntryBoolean; | |||
export const name = 'application/x-www-form-urlencoded' as const; | |||
class DeserializeInvalidFormatError extends DeserializeError {} | |||
const normalizeRhs = (lhs: string, rhs: string, processEntriesMap?: Record<string, ProcessEntry>) => { | |||
const defaultCoerceValues = { | |||
type: 'string' | |||
} as ProcessEntry; | |||
const coerceValues = processEntriesMap?.[lhs] ?? defaultCoerceValues; | |||
if (coerceValues?.type === 'number') { | |||
return { | |||
lhs, | |||
operator: '=', | |||
rhs: Number(rhs) | |||
} as QueryAnyExpression; | |||
} | |||
if (coerceValues?.type === 'boolean') { | |||
const truthyStrings = [ | |||
...(coerceValues.truthyStrings ?? []).map((s) => s.trim().toLowerCase()), | |||
'true', | |||
]; | |||
return { | |||
lhs, | |||
operator: '=', | |||
rhs: truthyStrings.includes(rhs) | |||
} as QueryAnyExpression; | |||
} | |||
if (coerceValues?.type === 'string') { | |||
switch (coerceValues?.matchType) { | |||
case 'startsWith': { | |||
return { | |||
lhs, | |||
operator: coerceValues.caseInsensitiveMatch ? 'ILIKE' : 'LIKE', | |||
rhs: `%${rhs}`, | |||
} as QueryAnyExpression; | |||
} | |||
case 'endsWith': { | |||
return { | |||
lhs, | |||
operator: coerceValues.caseInsensitiveMatch ? 'ILIKE' : 'LIKE', | |||
rhs: `${rhs}%`, | |||
} as QueryAnyExpression; | |||
} | |||
case 'includes': { | |||
return { | |||
lhs, | |||
operator: coerceValues.caseInsensitiveMatch ? 'ILIKE' : 'LIKE', | |||
rhs: `%${rhs}%`, | |||
} as QueryAnyExpression; | |||
} | |||
case 'regexp': { | |||
return { | |||
lhs, | |||
operator: 'REGEXP', | |||
rhs: new RegExp(rhs, coerceValues.caseInsensitiveMatch ? 'i' : ''), | |||
} as QueryAnyExpression; | |||
} | |||
default: | |||
break; | |||
} | |||
return { | |||
lhs, | |||
operator: '=', | |||
rhs, | |||
} as QueryAnyExpression; | |||
} | |||
const unknownCoerceValues = coerceValues as unknown as Record<string, string>; | |||
throw new DeserializeInvalidFormatError(`Invalid coercion type: ${unknownCoerceValues.type}`); | |||
// this will be sent to the data source, e.g., the SQL query | |||
// we can also make this function act as a "sanitizer" | |||
} | |||
interface DeserializeOptions { | |||
processEntries?: Record<string, ProcessEntry>; | |||
} | |||
const doesGroupHaveExpression = (ex2: QueryAnyExpression, key: string) => { | |||
if ('operator' in ex2) { | |||
return ex2.lhs === key; | |||
} | |||
if ('name' in ex2) { | |||
return ex2.name === key; | |||
} | |||
return false; | |||
}; | |||
export const fromUrlSearchParams = (q: URLSearchParams, options = {} as DeserializeOptions) => ( | |||
Array.from(q.entries()).reduce( | |||
(queries, [key, value]) => { | |||
const defaultOr = { | |||
type: 'or' as const, | |||
expressions: [], | |||
} as QueryOrGrouping; | |||
const existingOr = queries.expressions.find((ex) => ( | |||
ex.expressions.some((ex2) => doesGroupHaveExpression(ex2, key)) | |||
)) ?? defaultOr; | |||
const existingLhs = existingOr.expressions.find((ex) => doesGroupHaveExpression(ex, key)); | |||
const newExpression = normalizeRhs(key, value, options.processEntries); | |||
if (typeof existingLhs === 'undefined') { | |||
return { | |||
...queries, | |||
expressions: [ | |||
...queries.expressions, | |||
{ | |||
type: 'or' as const, | |||
expressions: [ | |||
newExpression, | |||
], | |||
}, | |||
], | |||
}; | |||
} | |||
return { | |||
...queries, | |||
expressions: queries.expressions.map((ex) => ( | |||
ex.expressions.some((ex2) => !doesGroupHaveExpression(ex2, key)) | |||
? ex | |||
: { | |||
...existingOr, | |||
expressions: [ | |||
...(existingOr.expressions ?? []), | |||
newExpression, | |||
], | |||
} | |||
)), | |||
}; | |||
}, | |||
{ | |||
type: 'and' as const, | |||
expressions: [], | |||
} as QueryAndGrouping | |||
) | |||
); | |||
class SerializeInvalidExpressionError extends SerializeError {} | |||
const serializeExpression = (ex2: QueryAnyExpression) => { | |||
if ('name' in ex2) { | |||
return [ex2.name, `(${ex2.args.map((s) => s.toString()).join(',')})`]; | |||
} | |||
if (ex2.rhs instanceof RegExp) { | |||
if (ex2.operator !== 'REGEXP') { | |||
throw new SerializeInvalidExpressionError(`Invalid rhs given for operator: ${ex2.lhs} ${ex2.operator} <rhs>`); | |||
} | |||
return [ex2.lhs, ex2.rhs.toString()]; | |||
} | |||
switch (typeof ex2.rhs) { | |||
case 'string': { | |||
switch (ex2.operator) { | |||
case 'ILIKE': | |||
case 'LIKE': | |||
return [ex2.lhs, ex2.rhs.replace(/^%+/, '').replace(/%+$/, '')]; | |||
case '=': | |||
return [ex2.lhs, ex2.rhs]; | |||
default: | |||
break; | |||
} | |||
throw new SerializeInvalidExpressionError(`Invalid operator given for lhs: ${ex2.lhs} <op> ${ex2.rhs}`); | |||
} | |||
case 'number': { | |||
return [ex2.lhs, ex2.rhs.toString()]; | |||
} | |||
case 'boolean': { | |||
return [ex2.lhs, ex2.rhs ? 'true' : 'false']; | |||
} | |||
default: | |||
break; | |||
} | |||
throw new SerializeInvalidExpressionError(`Unknown type for rhs: ${ex2.lhs} ${ex2.operator} <rhs>`); | |||
}; | |||
export const toUrlSearchParams = (q: QueryAndGrouping) => ( | |||
new URLSearchParams( | |||
q.expressions.flatMap((ex) => ( | |||
ex.expressions.map((ex2) => serializeExpression(ex2)) | |||
)) as [string, string][] | |||
) | |||
); |
@@ -170,14 +170,36 @@ class ServerInstance<Backend extends BaseBackend> implements Server<Backend> { | |||
const bodyDecoded = decoder(bodyBuffer, requestBodyParams); | |||
body = requestBodyMediaType.deserialize(bodyDecoded, requestBodyParams); | |||
} | |||
const isCqrs = false; | |||
let hasSentResponse = false; | |||
const responseSpec = await implementation({ | |||
endpoint, | |||
body, | |||
params: endpointParams ?? {}, | |||
query: typeof search !== 'undefined' ? new URLSearchParams(search) : undefined, | |||
dataSource: this.backend.dataSource, | |||
language: language ?? fallbackLanguage, | |||
setLocation: (location: string) => { | |||
// TODO set base path | |||
responseHeaders['Location'] = `${location}`; | |||
if (!isCqrs) { | |||
return; | |||
} | |||
res.writeHead(statusCodes.HTTP_STATUS_SEE_OTHER, { | |||
Location: location, | |||
}); | |||
res.end(); | |||
hasSentResponse = true; | |||
}, | |||
}); | |||
if (hasSentResponse) { | |||
return; | |||
} | |||
if (typeof responseSpec === 'undefined') { | |||
res.writeHead(statusCodes.HTTP_STATUS_UNPROCESSABLE_ENTITY, errorHeaders); | |||
res.end(); | |||
@@ -36,6 +36,8 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||
} | |||
async connect(params: ServiceParams): Promise<ClientConnection> { | |||
// we should be explicit with connection params in order not to give a false impression to the user they are "connected" | |||
// to the target server | |||
const connection = { | |||
host: params.host ?? DEFAULT_HOST, | |||
port: params.port ?? DEFAULT_PORT, | |||
@@ -11,6 +11,7 @@ import * as deleteOperation from './implementation/delete'; | |||
interface AddResourceRecipeParams { | |||
endpointName: string; | |||
dataSource?: DataSource; | |||
// TODO specify the available operations to implement | |||
} | |||
export const addResourceRecipe = (params: AddResourceRecipeParams): Recipe => (a) => { | |||
@@ -1,5 +1,10 @@ | |||
import {ImplementationFunction} from '@modal-sh/yasumi/backend'; | |||
import {operation as defineOperation} from '@modal-sh/yasumi'; | |||
import {DataSource, ImplementationFunction} from '@modal-sh/yasumi/backend'; | |||
import {operation as defineOperation, validation as v} from '@modal-sh/yasumi'; | |||
import { | |||
DataSourceMethodNotImplementedResponseError, IncompleteEndpointMetadataResponseError, | |||
ResourceCreatedResponse, | |||
UnableToAssignIdFromDataSourceResponseError, UnableToCreateResourceResponseError, | |||
} from '../response'; | |||
export const name = 'create' as const; | |||
@@ -11,5 +16,74 @@ export const operation = defineOperation({ | |||
}); | |||
export const implementation: ImplementationFunction = async (ctx) => { | |||
const { setLocation, language, endpoint, dataSource, body } = ctx; | |||
const effectiveDataSource: DataSource = endpoint.dataSource ?? dataSource ?? {} as DataSource; | |||
const { newId, create, getTotalCount } = effectiveDataSource; | |||
const idAttr = endpoint.metadata.get('idAttr'); // todo use metadata | |||
if (typeof idAttr !== 'string') { | |||
throw new IncompleteEndpointMetadataResponseError( | |||
// TODO get the status message | |||
); | |||
} | |||
if (typeof newId === 'undefined') { | |||
throw new DataSourceMethodNotImplementedResponseError(language.statusMessages.dataSourceMethodNotImplemented); | |||
} | |||
if (typeof create === 'undefined') { | |||
throw new DataSourceMethodNotImplementedResponseError(language.statusMessages.dataSourceMethodNotImplemented); | |||
} | |||
let resourceNewId; | |||
let params: v.Output<typeof endpoint.schema>; | |||
try { | |||
resourceNewId = await newId(); | |||
params = { ...body as Record<string, unknown> }; | |||
params[idAttr] = resourceNewId; | |||
} catch (cause) { | |||
throw new UnableToAssignIdFromDataSourceResponseError( | |||
language.statusMessages.unableToAssignIdFromResourceDataSource, | |||
{ | |||
cause, | |||
} | |||
); | |||
} | |||
setLocation(`/${endpoint.name}/${resourceNewId}`); | |||
let newObject; | |||
let totalItemCount: number | undefined; | |||
// TODO put this in ImplementationContext argument | |||
const showTotalItemCountOnCreateItem = false; | |||
try { | |||
if ( | |||
showTotalItemCountOnCreateItem | |||
&& typeof getTotalCount === 'function' | |||
) { | |||
totalItemCount = await getTotalCount(); | |||
totalItemCount += 1; | |||
} | |||
newObject = await create(params); | |||
} catch (cause) { | |||
throw new UnableToCreateResourceResponseError( | |||
language.statusMessages.unableToCreateResource, | |||
{ | |||
cause, | |||
} | |||
); | |||
} | |||
const headers: Record<string, string> = {}; | |||
// no need to set location here | |||
if (typeof totalItemCount !== 'undefined') { | |||
headers['X-Resource-Total-Item-Count'] = totalItemCount.toString(); | |||
} | |||
return new ResourceCreatedResponse({ | |||
statusMessage: language.statusMessages.resourceCreated, | |||
body: newObject, | |||
headers, | |||
}); | |||
}; |
@@ -1,8 +1,8 @@ | |||
import {ImplementationFunction, DataSource} from '@modal-sh/yasumi/backend'; | |||
import {operation as defineOperation} from '@modal-sh/yasumi'; | |||
import {operation as defineOperation, fromUrlSearchParams} from '@modal-sh/yasumi'; | |||
import { | |||
DataSourceNotFoundResponseError, | |||
ItemNotFoundReponseError, | |||
DataSourceMethodNotImplementedResponseError, | |||
ResourceNotFoundResponseError, | |||
ResourceCollectionFetchedResponse, | |||
ResourceItemFetchedResponse, | |||
} from '../response'; | |||
@@ -17,35 +17,42 @@ export const operation = defineOperation({ | |||
}); | |||
export const implementation: ImplementationFunction = async (ctx) => { | |||
const dataSource: DataSource = ctx.endpoint.dataSource ?? ctx.dataSource ?? {} as DataSource; | |||
// need to genericise the response here so we don't depend on the HTTP responses. | |||
const { resourceId } = ctx.params; | |||
const { getById, getMultiple } = dataSource; | |||
const { | |||
params, | |||
endpoint, | |||
dataSource, | |||
language, | |||
query, | |||
} = ctx; | |||
// need to genericise the response here so that we don't depend on the HTTP responses. | |||
const { resourceId } = params; | |||
const effectiveDataSource: DataSource = endpoint.dataSource ?? dataSource ?? {} as DataSource; | |||
const { getById, getMultiple } = effectiveDataSource; | |||
if (typeof resourceId === 'undefined') { | |||
if (typeof getMultiple === 'undefined') { | |||
throw new DataSourceNotFoundResponseError(); | |||
throw new DataSourceMethodNotImplementedResponseError(language.statusMessages.dataSourceMethodNotImplemented); | |||
} | |||
// TODO add query here | |||
const items = await getMultiple(); | |||
const dataSourceQuery = typeof query !== 'undefined' ? fromUrlSearchParams(query) : undefined; | |||
const items = await getMultiple(dataSourceQuery); | |||
return new ResourceCollectionFetchedResponse({ | |||
statusMessage: 'Resource Collection Fetched', | |||
statusMessage: language.statusMessages.resourceCollectionFetched, | |||
body: items, | |||
}); | |||
} | |||
if (typeof getById === 'undefined') { | |||
throw new DataSourceNotFoundResponseError(); | |||
throw new DataSourceMethodNotImplementedResponseError(language.statusMessages.dataSourceMethodNotImplemented); | |||
} | |||
const item = await getById(resourceId); | |||
if (!item) { | |||
throw new ItemNotFoundReponseError(); | |||
throw new ResourceNotFoundResponseError(language.statusMessages.resourceNotFound); | |||
} | |||
return new ResourceItemFetchedResponse({ | |||
statusMessage: 'Resource Item Fetched', | |||
statusMessage: language.statusMessages.resourceFetched, | |||
body: item, | |||
}); | |||
}; |
@@ -4,6 +4,14 @@ export class ResourceItemFetchedResponse extends HttpResponse(statusCodes.HTTP_S | |||
export class ResourceCollectionFetchedResponse extends HttpResponse(statusCodes.HTTP_STATUS_OK) {} | |||
export class DataSourceNotFoundResponseError extends HttpResponse(statusCodes.HTTP_STATUS_INTERNAL_SERVER_ERROR) {} | |||
export class DataSourceMethodNotImplementedResponseError extends HttpResponse(statusCodes.HTTP_STATUS_NOT_IMPLEMENTED) {} | |||
export class ItemNotFoundReponseError extends HttpResponse(statusCodes.HTTP_STATUS_NOT_FOUND) {} | |||
export class ResourceNotFoundResponseError extends HttpResponse(statusCodes.HTTP_STATUS_NOT_FOUND) {} | |||
export class ResourceCreatedResponse extends HttpResponse(statusCodes.HTTP_STATUS_CREATED) {} | |||
export class UnableToAssignIdFromDataSourceResponseError extends HttpResponse(statusCodes.HTTP_STATUS_INTERNAL_SERVER_ERROR) {} | |||
export class UnableToCreateResourceResponseError extends HttpResponse(statusCodes.HTTP_STATUS_INTERNAL_SERVER_ERROR) {} | |||
export class IncompleteEndpointMetadataResponseError extends HttpResponse(statusCodes.HTTP_STATUS_NOT_IMPLEMENTED) {} |