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'; | import {DataSource} from './data-source'; | ||||
interface BackendParams<App extends BaseApp> { | interface BackendParams<App extends BaseApp> { | ||||
@@ -12,6 +19,8 @@ export interface ImplementationContext { | |||||
params: Record<string, unknown>; | params: Record<string, unknown>; | ||||
query?: URLSearchParams; | query?: URLSearchParams; | ||||
dataSource?: DataSource; | dataSource?: DataSource; | ||||
language: Language; | |||||
setLocation(location: string): void; | |||||
} | } | ||||
export type ImplementationFunction = (params: ImplementationContext) => Promise<Response | void>; | export type ImplementationFunction = (params: ImplementationContext) => Promise<Response | void>; | ||||
@@ -92,6 +92,7 @@ export interface Endpoint< | |||||
schema: Schema; | schema: Schema; | ||||
params: Set<string>; | params: Set<string>; | ||||
operations: Set<string>; | operations: Set<string>; | ||||
metadata: Map<string, unknown>; | |||||
can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | ||||
op: OpName, | op: OpName, | ||||
value?: OpValue | value?: OpValue | ||||
@@ -110,7 +111,8 @@ export interface Endpoint< | |||||
operations: State['operations']; | operations: State['operations']; | ||||
params: State['params'] extends string[] ? readonly [...State['params'], ParamName]: [ParamName]; | 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> ? ( | export type GetEndpointParams<T extends Endpoint> = T extends Endpoint<any, infer R> ? ( | ||||
@@ -132,6 +134,7 @@ class EndpointInstance< | |||||
readonly operations: Set<string>; | readonly operations: Set<string>; | ||||
readonly params: Set<string>; | readonly params: Set<string>; | ||||
readonly schema: Params['schema']; | readonly schema: Params['schema']; | ||||
readonly metadata: Map<string, unknown>; | |||||
constructor(params: Params) { | constructor(params: Params) { | ||||
this.name = params.name; | this.name = params.name; | ||||
@@ -139,6 +142,7 @@ class EndpointInstance< | |||||
this.operations = new Set<string>(); | this.operations = new Set<string>(); | ||||
this.params = new Set<string>(); | this.params = new Set<string>(); | ||||
this.dataSource = params.dataSource; | this.dataSource = params.dataSource; | ||||
this.metadata = new Map<string, unknown>(); | |||||
} | } | ||||
can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | ||||
@@ -159,7 +163,11 @@ class EndpointInstance< | |||||
name: ParamName | name: ParamName | ||||
) { | ) { | ||||
this.params.add(name); | this.params.add(name); | ||||
return this; | |||||
} | |||||
id<IdAttr extends string = string>(id: IdAttr) { | |||||
this.metadata.set('idAttr', id); | |||||
return this; | return this; | ||||
} | } | ||||
} | } | ||||
@@ -15,7 +15,7 @@ export const LANGUAGE_DEFAULT_ERROR_STATUS_MESSAGE_KEYS = [ | |||||
'badRequest', | 'badRequest', | ||||
'deleteNonExistingResource', | 'deleteNonExistingResource', | ||||
'unableToCreateResource', | 'unableToCreateResource', | ||||
'unableToBindResourceDataSource', | |||||
'dataSourceMethodNotImplemented', | |||||
'unableToGenerateIdFromResourceDataSource', | 'unableToGenerateIdFromResourceDataSource', | ||||
'unableToAssignIdFromResourceDataSource', | 'unableToAssignIdFromResourceDataSource', | ||||
'unableToEmplaceResource', | 'unableToEmplaceResource', | ||||
@@ -67,41 +67,41 @@ export const FALLBACK_LANGUAGE = { | |||||
statusMessages: { | statusMessages: { | ||||
unableToSerializeResponse: 'Unable To Serialize Response', | unableToSerializeResponse: 'Unable To Serialize Response', | ||||
unableToEncodeResponse: 'Unable To Encode 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', | languageNotAcceptable: 'Language Not Acceptable', | ||||
characterSetNotAcceptable: 'Character Set 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', | mediaTypeNotAcceptable: 'Media Type Not Acceptable', | ||||
methodNotAllowed: 'Method Not Allowed', | methodNotAllowed: 'Method Not Allowed', | ||||
urlNotFound: 'URL Not Found', | urlNotFound: 'URL Not Found', | ||||
badRequest: 'Bad Request', | badRequest: 'Bad Request', | ||||
ok: 'OK', | ok: 'OK', | ||||
provideOptions: 'Provide Options', | 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', | 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', | notImplemented: 'Not Implemented', | ||||
internalServerError: 'Internal Server Error', | internalServerError: 'Internal Server Error', | ||||
}, | }, | ||||
@@ -211,7 +211,7 @@ export const FALLBACK_LANGUAGE = { | |||||
'Contact the administrator regarding missing configuration or unavailability of dependencies.', | 'Contact the administrator regarding missing configuration or unavailability of dependencies.', | ||||
], | ], | ||||
], | ], | ||||
unableToBindResourceDataSource: [ | |||||
dataSourceMethodNotImplemented: [ | |||||
'The resource could not be associated from the data source.', | 'The resource could not be associated from the data source.', | ||||
[ | [ | ||||
'Try the request again at a later time.', | 'Try the request again at a later time.', | ||||
@@ -1 +1,2 @@ | |||||
export * from './common'; | 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); | const bodyDecoded = decoder(bodyBuffer, requestBodyParams); | ||||
body = requestBodyMediaType.deserialize(bodyDecoded, requestBodyParams); | body = requestBodyMediaType.deserialize(bodyDecoded, requestBodyParams); | ||||
} | } | ||||
const isCqrs = false; | |||||
let hasSentResponse = false; | |||||
const responseSpec = await implementation({ | const responseSpec = await implementation({ | ||||
endpoint, | endpoint, | ||||
body, | body, | ||||
params: endpointParams ?? {}, | params: endpointParams ?? {}, | ||||
query: typeof search !== 'undefined' ? new URLSearchParams(search) : undefined, | query: typeof search !== 'undefined' ? new URLSearchParams(search) : undefined, | ||||
dataSource: this.backend.dataSource, | 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') { | if (typeof responseSpec === 'undefined') { | ||||
res.writeHead(statusCodes.HTTP_STATUS_UNPROCESSABLE_ENTITY, errorHeaders); | res.writeHead(statusCodes.HTTP_STATUS_UNPROCESSABLE_ENTITY, errorHeaders); | ||||
res.end(); | res.end(); | ||||
@@ -36,6 +36,8 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||||
} | } | ||||
async connect(params: ServiceParams): Promise<ClientConnection> { | 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 = { | const connection = { | ||||
host: params.host ?? DEFAULT_HOST, | host: params.host ?? DEFAULT_HOST, | ||||
port: params.port ?? DEFAULT_PORT, | port: params.port ?? DEFAULT_PORT, | ||||
@@ -11,6 +11,7 @@ import * as deleteOperation from './implementation/delete'; | |||||
interface AddResourceRecipeParams { | interface AddResourceRecipeParams { | ||||
endpointName: string; | endpointName: string; | ||||
dataSource?: DataSource; | dataSource?: DataSource; | ||||
// TODO specify the available operations to implement | |||||
} | } | ||||
export const addResourceRecipe = (params: AddResourceRecipeParams): Recipe => (a) => { | 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; | export const name = 'create' as const; | ||||
@@ -11,5 +16,74 @@ export const operation = defineOperation({ | |||||
}); | }); | ||||
export const implementation: ImplementationFunction = async (ctx) => { | 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 {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 { | import { | ||||
DataSourceNotFoundResponseError, | |||||
ItemNotFoundReponseError, | |||||
DataSourceMethodNotImplementedResponseError, | |||||
ResourceNotFoundResponseError, | |||||
ResourceCollectionFetchedResponse, | ResourceCollectionFetchedResponse, | ||||
ResourceItemFetchedResponse, | ResourceItemFetchedResponse, | ||||
} from '../response'; | } from '../response'; | ||||
@@ -17,35 +17,42 @@ export const operation = defineOperation({ | |||||
}); | }); | ||||
export const implementation: ImplementationFunction = async (ctx) => { | 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 resourceId === 'undefined') { | ||||
if (typeof getMultiple === '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({ | return new ResourceCollectionFetchedResponse({ | ||||
statusMessage: 'Resource Collection Fetched', | |||||
statusMessage: language.statusMessages.resourceCollectionFetched, | |||||
body: items, | body: items, | ||||
}); | }); | ||||
} | } | ||||
if (typeof getById === 'undefined') { | if (typeof getById === 'undefined') { | ||||
throw new DataSourceNotFoundResponseError(); | |||||
throw new DataSourceMethodNotImplementedResponseError(language.statusMessages.dataSourceMethodNotImplemented); | |||||
} | } | ||||
const item = await getById(resourceId); | const item = await getById(resourceId); | ||||
if (!item) { | if (!item) { | ||||
throw new ItemNotFoundReponseError(); | |||||
throw new ResourceNotFoundResponseError(language.statusMessages.resourceNotFound); | |||||
} | } | ||||
return new ResourceItemFetchedResponse({ | return new ResourceItemFetchedResponse({ | ||||
statusMessage: 'Resource Item Fetched', | |||||
statusMessage: language.statusMessages.resourceFetched, | |||||
body: item, | 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 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) {} |