🚀 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.9.0

278

lib/chisel.d.ts

@@ -0,25 +1,20 @@

/// <reference lib="deno.core" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
declare enum OpType {
BaseEntity = "BaseEntity",
Take = "Take",
Skip = "Skip",
ColumnsSelect = "ColumnsSelect",
RestrictionFilter = "RestrictionFilter",
PredicateFilter = "PredicateFilter",
Sort = "Sort"
ExpressionFilter = "ExpressionFilter",
SortBy = "SortBy"
}
/**
* Base class for various Operators applicable on `ChiselCursor`. Each operator
* should extend this class and pass on its `type` identifier from the `OpType`
* enum.
*/
declare abstract class Operator {
declare abstract class Operator<T> {
readonly type: OpType;
readonly inner: Operator | undefined;
constructor(type: OpType, inner: Operator | undefined);
/** Applies specified Operator `op` on each element of passed iterable
* `iter` creating a new iterable.
*/
abstract apply(iter: AsyncIterable<Record<string, unknown>>): AsyncIterable<Record<string, unknown>>;
readonly inner: Operator<T> | undefined;
constructor(type: OpType, inner: Operator<T> | undefined);
abstract apply(iter: AsyncIterable<T>): AsyncIterable<T>;
containsType(opType: OpType): boolean;
}
/** ChiselCursor is a lazy iterator that will be used by ChiselStrike to construct an optimized query. */
export declare class ChiselCursor<T> {

@@ -30,170 +25,52 @@ private baseConstructor;

new (): T;
}, inner: Operator);
/** Force ChiselStrike to fetch just the `...columns` that are part of the colums list. */
}, inner: Operator<T>);
select<C extends (keyof T)[]>(...columns: C): ChiselCursor<Pick<T, C[number]>>;
/** Restricts this cursor to contain only at most `count` elements */
take(count: number): ChiselCursor<T>;
/**
* Restricts this cursor to contain only elements that match the given @predicate.
*/
skip(count: number): ChiselCursor<T>;
filter(predicate: (arg: T) => boolean): ChiselCursor<T>;
/**
* Restricts this cursor to contain just the objects that match the `Partial`
* object `restrictions`.
*/
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.
*/
__filterWithExpression(predicate: (arg: T) => boolean, expression: Record<string, unknown>): ChiselCursor<T>;
sortBy(key: keyof T, ascending?: boolean): ChiselCursor<T>;
/** Executes the function `func` for each element of this cursor. */
forEach(func: (arg: T) => void): Promise<void>;
/** Converts this cursor to an Array.
*
* Use this with caution as the result set can be very big.
* It is recommended that you take() first to cap the maximum number of elements. */
toArray(): Promise<Partial<T>[]>;
/** ChiselCursor implements asyncIterator, meaning you can use it in any asynchronous context. */
[Symbol.asyncIterator](): AsyncGenerator<Awaited<T>, void, unknown>;
/** Performs recursive descent via Operator.inner examining the whole operator
* 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.
*/
[Symbol.asyncIterator](): AsyncIterator<T>;
private makeTransformedQueryIter;
private makeQueryIter;
/** Recursively examines operator chain searching for ColumnsSelect operator.
* Returns true if found, false otherwise.
*/
private containsSelect;
}
export declare class ChiselRequest extends Request {
version: string;
endpoint: string;
pathParams: string;
user?: OAuthUser | undefined;
constructor(input: string, init: RequestInit, version: string, endpoint: string, pathParams: string, user?: OAuthUser | undefined);
pathComponents(): string[];
}
export declare function chiselIterator<T>(type: {
new (): T;
}): ChiselCursor<T>;
/** ChiselEntity is a class that ChiselStrike user-defined entities are expected to extend.
*
* It provides properties that are inherent to a ChiselStrike entity, like an id, and static
* methods that can be used to obtain a `ChiselCursor`.
*/
export declare class ChiselEntity {
/** UUID identifying this object. */
id?: string;
/**
* Builds a new entity.
*
* @param properties The properties of the created entity. If more than one property
* is passed, the expected order of assignment is the same as Object.assign.
*
* @example
* ```typescript
* export class User extends ChiselEntity {
* username: string,
* email: string,
* }
* // Create an entity from object literal:
* const user = User.build({ username: "alice", email: "alice@example.com" });
* // Create an entity from JSON:
* const userJson = JSON.parse('{"username": "alice", "email": "alice@example.com"}');
* const anotherUser = User.build(userJson);
*
* // Create an entity from different JSON objects:
* const otherUserJson = JSON.parse('{"username": "alice"}, {"email": "alice@example.com"}');
* const yetAnotherUser = User.build(userJson);
*
* // now optionally save them to the backend
* await user.save();
* await anotherUser.save();
* await yetAnotherUser.save();
* ```
* @returns The persisted entity with given properties and the `id` property set.
*/
static build<T extends ChiselEntity>(this: {
new (): T;
}, ...properties: Record<string, unknown>[]): T;
/** saves the current object into the backend */
save(): Promise<void>;
/** Returns a `ChiselCursor` containing all elements of type T known to ChiselStrike.
*
* Note that `ChiselCursor` is a lazy iterator, so this doesn't mean a query will be generating fetching all elements at this point. */
static cursor<T>(this: {
new (): T;
}): ChiselCursor<T>;
/** Restricts this iterator to contain just the objects that match the `Partial` object `restrictions`. */
static findAll<T>(this: {
new (): T;
}, take?: number): Promise<Partial<T>[]>;
static findMany<T>(this: {
new (): T;
}, restrictions: Partial<T>, take?: number): Promise<Partial<T>[]>;
/** Returns a single object that matches the `Partial` object `restrictions` passed as its parameter.
*
* If more than one match is found, any is returned. */
static findOne<T extends ChiselEntity>(this: {
new (): T;
}, restrictions: Partial<T>): Promise<T | undefined>;
/**
* Deletes all entities that match the `restrictions` object.
*
* @example
* ```typescript
* export class User extends ChiselEntity {
* username: string,
* email: string,
* }
* const user = User.build({ username: "alice", email: "alice@example.com" });
* await user.save();
*
* await User.delete({ email: "alice@example.com"})
* ```
*/
static delete<T extends ChiselEntity>(this: {
new (): T;
}, restrictions: Partial<T>): Promise<void>;
/**
* 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 Comment.crud();
* ```
*
* This results in a /comments endpoint that correctly handles all REST methods over Comment. The default endpoints are:
*
* * **POST:**
* * `/comments` Creates a new object. Payload is a JSON with the properties of `Comment` as keys.
*
* * **GET:**
* * `/comments` returns an array with all elements (use carefully in datasets expected to be large)
* * `/comments?f={key:val}` returns all elements that match the filter specified by the json object given as search param.
* * `/comments/:id` returns the element with the given ID.
*
* * **DELETE:**
* * `/comments/:id` deletes the element with the given ID.
* * `/comments?f={key:val}` deletes all elements that match the filter specified by the json object given as search param.
* * `/comments?f={}` deletes all elements
*
* * **PUT:**
* * `/comments/:id` overwrites the element with the given ID. Payload is a JSON with the properties of `Comment` as keys
*
* If you need more control over which method to generate and their behavior, see the top-level `crud()` function
*
* @returns A request-handling function suitable as a default export in an endpoint.
*/
static crud(_ignored?: string): (req: Request) => Promise<Response>;
static create<T extends ChiselEntity>(this: {
new (): T;
}, ...properties: Record<string, unknown>[]): Promise<T>;
}

