Skip to content

Commit

Permalink
Minor crud adjustments (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
G4brym authored Dec 17, 2024
1 parent 8a50429 commit 2d353f4
Show file tree
Hide file tree
Showing 17 changed files with 299 additions and 17 deletions.
3 changes: 0 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint
npm run test
7 changes: 7 additions & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ nav:
- advanced-user-guide/openapi-schema-customizations.md
- advanced-user-guide/streaming-responses.md
- advanced-user-guide/utilities.md
- Generic CRUD endpoints:
- generic-cruds/getting-started-with-cruds.md
- generic-cruds/create-endpoint.md
- generic-cruds/read-endpoint.md
- generic-cruds/update-endpoint.md
- generic-cruds/delete-endpoint.md
- generic-cruds/list-endpoint.md
markdown_extensions:
- toc:
permalink: true
Expand Down
95 changes: 95 additions & 0 deletions docs/pages/generic-cruds/create-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
The create class, allows you to specify witch fields the user is able to submit, you can set this in the `fields` parameter of the `_meta` variable.

Methods available:

- `getSchema();` this allows you to live change your openapi schema
- `async getObject(): Promise<O<typeof this.meta>>;` this allows you to customize the object after validation
- `async before(data: O<typeof this.meta>): Promise<O<typeof this.meta>>;` add custom logic before creating the object
- `async create(data: O<typeof this.meta>): Promise<O<typeof this.meta>>;` create the object in the database
- `async after(data: O<typeof this.meta>): Promise<O<typeof this.meta>>;` add custom logic after creating the object
- `async handle(...args);` overwrite the response or the order of the method calls

Call order:

```ts
async function handle(...args) {
let obj = await this.getObject();

obj = await this.before(obj);

obj = await this.create(obj);

obj = await this.after(obj);

return {
success: true,
result: this.meta.model.serializer(obj as object),
};
}
```

Example router registration:
```ts
// Primary keys not present in the url are expected in the request body
router.post("/gateways/:gateway_id/evaluations", CreateEvaluation);
```


## D1 Endpoint
The D1 create adapter exposes 3 additional configuration:

- `dbName`: your D1 binding name, by default is set to "DB"
- `logger`: this allows you to get logger messages for validation errors, objects created and other;
- `constraintsMessages`: this allows you to customize database constraint error messages

Here is an example of a simple create endpoint, using the example model defined in the [getting started with cruds example](./getting-started-with-cruds.md):
```ts
import { D1CreateEndpoint } from 'chanfana'

export class CreateEvaluation extends D1CreateEndpoint {
_meta = {
model: evaluationModel,
fields: evaluationModel.schema
.pick({
gateway_id: true,
name: true,
})
};
}
```

### Customizing constraints messages
In order to have custom error messages for database constraint error, just define the field inside your class
with an object where the key is a comma separated list of the fields in the constraint and the value is a exception of the type

```ts
import { D1CreateEndpoint, InputValidationException } from 'chanfana'

export class CreateEvaluation extends D1CreateEndpoint {
_meta = {...}

constraintsMessages = {
"evaluations.name, evaluations.gateway_id": new InputValidationException(
"An object with this name already exists",
["body", "name"],
),
};
}
```

## Bring your own Database Endpoint

In our to use Bring your own Database, just extend your class from the base `CreateEndpoint` and overwrite the `create` method.

```ts
import { CreateEndpoint } from 'chanfana'

export class MyCustomCreate extends CreateEndpoint {
async create(data: O<typeof this.meta>): Promise<O<typeof this.meta>> {
// TODO: insert my object here, data is already validated

// Return the insert object
return data;
}
}
```
97 changes: 97 additions & 0 deletions docs/pages/generic-cruds/delete-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
Methods available:

- `getSchema();` this allows you to live change your openapi schema
- `async getObject(filters: Filters): Promise<O<typeof this.meta> | null>;` this parses the filters from the query and url parameters
- `async getObject(): Promise<O<typeof this.meta>>;` this receives the filters and returns the object that will be deleted
- `async before(oldObj: O<typeof this.meta>, filters: Filters): Promise<Filters>;` add custom logic before deleting the object, allows you to inject additional sql conditions
- `async after(data: O<typeof this.meta>): Promise<O<typeof this.meta>>;` add custom logic after deleting the object
- `async delete(oldObj: O<typeof this.meta>, filters: Filters): Promise<O<typeof this.meta> | null>;` deletes the object
- `async handle(...args);` overwrite the response or the order of the method calls

Call order:

```ts
async function handle(...args) {
let filters = await this.getFilters();

const oldObj = await this.getObject(filters);

if (oldObj === null) {
throw new NotFoundException();
}

filters = await this.before(oldObj, filters);

let obj = await this.delete(oldObj, filters);

if (obj === null) {
throw new NotFoundException();
}

obj = await this.after(obj);

return {
success: true,
result: this.meta.model.serializer(obj),
};
}
```

Example router registration:
```ts
// Primary keys not present in the url are expected in the request body
router.delete("/gateways/:gateway_id/evaluations/:id", DeleteEvaluation);
```

## D1 Endpoint
The D1 delete adapter exposes 2 additional configuration:

- `dbName`: your D1 binding name, by default is set to "DB"
- `logger`: this allows you to get logger messages for validation errors, objects deleted and other;

Here is an example of a simple delete endpoint, using the example model defined in the [getting started with cruds example](./getting-started-with-cruds.md):
```ts
import { D1DeleteEndpoint } from 'chanfana'

export class DeleteEvaluation extends D1DeleteEndpoint {
_meta = {
model: evaluationModel
};
}
```

## Bring your own Database Endpoint

In our to use Bring your own Database, just extend your class from the base `DeleteEndpoint` and overwrite the `delete` and `getObject` methods.

```ts
import { DeleteEndpoint } from 'chanfana'

export class MyCustomDelete extends DeleteEndpoint {
async getObject(filters: Filters): Promise<O<typeof this.meta> | null> {
// TODO: retreive my object using filters

if (myObject ===undefined) {
// if there is no object matching the filters, return null to throw a http 404
return null;
}

// Object found, return it
return myobject
}

async delete(oldObj: O<typeof this.meta>, filters: Filters): Promise<O<typeof this.meta> | null> {
// the variable oldObj will contain the object from getObject

// TODO: actually delete the object

if (result.meta.changes === 0) {
// If there were no sql changes, you can return null to throw a http 404
return null;
}

// Return deleted object, this will be included in the endpoint response
return oldObj;
}
}
```
83 changes: 83 additions & 0 deletions docs/pages/generic-cruds/getting-started-with-cruds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
chanfana now has built-in classes for the common CRUD operation.

This allows you to get your work done faster, just create a new class that extends the operation you want and define your model.

Currently, there is support for `D1 Databases` and `Bring your own database`.

## Model definition
A chanfana model is compose of the following fields:

- `tableName`: the target table name
- `schema`: a Zod object containing all your database fields
- `primaryKeys`: an array of the primary keys of your table, this is used for updated, deletes and other operations
- `serializer`: optionally, a serializer function, this is useful when you don't want to show all fields in the response or convert something
- `serializerSchema`: optionally, the Zod schema output of the serializer function defined above

Start by defining your database model in this format:
```ts
export const Model = {
tableName: string,
schema: AnyZodObject,
primaryKeys: Array<string>,
serializer: (obj: object) => object, // Optional
serializerSchema: AnyZodObject, // Optional
};
```

!!! note

You will be able to customize what fields are applied to each operation inside each endpoint class


Here's an example model
```ts
import { z } from "zod";

export function EvaluationSerializer(obj) {
return {
id: obj.id,
gateway_id: obj.gateway_id,
name: obj.name,
processed: Boolean(obj.processed),
total_logs: obj.total_logs,
created_at: obj.created_at,
modified_at: obj.modified_at,
};
}

export const evaluation = z.object({
id: z.string(),
gateway_id: z.string(),
name: z.string(),
created_at: z.string().datetime(),
modified_at: z.string().datetime(),
processed: z.boolean(),
total_logs: z.number(),
});

export const evaluationModel = {
schema: evaluation,
primaryKeys: ["id", "gateway_id"],
tableName: "evaluations",
serializer: EvaluationSerializer,
serializerSchema: evaluation
.pick({
id: true,
gateway_id: true,
name: true,
processed: true,
total_logs: true,
results: true,
created_at: true,
modified_at: true,
})
};
```

Learn more about each operation here:

- [create endpoint](./create-endpoint.md)
- [read endpoint](./read-endpoint.md)
- [update endpoint](./update-endpoint.md)
- [delete endpoint](./delete-endpoint.md)
- [list endpoint](./list-endpoint.md)
1 change: 1 addition & 0 deletions docs/pages/generic-cruds/list-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TODO
1 change: 1 addition & 0 deletions docs/pages/generic-cruds/read-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TODO
1 change: 1 addition & 0 deletions docs/pages/generic-cruds/update-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TODO
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "chanfana",
"version": "2.5.1",
"version": "2.5.2",
"description": "OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!",
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class CreateEndpoint<HandleArgs extends Array<object> = Array<object>> ex
description: "Returns the created Object",
...contentJson({
success: Boolean,
result: this.meta.model.serializerObject,
result: this.meta.model.serializerSchema,
}),
...this.schema?.responses?.[200],
},
Expand Down
4 changes: 2 additions & 2 deletions src/endpoints/d1/fetch.ts → src/endpoints/d1/read.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ApiException } from "../../exceptions";
import { FetchEndpoint } from "../fetch";
import { ReadEndpoint } from "../read";
import type { ListFilters, Logger, O } from "../types";

