🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more
Socket
DemoInstallSign in
Socket

@chiselstrike/api

Package Overview
Dependencies
Maintainers
3
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@chiselstrike/api - npm Package Compare versions

Comparing version

to
0.8.0

167

lib/chisel.d.ts

@@ -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

2

package.json
{
"name": "@chiselstrike/api",
"version": "0.7.0",
"version": "0.8.0",
"main": "lib/chisel.js",

@@ -5,0 +5,0 @@ "types": "lib/chisel.d.ts",