@@ -203,10 +80,2 @@ export declare class OAuthUser extends ChiselEntity {

}
export declare function buildReadableStreamForBody(rid: number): ReadableStream<string>;
/**
* Gets a secret from the environment
*
* To allow a secret to be used, the server has to be run with * --allow-env <YOUR_SECRET>
*
* In development mode, all of your environment variables are accessible
*/
declare type JSONValue = string | number | boolean | null | {

@@ -218,5 +87,10 @@ [x: string]: JSONValue;

export declare function labels(..._val: string[]): <T>(_target: T, _propertyName: string) => void;
export declare function unique(): void;
/** Returns the currently logged-in user or null if no one is logged in. */
export declare function unique(_target: unknown, _name: string): void;
export declare function loggedInUser(): Promise<OAuthUser | undefined>;
export declare const requestContext: {
path: string;
method: string;
apiVersion: string;
userId?: string;
};
export declare function regExParamParse(str: string, loose: boolean): {

@@ -234,46 +108,13 @@ keys: string[];

delete: (restrictions: Partial<T>) => Promise<void>;
cursor: () => ChiselCursor<T>;
};
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> = {

@@ -288,19 +129,2 @@ GET: CRUDMethodSignature<T, E, P>;

};
/**
* 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,
* ':id',
* {
* PUT: standardCRUDMethods.notFound, // do not update, instead returns 404
* DELETE: standardCRUDMethods.methodNotAllowed, // do not delete, instead returns 405
* },
* );
* ```
*/
export declare const standardCRUDMethods: {

@@ -311,32 +135,2 @@ readonly forbidden: (_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, ":id");
* ```
* This results in a /comments endpoint that correctly handles all REST methods over Comment.
* @param entity Entity type
* @param urlTemplateSuffix A suffix to be added to the Request URL (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.
* @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, urlTemplateSuffix: string, config?: {

@@ -343,0 +137,0 @@ customMethods?: Partial<CRUDMethods<T, ChiselEntityClass<T>, P>>;

587

lib/chisel.js

@@ -1,4 +0,1 @@

// SPDX-FileCopyrightText: © 2021 ChiselStrike <info@chiselstrike.com>
/// <reference types="./lib.deno_core.d.ts" />
/// <reference lib="dom" />
var OpType;

@@ -8,12 +5,8 @@ (function (OpType) {

OpType["Take"] = "Take";
OpType["Skip"] = "Skip";
OpType["ColumnsSelect"] = "ColumnsSelect";
OpType["RestrictionFilter"] = "RestrictionFilter";
OpType["PredicateFilter"] = "PredicateFilter";
OpType["Sort"] = "Sort";
OpType["ExpressionFilter"] = "ExpressionFilter";
OpType["SortBy"] = "SortBy";
})(OpType || (OpType = {}));
/**
* Base class for various Operators applicable on `ChiselCursor`. Each operator
* should extend this class and pass on its `type` identifier from the `OpType`
* enum.
*/
class Operator {

@@ -26,6 +19,14 @@ type;

}
containsType(opType) {
if (this.type == opType) {
return true;
}
else if (this.inner === undefined) {
return false;
}
else {
return this.inner.containsType(opType);
}
}
}
/**
* Specifies Entity whose elements are to be fetched.
*/
class BaseEntity extends Operator {

@@ -41,6 +42,2 @@ name;

}
/**
* Take operator takes first `count` elements from a collection.
* The rest is ignored.
*/
class Take extends Operator {

@@ -70,5 +67,22 @@ count;

}
/**
* Forces fetch of just the `columns` (fields) of a given entity.
*/
class Skip extends Operator {
count;
constructor(count, inner) {
super(OpType.Skip, inner);
this.count = count;
}
apply(iter) {
const count = this.count;
return {
[Symbol.asyncIterator]: async function* () {
let i = 0;
for await (const e of iter) {
if (++i > count) {
yield e;
}
}
},
};
}
}
class ColumnsSelect extends Operator {

@@ -97,6 +111,2 @@ columns;

}
/**
* PredicateFilter operator applies `predicate` on each element and keeps
* only those for which the `predicate` returns true.
*/
class PredicateFilter extends Operator {

@@ -121,24 +131,16 @@ predicate;

}
/**
* RestrictionFilter operator applies `restrictions` on each element
* and keeps only those where field value of a field, specified
* by restriction key, equals to restriction value.
*/
class RestrictionFilter extends Operator {
restrictions;
constructor(restrictions, inner) {
super(OpType.RestrictionFilter, inner);
this.restrictions = restrictions;
class ExpressionFilter extends Operator {
predicate;
expression;
constructor(predicate, expression, inner) {
super(OpType.ExpressionFilter, inner);
this.predicate = predicate;
this.expression = expression;
}
apply(iter) {
const restrictions = Object.entries(this.restrictions);
const predicate = this.predicate;
return {
[Symbol.asyncIterator]: async function* () {
for await (const arg of iter) {
verifyMatch: {
for (const [key, value] of restrictions) {
if (arg[key] != value) {
break verifyMatch;
}
}
if (predicate(arg)) {
yield arg;

@@ -151,15 +153,13 @@ }

}
/**
* 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;
class SortBy extends Operator {
key;
ascending;
constructor(key, ascending = true, inner) {
super(OpType.SortBy, inner);
this.key = key;
this.ascending = ascending;
}
apply(iter) {
const cmp = this.comparator;
const key = this.key;
const ord = this.ascending ? -1 : 1;
return {

@@ -172,3 +172,3 @@ [Symbol.asyncIterator]: async function* () {

elements.sort((lhs, rhs) => {
return cmp(lhs, rhs) ? -1 : 1;
return lhs[key] < rhs[key] ? ord : (-1) * ord;
});

@@ -182,3 +182,2 @@ for (const e of elements) {

}
/** ChiselCursor is a lazy iterator that will be used by ChiselStrike to construct an optimized query. */
export class ChiselCursor {

@@ -191,11 +190,11 @@ baseConstructor;

}
/** Force ChiselStrike to fetch just the `...columns` that are part of the colums list. */
select(...columns) {
return new ChiselCursor(this.baseConstructor, new ColumnsSelect(columns, this.inner));
}
/** Restricts this cursor to contain only at most `count` elements */
take(count) {
return new ChiselCursor(this.baseConstructor, new Take(count, this.inner));
}
// Common implementation for filter overloads.
skip(count) {
return new ChiselCursor(this.baseConstructor, new Skip(count, this.inner));
}
filter(arg1) {

@@ -206,32 +205,27 @@ if (typeof arg1 == "function") {

else {
return new ChiselCursor(this.baseConstructor, new RestrictionFilter(arg1, this.inner));
const restrictions = arg1;
const expr = restrictionsToFilterExpr(restrictions);
if (expr === undefined) {
return this;
}
const predicate = (arg) => {
for (const key in restrictions) {
if (restrictions[key] === undefined) {
continue;
}
if (arg[key] != restrictions[key]) {
return false;
}
}
return true;
};
return new ChiselCursor(this.baseConstructor, new ExpressionFilter(predicate, expr, this.inner));
}
}
/**
* 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));
__filterWithExpression(predicate, expression) {
return new ChiselCursor(this.baseConstructor, new ExpressionFilter(predicate, expression, 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));
return new ChiselCursor(this.baseConstructor, new SortBy(key, ascending, this.inner));
}
/** Executes the function `func` for each element of this cursor. */
async forEach(func) {

@@ -242,6 +236,2 @@ for await (const t of this) {

}
/** Converts this cursor to an Array.
*
* Use this with caution as the result set can be very big.
* It is recommended that you take() first to cap the maximum number of elements. */
async toArray() {

@@ -254,4 +244,3 @@ const arr = [];

}
/** ChiselCursor implements asyncIterator, meaning you can use it in any asynchronous context. */
async *[Symbol.asyncIterator]() {
[Symbol.asyncIterator]() {
let iter = this.makeTransformedQueryIter(this.inner);

@@ -261,12 +250,4 @@ if (iter === undefined) {

}
for await (const it of iter) {
yield it;
}
return iter[Symbol.asyncIterator]();
}
/** Performs recursive descent via Operator.inner examining the whole operator
* 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.
*/
makeTransformedQueryIter(op) {

@@ -283,3 +264,3 @@ if (op.type == OpType.BaseEntity) {

}
else if (op.type == OpType.PredicateFilter || op.type == OpType.Sort) {
else if (op.type == OpType.PredicateFilter) {
iter = this.makeQueryIter(op.inner);

@@ -293,9 +274,11 @@ return op.apply(iter);

makeQueryIter(op) {
const ctor = this.containsSelect(op) ? undefined : this.baseConstructor;
const ctor = op.containsType(OpType.ColumnsSelect)
? undefined
: this.baseConstructor;
return {
[Symbol.asyncIterator]: async function* () {
const rid = Deno.core.opSync("chisel_relational_query_create", op);
const rid = Deno.core.opSync("op_chisel_relational_query_create", op, requestContext);
try {
while (true) {
const properties = await Deno.core.opAsync("chisel_relational_query_next", rid);
const properties = await Deno.core.opAsync("op_chisel_query_next", rid);
if (properties == undefined) {

@@ -320,16 +303,18 @@ break;

}
/** Recursively examines operator chain searching for ColumnsSelect operator.
* Returns true if found, false otherwise.
*/
containsSelect(op) {
if (op.type == OpType.ColumnsSelect) {
return true;
}
else if (op.inner === undefined) {
return false;
}
else {
return this.containsSelect(op.inner);
}
}
export class ChiselRequest extends Request {
version;
endpoint;
pathParams;
user;
constructor(input, init, version, endpoint, pathParams, user) {
super(input, init);
this.version = version;
this.endpoint = endpoint;
this.pathParams = pathParams;
this.user = user;
}
pathComponents() {
return this.pathParams.split("/").filter((n) => n.length != 0);
}
}

@@ -340,39 +325,4 @@ export function chiselIterator(type) {

}
/** ChiselEntity is a class that ChiselStrike user-defined entities are expected to extend.
*
* It provides properties that are inherent to a ChiselStrike entity, like an id, and static
* methods that can be used to obtain a `ChiselCursor`.
*/
export class ChiselEntity {
/** UUID identifying this object. */
id;
/**
* Builds a new entity.
*
* @param properties The properties of the created entity. If more than one property
* is passed, the expected order of assignment is the same as Object.assign.
*
* @example
* ```typescript
* export class User extends ChiselEntity {
* username: string,
* email: string,
* }
* // Create an entity from object literal:
* const user = User.build({ username: "alice", email: "alice@example.com" });
* // Create an entity from JSON:
* const userJson = JSON.parse('{"username": "alice", "email": "alice@example.com"}');
* const anotherUser = User.build(userJson);
*
* // Create an entity from different JSON objects:
* const otherUserJson = JSON.parse('{"username": "alice"}, {"email": "alice@example.com"}');
* const yetAnotherUser = User.build(userJson);
*
* // now optionally save them to the backend
* await user.save();
* await anotherUser.save();
* await yetAnotherUser.save();
* ```
* @returns The persisted entity with given properties and the `id` property set.
*/
static build(...properties) {

@@ -383,8 +333,8 @@ const result = new this();

}
/** saves the current object into the backend */
async save() {
const jsonIds = await Deno.core.opAsync("chisel_store", {
ensureNotGet();
const jsonIds = await Deno.core.opAsync("op_chisel_store", {
name: this.constructor.name,
value: this,
});
}, requestContext);
function backfillIds(this_, jsonIds) {

@@ -403,10 +353,6 @@ for (const [fieldName, value] of Object.entries(jsonIds)) {

}
/** Returns a `ChiselCursor` containing all elements of type T known to ChiselStrike.
*
* Note that `ChiselCursor` is a lazy iterator, so this doesn't mean a query will be generating fetching all elements at this point. */
static cursor() {
return chiselIterator(this);
}
/** Restricts this iterator to contain just the objects that match the `Partial` object `restrictions`. */
static async findMany(restrictions, take) {
static async findAll(take) {
let it = chiselIterator(this);

@@ -416,7 +362,11 @@ if (take) {

}
return await it.filter(restrictions).toArray();
return await it.toArray();
}
/** Returns a single object that matches the `Partial` object `restrictions` passed as its parameter.
*
* If more than one match is found, any is returned. */
static async findMany(restrictions, take) {
let it = chiselIterator(this).filter(restrictions);
if (take) {
it = it.take(take);
}
return await it.toArray();
}
static async findOne(restrictions) {

@@ -429,82 +379,57 @@ const it = chiselIterator(this).filter(restrictions).take(1);

}
/**
* Deletes all entities that match the `restrictions` object.
*
* @example
* ```typescript
* export class User extends ChiselEntity {
* username: string,
* email: string,
* }
* const user = User.build({ username: "alice", email: "alice@example.com" });
* await user.save();
*
* await User.delete({ email: "alice@example.com"})
* ```
*/
static async delete(restrictions) {
await Deno.core.opAsync("chisel_entity_delete", {
type_name: this.name,
restrictions: restrictions,
});
ensureNotGet();
await Deno.core.opAsync("op_chisel_entity_delete", {
typeName: this.name,
filterExpr: restrictionsToFilterExpr(restrictions),
}, requestContext);
}
/**
* 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 Comment.crud();
* ```
*
* This results in a /comments endpoint that correctly handles all REST methods over Comment. The default endpoints are:
*
* * **POST:**
* * `/comments` Creates a new object. Payload is a JSON with the properties of `Comment` as keys.
*
* * **GET:**
* * `/comments` returns an array with all elements (use carefully in datasets expected to be large)
* * `/comments?f={key:val}` returns all elements that match the filter specified by the json object given as search param.
* * `/comments/:id` returns the element with the given ID.
*
* * **DELETE:**
* * `/comments/:id` deletes the element with the given ID.
* * `/comments?f={key:val}` deletes all elements that match the filter specified by the json object given as search param.
* * `/comments?f={}` deletes all elements
*
* * **PUT:**
* * `/comments/:id` overwrites the element with the given ID. Payload is a JSON with the properties of `Comment` as keys
*
* If you need more control over which method to generate and their behavior, see the top-level `crud()` function
*
* @returns A request-handling function suitable as a default export in an endpoint.
*/
static crud(_ignored) {
return crud(this, "");
}
static async create(...properties) {
const result = new this();
Object.assign(result, ...properties);
await result.save();
return result;
}
}
function restrictionsToFilterExpr(restrictions) {
let expr = undefined;
for (const key in restrictions) {
if (restrictions[key] === undefined) {
continue;
}
const cmpExpr = {
exprType: "Binary",
left: {
exprType: "Property",
object: { exprType: "Parameter", position: 0 },
property: key,
},
op: "Eq",
right: {
exprType: "Literal",
value: restrictions[key],
},
};
if (expr === undefined) {
expr = cmpExpr;
}
else {
expr = {
exprType: "Binary",
left: cmpExpr,
op: "And",
right: expr,
};
}
}
return expr;
}
export class OAuthUser extends ChiselEntity {
username = undefined;
}
export function buildReadableStreamForBody(rid) {
return new ReadableStream({
async pull(controller) {
const chunk = await Deno.core.opAsync("chisel_read_body", rid);
if (chunk) {
controller.enqueue(chunk);
}
else {
controller.close();
Deno.core.opSync("op_close", rid);
}
},
cancel() {
Deno.core.opSync("op_close", rid);
},
});
}
export function getSecret(key) {
const secret = Deno.core.opSync("chisel_get_secret", key);
const secret = Deno.core.opSync("op_chisel_get_secret", key);
if (secret === undefined || secret === null) {

@@ -516,7 +441,6 @@ return undefined;

export function responseFromJson(body, status = 200) {
// https://fetch.spec.whatwg.org/#null-body-status
const isNullBody = (status) => {
return status == 101 || status == 204 || status == 205 || status == 304;
};
const json = isNullBody(status) ? null : JSON.stringify(body);
const json = isNullBody(status) ? null : JSON.stringify(body, null, 2);
return new Response(json, {

@@ -531,12 +455,9 @@ status: status,

return (_target, _propertyName) => {
// chisel-decorator, no content
};
}
export function unique() {
// chisel-decorator, no content
export function unique(_target, _name) {
}
/** Returns the currently logged-in user or null if no one is logged in. */
export async function loggedInUser() {
const id = await Deno.core.opAsync("chisel_user", {});
if (id == null) {
const id = requestContext.userId;
if (id === undefined) {
return undefined;

@@ -546,30 +467,12 @@ }

}
// 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.
function ensureNotGet() {
if (requestContext.method === "GET") {
throw new Error("Mutating the backend is not allowed during GET");
}
}
export const requestContext = {
path: "",
method: "",
apiVersion: "",
};
export function regExParamParse(str, loose) {

@@ -602,32 +505,2 @@ let tmp, pattern = "";

}
/**
* 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) {

@@ -647,10 +520,2 @@ const { pattern, keys: keysOrFalse } = regExParamParse(pathTemplate, loose);

}
/**
* 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) {

@@ -660,13 +525,48 @@ const pathParser = createPathParser(pathTemplate, loose);

}
async function fetchEntitiesCrud(type, url) {
const iter = {
[Symbol.asyncIterator]: async function* () {
const rid = Deno.core.opSync("op_chisel_crud_query_create", {
typeName: type.name,
url,
}, requestContext);
try {
while (true) {
const properties = await Deno.core.opAsync("op_chisel_query_next", rid);
if (properties == undefined) {
break;
}
const result = new type();
Object.assign(result, properties);
yield result;
}
}
finally {
Deno.core.opSync("op_close", rid);
}
},
};
const arr = [];
for await (const t of iter) {
arr.push(t);
}
return arr;
}
async function deleteEntitiesCrud(type, url) {
await Deno.core.opAsync("op_chisel_crud_delete", {
typeName: type.name,
url,
}, requestContext);
}
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);
if (id) {
const u = await entity.findOne({ id });
return createResponse(u ?? "Not found", u ? 200 : 404);
}
const u = await entity.findOne({ id });
return createResponse(u ?? "Not found", u ? 200 : 404);
else {
return createResponse(await fetchEntitiesCrud(entity, url.href), 200);
}
},
// 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) => {

@@ -678,3 +578,2 @@ const u = entity.build(await req.json());

},
// Updates and returns the entity matching params.id (which must be set) from the `req` payload.
PUT: async (entity, req, params, _url, createResponse) => {

@@ -690,3 +589,2 @@ const { id } = params;

},
// 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) => {

@@ -698,27 +596,8 @@ const { id } = params;

}
const restrictions = getEntityFiltersFromURL(entity, url);
if (restrictions) {
await entity.delete(restrictions);
return createResponse(`Deleted entities matching ${JSON.stringify(restrictions)}`, 200);
else {
await deleteEntitiesCrud(entity, url.href);
return createResponse(`Deleted entities matching ${url.search}`, 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,
* ':id',
* {
* PUT: standardCRUDMethods.notFound, // do not update, instead returns 404
* DELETE: standardCRUDMethods.methodNotAllowed, // do not delete, instead returns 405
* },
* );
* ```
*/
export const standardCRUDMethods = {

@@ -729,39 +608,8 @@ forbidden: (_entity, _req, _params, _url, createResponse) => Promise.resolve(createResponse("Forbidden", 403)),

};
/**
* 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, ":id");
* ```
* This results in a /comments endpoint that correctly handles all REST methods over Comment.
* @param entity Entity type
* @param urlTemplateSuffix A suffix to be added to the Request URL (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.
* @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, urlTemplateSuffix, config) {
const endpointPath = Deno.core.opSync("chisel_current_path");
const pathTemplateRaw = "/:chiselVersion" + endpointPath + "/" +
const pathTemplateRaw = "/:chiselVersion" + requestContext.path + "/" +
(urlTemplateSuffix.includes(":id")
? urlTemplateSuffix
: `${urlTemplateSuffix}/:id`);
const pathTemplate = pathTemplateRaw.replace(/\/+/g, "/"); // in case we end up with foo///bar somehow.
const pathTemplate = pathTemplateRaw.replace(/\/+/g, "/");
const defaultCreateResponse = config?.defaultCreateResponse ||

@@ -776,3 +624,3 @@ responseFromJson;

return (req) => {
const methodName = req.method; // assume valid, will be handled gracefully
const methodName = req.method;
const createResponse = config?.createResponses?.[methodName] ||

@@ -789,2 +637,1 @@ defaultCreateResponse;

}
// TODO: END: this should be in another file: crud.ts
{
"name": "@chiselstrike/api",
"version": "0.8.4",
"version": "0.9.0",
"main": "lib/chisel.js",

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

"license": "Proprietary",
"prepare": "npm run build",
"scripts": {
"prepare": "npm run build",
"build": "rimraf ./lib && tsc --noResolve && cp -La ../../api/src/lib.deno_core.d.ts lib/"
"build": "true"
},
"dependencies": {
"typescript": "^4.5.4"
},
"devDependencies": {
"rimraf": "^3.0.2"
},
"files": [

@@ -21,0 +13,0 @@ "/lib"