@chiselstrike/api
Advanced tools
Comparing version
@@ -7,3 +7,4 @@ /// <reference lib="dom" /> | ||
RestrictionFilter = "RestrictionFilter", | ||
PredicateFilter = "PredicateFilter" | ||
PredicateFilter = "PredicateFilter", | ||
Sort = "Sort" | ||
} | ||
@@ -32,3 +33,3 @@ /** | ||
/** Force ChiselStrike to fetch just the `...columns` that are part of the colums list. */ | ||
select(...columns: (keyof T)[]): ChiselCursor<Pick<T, (keyof T)>>; | ||
select<C extends (keyof T)[]>(...columns: C): ChiselCursor<Pick<T, C[number]>>; | ||
/** Restricts this cursor to contain only at most `count` elements */ | ||
@@ -45,2 +46,21 @@ take(count: number): ChiselCursor<T>; | ||
filter(restrictions: Partial<T>): ChiselCursor<T>; | ||
/** | ||
* Sorts cursor elements. | ||
* | ||
* @param comparator determines the sorting order. For two elements of the | ||
* iterable returns true if `lhs` is considered less than `rhs`, | ||
* false otherwise. | ||
* | ||
* Note: the sort is not guaranteed to be stable. | ||
*/ | ||
sort(comparator: (lhs: T, rhs: T) => boolean): ChiselCursor<T>; | ||
/** | ||
* Sorts cursor elements. | ||
* | ||
* @param key specifies which attribute of `T` is to be used as a sort key. | ||
* @param ascending if true, the sort will be ascending. Descending otherwise. | ||
* | ||
* Note: the sort is not guaranteed to be stable. | ||
*/ | ||
sortBy(key: keyof T, ascending?: boolean): ChiselCursor<T>; | ||
/** Executes the function `func` for each element of this cursor. */ | ||
@@ -56,6 +76,6 @@ forEach(func: (arg: T) => void): Promise<void>; | ||
/** Performs recursive descent via Operator.inner examining the whole operator | ||
* chain. If PredicateFilter is encountered, a backend query is generated and all consecutive | ||
* operations are applied on the resulting async iterable in TypeScript. In such a | ||
* case, the function returns the resulting AsyncIterable. | ||
* If no PredicateFilter is found, undefined is returned. | ||
* chain. If PredicateFilter or Sort is encountered, a backend query is generated | ||
* and all consecutive operations are applied on the resulting async iterable | ||
* in TypeScript. In such a case, the function returns the resulting AsyncIterable. | ||
* If no PredicateFilter or Sort is found, undefined is returned. | ||
*/ | ||
@@ -129,3 +149,3 @@ private makeTransformedQueryIter; | ||
new (): T; | ||
}, restrictions: Partial<T>): Promise<T | null>; | ||
}, restrictions: Partial<T>): Promise<T | undefined>; | ||
/** | ||
@@ -149,2 +169,6 @@ * Deletes all entities that match the `restrictions` object. | ||
}, restrictions: Partial<T>): Promise<void>; | ||
/** | ||
* Convenience method for crud() below. | ||
*/ | ||
static crud(p: string): (req: Request) => Promise<Response>; | ||
} | ||
@@ -170,3 +194,130 @@ export declare class OAuthUser extends ChiselEntity { | ||
/** Returns the currently logged-in user or null if no one is logged in. */ | ||
export declare function loggedInUser(): Promise<OAuthUser | null>; | ||
export declare function loggedInUser(): Promise<OAuthUser | undefined>; | ||
export declare function regExParamParse(str: string, loose: boolean): { | ||
keys: string[]; | ||
pattern: RegExp; | ||
}; | ||
declare type ChiselEntityClass<T extends ChiselEntity> = { | ||
new (): T; | ||
findOne: (_: { | ||
id: string; | ||
}) => Promise<T | undefined>; | ||
findMany: (_: Partial<T>) => Promise<Partial<T>[]>; | ||
build: (...properties: Record<string, unknown>[]) => T; | ||
delete: (restrictions: Partial<T>) => Promise<void>; | ||
}; | ||
declare type GenericChiselEntityClass = ChiselEntityClass<ChiselEntity>; | ||
/** | ||
* Get the filters to be used with a ChiselEntity from a URL. | ||
* | ||
* This will get the URL search parameter "f" and assume it's a JSON object. | ||
* @param _entity the entity class that will be filtered | ||
* @param url the url that provides the search parameters | ||
* @returns the filter object, if found and successfully parsed; undefined if not found; throws if parsing failed | ||
*/ | ||
export declare function getEntityFiltersFromURL<T extends ChiselEntity, E extends ChiselEntityClass<T>>(_entity: E, url: URL): Partial<T> | undefined; | ||
/** | ||
* Creates a path parser from a template using regexparam. | ||
* | ||
* @param pathTemplate the path template such as `/static`, `/param/:id/:otherParam`... | ||
* @param loose if true, it can match longer paths. False by default | ||
* @returns function that can parse paths given as string. | ||
* @see https://deno.land/x/regexparam@v2.0.0 | ||
*/ | ||
export declare function createPathParser<T extends Record<string, unknown>>(pathTemplate: string, loose?: boolean): ((path: string) => T); | ||
/** | ||
* Creates a path parser from a template using regexparam. | ||
* | ||
* @param pathTemplate the path template such as `/static`, `/param/:id/:otherParam`... | ||
* @param loose if true, it can match longer paths. False by default | ||
* @returns function that can parse paths given in URL.pathname. | ||
* @see https://deno.land/x/regexparam@v2.0.0 | ||
*/ | ||
export declare function createURLPathParser<T extends Record<string, unknown>>(pathTemplate: string, loose?: boolean): ((url: URL) => T); | ||
/** Creates a Response object from response body and status. */ | ||
export declare type CRUDCreateResponse = (body: unknown, status: number) => (Promise<Response> | Response); | ||
export declare type CRUDBaseParams = { | ||
/** identifier of the object being manipulated, if any */ | ||
id?: string; | ||
/** ChiselStrike's version/branch the server is running, | ||
* such as 'dev' for endpoint '/dev/example' | ||
* when using 'chisel apply --version dev' | ||
*/ | ||
chiselVersion: string; | ||
}; | ||
export declare type CRUDMethodSignature<T extends ChiselEntity, E extends ChiselEntityClass<T>, P extends CRUDBaseParams = CRUDBaseParams> = (entity: E, req: Request, params: P, url: URL, createResponse: CRUDCreateResponse) => Promise<Response>; | ||
/** | ||
* A dictionary mapping HTTP verbs into corresponding REST methods that process a Request and return a Response. | ||
*/ | ||
export declare type CRUDMethods<T extends ChiselEntity, E extends ChiselEntityClass<T>, P extends CRUDBaseParams = CRUDBaseParams> = { | ||
GET: CRUDMethodSignature<T, E, P>; | ||
POST: CRUDMethodSignature<T, E, P>; | ||
PUT: CRUDMethodSignature<T, E, P>; | ||
DELETE: CRUDMethodSignature<T, E, P>; | ||
}; | ||
export declare type CRUDCreateResponses<T extends ChiselEntity, E extends ChiselEntityClass<T>, P extends CRUDBaseParams = CRUDBaseParams> = { | ||
[K in keyof CRUDMethods<T, E, P>]: CRUDCreateResponse; | ||
}; | ||
/** | ||
* These methods can be used as `customMethods` in `ChiselStrike.crud()`. | ||
* | ||
* @example | ||
* Put this in the file 'endpoints/comments.ts': | ||
* ```typescript | ||
* import { Comment } from "../models/comment"; | ||
* export default crud( | ||
* Comment, | ||
* '/comments/:id', | ||
* { | ||
* PUT: standardCRUDMethods.notFound, // do not update, instead returns 404 | ||
* DELETE: standardCRUDMethods.methodNotAllowed, // do not delete, instead returns 405 | ||
* }, | ||
* ); | ||
* ``` | ||
*/ | ||
export declare const standardCRUDMethods: { | ||
readonly forbidden: (_entity: GenericChiselEntityClass, _req: Request, _params: CRUDBaseParams, _url: URL, createResponse: CRUDCreateResponse) => Promise<Response>; | ||
readonly notFound: (_entity: GenericChiselEntityClass, _req: Request, _params: CRUDBaseParams, _url: URL, createResponse: CRUDCreateResponse) => Promise<Response>; | ||
readonly methodNotAllowed: (_entity: GenericChiselEntityClass, _req: Request, _params: CRUDBaseParams, _url: URL, createResponse: CRUDCreateResponse) => Promise<Response>; | ||
}; | ||
/** | ||
* Generates endpoint code to handle REST methods GET/PUT/POST/DELETE for this entity. | ||
* @example | ||
* Put this in the file 'endpoints/comments.ts': | ||
* ```typescript | ||
* import { Comment } from "../models/comment"; | ||
* export default crud(Comment, "/comments/:id"); | ||
* ``` | ||
* This results in a /comments endpoint that correctly handles all REST methods over Comment. | ||
* @param entity Entity type | ||
* @param urlTemplate Request URL must match this template (see https://deno.land/x/regexparam for syntax). | ||
* Some CRUD methods rely on parts of the URL to identify the resource to apply to. Eg, GET /comments/1234 | ||
* returns the comment entity with id=1234, while GET /comments returns all comments. This parameter describes | ||
* how to find the relevant parts in the URL. Default CRUD methods (see `defaultCrudMethods`) look for the :id | ||
* part in this template to identify specific entity instances. If there is no :id in the template, then '/:id' | ||
* is automatically added to its end. Custom methods can use other named parts. NOTE: because of file-based | ||
* routing, `urlTemplate` must necessarily begin with the path of the endpoint invoking this function; it's the | ||
* only way for the request URL to match it. Eg, if `crud()` is invoked by the file `endpoints/a/b/foo.ts`, the | ||
* `urlTemplate` must begin with 'a/b/foo'; in fact, it can be exactly 'a/b/foo', taking advantage of reasonable | ||
* defaults to ensure that the created RESTful API works as you would expect. | ||
* @param config Configure the CRUD behavior: | ||
* - `customMethods`: custom request handlers overriding the defaults. | ||
* Each present property overrides that method's handler. You can use `standardCRUDMethods` members here to | ||
* conveniently reject some actions. When `customMethods` is absent, we use methods from `defaultCrudMethods`. | ||
* Note that these default methods look for the `id` property in their `params` argument; if set, its value is | ||
* the id of the entity to process. Conveniently, the default `urlTemplate` parser sets this property from the | ||
* `:id` pattern. | ||
* - `createResponses`: if present, a dictionary of method-specific Response creators. | ||
* - `defaultCreateResponse`: default function to create all responses if `createResponses` entry is not provided. | ||
* Defaults to `responseFromJson()`. | ||
* - `parsePath`: parses the URL path instead of https://deno.land/x/regexparam. The parsing result is passed to | ||
* CRUD methods as the `params` argument. | ||
* @returns A request-handling function suitable as a default export in an endpoint. | ||
*/ | ||
export declare function crud<T extends ChiselEntity, E extends ChiselEntityClass<T>, P extends CRUDBaseParams = CRUDBaseParams>(entity: E, urlTemplate: string, config?: { | ||
customMethods?: Partial<CRUDMethods<T, ChiselEntityClass<T>, P>>; | ||
createResponses?: Partial<CRUDCreateResponses<T, ChiselEntityClass<T>, P>>; | ||
defaultCreateResponse?: CRUDCreateResponse; | ||
parsePath?: (url: URL) => P; | ||
}): (req: Request) => Promise<Response>; | ||
export {}; |
@@ -11,2 +11,3 @@ // SPDX-FileCopyrightText: © 2021 ChiselStrike <info@chiselstrike.com> | ||
OpType["PredicateFilter"] = "PredicateFilter"; | ||
OpType["Sort"] = "Sort"; | ||
})(OpType || (OpType = {})); | ||
@@ -94,4 +95,4 @@ /** | ||
/** | ||
* PredicateFilter operator applies @predicate on each element and keeps | ||
* only those for which the @predicate returns true. | ||
* PredicateFilter operator applies `predicate` on each element and keeps | ||
* only those for which the `predicate` returns true. | ||
*/ | ||
@@ -146,2 +147,31 @@ class PredicateFilter extends Operator { | ||
} | ||
/** | ||
* Sort operator sorts elements using `comparator` which | ||
* for two elements of the iterable returns true if `lhs` | ||
* is considered less than `rhs`, false otherwise. | ||
*/ | ||
class Sort extends Operator { | ||
comparator; | ||
constructor(comparator, inner) { | ||
super(OpType.Sort, inner); | ||
this.comparator = comparator; | ||
} | ||
apply(iter) { | ||
const cmp = this.comparator; | ||
return { | ||
[Symbol.asyncIterator]: async function* () { | ||
const elements = []; | ||
for await (const e of iter) { | ||
elements.push(e); | ||
} | ||
elements.sort((lhs, rhs) => { | ||
return cmp(lhs, rhs) ? -1 : 1; | ||
}); | ||
for (const e of elements) { | ||
yield e; | ||
} | ||
}, | ||
}; | ||
} | ||
} | ||
/** ChiselCursor is a lazy iterator that will be used by ChiselStrike to construct an optimized query. */ | ||
@@ -172,2 +202,28 @@ export class ChiselCursor { | ||
} | ||
/** | ||
* Sorts cursor elements. | ||
* | ||
* @param comparator determines the sorting order. For two elements of the | ||
* iterable returns true if `lhs` is considered less than `rhs`, | ||
* false otherwise. | ||
* | ||
* Note: the sort is not guaranteed to be stable. | ||
*/ | ||
sort(comparator) { | ||
return new ChiselCursor(this.baseConstructor, new Sort(comparator, this.inner)); | ||
} | ||
/** | ||
* Sorts cursor elements. | ||
* | ||
* @param key specifies which attribute of `T` is to be used as a sort key. | ||
* @param ascending if true, the sort will be ascending. Descending otherwise. | ||
* | ||
* Note: the sort is not guaranteed to be stable. | ||
*/ | ||
sortBy(key, ascending = true) { | ||
const cmp = (lhs, rhs) => { | ||
return ascending === (lhs[key] < rhs[key]); | ||
}; | ||
return new ChiselCursor(this.baseConstructor, new Sort(cmp, this.inner)); | ||
} | ||
/** Executes the function `func` for each element of this cursor. */ | ||
@@ -201,6 +257,6 @@ async forEach(func) { | ||
/** Performs recursive descent via Operator.inner examining the whole operator | ||
* chain. If PredicateFilter is encountered, a backend query is generated and all consecutive | ||
* operations are applied on the resulting async iterable in TypeScript. In such a | ||
* case, the function returns the resulting AsyncIterable. | ||
* If no PredicateFilter is found, undefined is returned. | ||
* chain. If PredicateFilter or Sort is encountered, a backend query is generated | ||
* and all consecutive operations are applied on the resulting async iterable | ||
* in TypeScript. In such a case, the function returns the resulting AsyncIterable. | ||
* If no PredicateFilter or Sort is found, undefined is returned. | ||
*/ | ||
@@ -218,3 +274,3 @@ makeTransformedQueryIter(op) { | ||
} | ||
else if (op.type == OpType.PredicateFilter) { | ||
else if (op.type == OpType.PredicateFilter || op.type == OpType.Sort) { | ||
iter = this.makeQueryIter(op.inner); | ||
@@ -356,3 +412,3 @@ return op.apply(iter); | ||
} | ||
return null; | ||
return undefined; | ||
} | ||
@@ -380,2 +436,8 @@ /** | ||
} | ||
/** | ||
* Convenience method for crud() below. | ||
*/ | ||
static crud(p) { | ||
return crud(this, p); | ||
} | ||
} | ||
@@ -433,3 +495,242 @@ export class OAuthUser extends ChiselEntity { | ||
const id = await Deno.core.opAsync("chisel_user", {}); | ||
return id == null ? null : await OAuthUser.findOne({ id }); | ||
if (id == null) { | ||
return undefined; | ||
} | ||
return await OAuthUser.findOne({ id }); | ||
} | ||
// TODO: BEGIN: this should be in another file: crud.ts | ||
// TODO: BEGIN: when module import is fixed: | ||
// import { parse as regExParamParse } from "regexparam"; | ||
// or: | ||
// import { parse as regExParamParse } from "regexparam"; | ||
// In the meantime, the regExParamParse function is copied from | ||
// https://deno.land/x/regexparam@v2.0.0/src/index.js under MIT License included | ||
// below. ChiselStrike added the TS signature and minor cleanups. | ||
// | ||
// Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com) | ||
// | ||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in | ||
// all copies or substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
// THE SOFTWARE. | ||
export function regExParamParse(str, loose) { | ||
let tmp, pattern = ""; | ||
const keys = [], arr = str.split("/"); | ||
arr[0] || arr.shift(); | ||
while ((tmp = arr.shift())) { | ||
const c = tmp[0]; | ||
if (c === "*") { | ||
keys.push("wild"); | ||
pattern += "/(.*)"; | ||
} | ||
else if (c === ":") { | ||
const o = tmp.indexOf("?", 1); | ||
const ext = tmp.indexOf(".", 1); | ||
keys.push(tmp.substring(1, ~o ? o : ~ext ? ext : tmp.length)); | ||
pattern += !!~o && !~ext ? "(?:/([^/]+?))?" : "/([^/]+?)"; | ||
if (~ext) | ||
pattern += (~o ? "?" : "") + "\\" + tmp.substring(ext); | ||
} | ||
else { | ||
pattern += "/" + tmp; | ||
} | ||
} | ||
return { | ||
keys: keys, | ||
pattern: new RegExp("^" + pattern + (loose ? "(?=$|/)" : "/?$"), "i"), | ||
}; | ||
} | ||
/** | ||
* Get the filters to be used with a ChiselEntity from a URL. | ||
* | ||
* This will get the URL search parameter "f" and assume it's a JSON object. | ||
* @param _entity the entity class that will be filtered | ||
* @param url the url that provides the search parameters | ||
* @returns the filter object, if found and successfully parsed; undefined if not found; throws if parsing failed | ||
*/ | ||
export function getEntityFiltersFromURL(_entity, url) { | ||
// TODO: it's more common to have filters as regular query parameters, URI-encoded, | ||
// then entity may be used to get such field names | ||
// TODO: validate if unknown filters where given? | ||
const f = url.searchParams.get("f"); | ||
if (!f) { | ||
return undefined; | ||
} | ||
const o = JSON.parse(decodeURI(f)); | ||
if (o && typeof o === "object") { | ||
return o; | ||
} | ||
throw new Error(`provided search parameter 'f=${f}' is not a JSON object.`); | ||
} | ||
/** | ||
* Creates a path parser from a template using regexparam. | ||
* | ||
* @param pathTemplate the path template such as `/static`, `/param/:id/:otherParam`... | ||
* @param loose if true, it can match longer paths. False by default | ||
* @returns function that can parse paths given as string. | ||
* @see https://deno.land/x/regexparam@v2.0.0 | ||
*/ | ||
export function createPathParser(pathTemplate, loose = false) { | ||
const { pattern, keys: keysOrFalse } = regExParamParse(pathTemplate, loose); | ||
if (typeof keysOrFalse === "boolean") { | ||
throw new Error(`invalid pathTemplate=${pathTemplate}, expected string`); | ||
} | ||
const keys = keysOrFalse; | ||
return function pathParser(path) { | ||
const matches = pattern.exec(path); | ||
return keys.reduce((acc, key, index) => { | ||
acc[key] = matches?.[index + 1]; | ||
return acc; | ||
}, {}); | ||
}; | ||
} | ||
/** | ||
* Creates a path parser from a template using regexparam. | ||
* | ||
* @param pathTemplate the path template such as `/static`, `/param/:id/:otherParam`... | ||
* @param loose if true, it can match longer paths. False by default | ||
* @returns function that can parse paths given in URL.pathname. | ||
* @see https://deno.land/x/regexparam@v2.0.0 | ||
*/ | ||
export function createURLPathParser(pathTemplate, loose = false) { | ||
const pathParser = createPathParser(pathTemplate, loose); | ||
return (url) => pathParser(url.pathname); | ||
} | ||
const defaultCrudMethods = { | ||
// Returns a specific entity matching params.id (if present) or all entities matching the filter in the `f` URL parameter. | ||
GET: async (entity, _req, params, url, createResponse) => { | ||
const { id } = params; | ||
if (!id) { | ||
return createResponse(await entity.findMany(getEntityFiltersFromURL(entity, url) || {}), 200); | ||
} | ||
const u = await entity.findOne({ id }); | ||
return createResponse(u ?? "Not found", u ? 200 : 404); | ||
}, | ||
// Creates and returns a new entity from the `req` payload. Ignores the payload's id property and assigns a fresh one. | ||
POST: async (entity, req, _params, _url, createResponse) => { | ||
const u = entity.build(await req.json()); | ||
u.id = undefined; | ||
await u.save(); | ||
return createResponse(u, 200); | ||
}, | ||
// Updates and returns the entity matching params.id (which must be set) from the `req` payload. | ||
PUT: async (entity, req, params, _url, createResponse) => { | ||
const { id } = params; | ||
if (!id) { | ||
return createResponse("PUT requires item ID in the URL", 400); | ||
} | ||
const u = entity.build(await req.json()); | ||
u.id = id; | ||
await u.save(); | ||
return createResponse(u, 200); | ||
}, | ||
// Deletes the entity matching params.id (if present) or all entities matching the filter in the `f` URL parameter. One of the two must be present. | ||
DELETE: async (entity, _req, params, url, createResponse) => { | ||
const { id } = params; | ||
if (id) { | ||
await entity.delete({ id }); | ||
return createResponse(`Deleted ID ${id}`, 200); | ||
} | ||
const restrictions = getEntityFiltersFromURL(entity, url); | ||
if (restrictions) { | ||
await entity.delete(restrictions); | ||
return createResponse(`Deleted entities matching ${JSON.stringify(restrictions)}`, 200); | ||
} | ||
return createResponse("Neither ID nor filter found", 422); | ||
}, | ||
}; | ||
/** | ||
* These methods can be used as `customMethods` in `ChiselStrike.crud()`. | ||
* | ||
* @example | ||
* Put this in the file 'endpoints/comments.ts': | ||
* ```typescript | ||
* import { Comment } from "../models/comment"; | ||
* export default crud( | ||
* Comment, | ||
* '/comments/:id', | ||
* { | ||
* PUT: standardCRUDMethods.notFound, // do not update, instead returns 404 | ||
* DELETE: standardCRUDMethods.methodNotAllowed, // do not delete, instead returns 405 | ||
* }, | ||
* ); | ||
* ``` | ||
*/ | ||
export const standardCRUDMethods = { | ||
forbidden: (_entity, _req, _params, _url, createResponse) => Promise.resolve(createResponse("Forbidden", 403)), | ||
notFound: (_entity, _req, _params, _url, createResponse) => Promise.resolve(createResponse("Not Found", 404)), | ||
methodNotAllowed: (_entity, _req, _params, _url, createResponse) => Promise.resolve(createResponse("Method Not Allowed", 405)), | ||
}; | ||
/** | ||
* Generates endpoint code to handle REST methods GET/PUT/POST/DELETE for this entity. | ||
* @example | ||
* Put this in the file 'endpoints/comments.ts': | ||
* ```typescript | ||
* import { Comment } from "../models/comment"; | ||
* export default crud(Comment, "/comments/:id"); | ||
* ``` | ||
* This results in a /comments endpoint that correctly handles all REST methods over Comment. | ||
* @param entity Entity type | ||
* @param urlTemplate Request URL must match this template (see https://deno.land/x/regexparam for syntax). | ||
* Some CRUD methods rely on parts of the URL to identify the resource to apply to. Eg, GET /comments/1234 | ||
* returns the comment entity with id=1234, while GET /comments returns all comments. This parameter describes | ||
* how to find the relevant parts in the URL. Default CRUD methods (see `defaultCrudMethods`) look for the :id | ||
* part in this template to identify specific entity instances. If there is no :id in the template, then '/:id' | ||
* is automatically added to its end. Custom methods can use other named parts. NOTE: because of file-based | ||
* routing, `urlTemplate` must necessarily begin with the path of the endpoint invoking this function; it's the | ||
* only way for the request URL to match it. Eg, if `crud()` is invoked by the file `endpoints/a/b/foo.ts`, the | ||
* `urlTemplate` must begin with 'a/b/foo'; in fact, it can be exactly 'a/b/foo', taking advantage of reasonable | ||
* defaults to ensure that the created RESTful API works as you would expect. | ||
* @param config Configure the CRUD behavior: | ||
* - `customMethods`: custom request handlers overriding the defaults. | ||
* Each present property overrides that method's handler. You can use `standardCRUDMethods` members here to | ||
* conveniently reject some actions. When `customMethods` is absent, we use methods from `defaultCrudMethods`. | ||
* Note that these default methods look for the `id` property in their `params` argument; if set, its value is | ||
* the id of the entity to process. Conveniently, the default `urlTemplate` parser sets this property from the | ||
* `:id` pattern. | ||
* - `createResponses`: if present, a dictionary of method-specific Response creators. | ||
* - `defaultCreateResponse`: default function to create all responses if `createResponses` entry is not provided. | ||
* Defaults to `responseFromJson()`. | ||
* - `parsePath`: parses the URL path instead of https://deno.land/x/regexparam. The parsing result is passed to | ||
* CRUD methods as the `params` argument. | ||
* @returns A request-handling function suitable as a default export in an endpoint. | ||
*/ | ||
export function crud(entity, urlTemplate, config) { | ||
const pathTemplate = "/:chiselVersion" + | ||
(urlTemplate.startsWith("/") ? "" : "/") + | ||
(urlTemplate.includes(":id") ? urlTemplate : `${urlTemplate}/:id`); | ||
const defaultCreateResponse = config?.defaultCreateResponse || | ||
responseFromJson; | ||
const parsePath = config?.parsePath || | ||
createURLPathParser(pathTemplate); | ||
const localDefaultCrudMethods = defaultCrudMethods; | ||
const methods = config?.customMethods | ||
? { ...localDefaultCrudMethods, ...config?.customMethods } | ||
: localDefaultCrudMethods; | ||
return (req) => { | ||
const methodName = req.method; // assume valid, will be handled gracefully | ||
const createResponse = config?.createResponses?.[methodName] || | ||
defaultCreateResponse; | ||
const method = methods[methodName]; | ||
if (!method) { | ||
return Promise.resolve(createResponse(`Unsupported HTTP method: ${methodName}`, 405)); | ||
} | ||
const url = new URL(req.url); | ||
const params = parsePath(url); | ||
return method(entity, req, params, url, createResponse); | ||
}; | ||
} | ||
// TODO: END: this should be in another file: crud.ts |
{ | ||
"name": "@chiselstrike/api", | ||
"version": "0.7.0", | ||
"version": "0.8.0", | ||
"main": "lib/chisel.js", | ||
@@ -5,0 +5,0 @@ "types": "lib/chisel.d.ts", |
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
48303
81.05%1164
63.48%