export class D1FetchEndpoint<HandleArgs extends Array<object> = Array<object>> extends FetchEndpoint<HandleArgs> {
export class D1ReadEndpoint<HandleArgs extends Array<object> = Array<object>> extends ReadEndpoint<HandleArgs> {
dbName = "DB";
logger?: Logger;

Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class DeleteEndpoint<HandleArgs extends Array<object> = Array<object>> ex
description: "Returns the Object if it was successfully deleted",
...contentJson({
success: Boolean,
result: this.meta.model.serializerObject,
result: this.meta.model.serializerSchema,
}),
...this.schema?.responses?.[200],
},
Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class ListEndpoint<HandleArgs extends Array<object> = Array<object>> exte
description: "List objects",
...contentJson({
success: Boolean,
result: [this.meta.model.serializerObject],
result: [this.meta.model.serializerSchema],
}),
...this.schema?.responses?.[200],
},
Expand Down
4 changes: 2 additions & 2 deletions src/endpoints/fetch.ts → src/endpoints/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NotFoundException } from "../exceptions";
import { OpenAPIRoute } from "../route";
import { type FilterCondition, type ListFilters, MetaGenerator, type MetaInput, type O } from "./types";

export class FetchEndpoint<HandleArgs extends Array<object> = Array<object>> extends OpenAPIRoute<HandleArgs> {
export class ReadEndpoint<HandleArgs extends Array<object> = Array<object>> extends OpenAPIRoute<HandleArgs> {
// @ts-ignore
_meta: MetaInput;

Expand Down Expand Up @@ -34,7 +34,7 @@ export class FetchEndpoint<HandleArgs extends Array<object> = Array<object>> ext
description: "Returns a single object if found",
...contentJson({
success: Boolean,
result: this.meta.model.serializerObject,
result: this.meta.model.serializerSchema,
}),
...this.schema?.responses?.[200],
},
Expand Down
6 changes: 3 additions & 3 deletions src/endpoints/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ export type Model = {
schema: AnyZodObject;
primaryKeys: Array<string>;
serializer?: (obj: object) => object;
serializerObject?: AnyZodObject;
serializerSchema?: AnyZodObject;
};

export type ModelComplete = SetRequired<Model, "serializer" | "serializerObject">;
export type ModelComplete = SetRequired<Model, "serializer" | "serializerSchema">;

export type MetaInput = {
model: Model;
Expand All @@ -57,7 +57,7 @@ export function MetaGenerator(meta: MetaInput) {
fields: meta.fields ?? meta.model.schema,
model: {
serializer: (obj: any): any => obj,
serializerObject: meta.model.schema,
serializerSchema: meta.model.schema,
...meta.model,
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class UpdateEndpoint<HandleArgs extends Array<object> = Array<object>> ex
description: "Returns the updated Object",
...contentJson({
success: Boolean,
result: this.meta.model.serializerObject,
result: this.meta.model.serializerSchema,
}),
...this.schema?.responses?.[200],
},
Expand Down
Loading

0 comments on commit 2d353f4

Please sign in to comment.