From 5ba6d9519259096fec7688c9e498857b5b88dff3 Mon Sep 17 00:00:00 2001 From: Gabriel Massadas <5445926+G4brym@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:51:44 +0000 Subject: [PATCH] Update generic cruds to use generic d1 binding methods (#212) --- .../create-pullrequest-prerelease.yml | 4 +- biome.json | 62 ++++---- package.json | 140 +++++++++--------- src/adapters/hono.ts | 4 + src/adapters/ittyRouter.ts | 4 + src/endpoints/create.ts | 10 -- src/endpoints/d1/create.ts | 54 +++++++ src/endpoints/d1/delete.ts | 78 ++++++++++ src/endpoints/d1/fetch.ts | 36 +++++ src/endpoints/d1/list.ts | 76 ++++++++++ src/endpoints/d1/update.ts | 84 +++++++++++ src/endpoints/orms/workers-qb/create.ts | 38 ----- src/endpoints/orms/workers-qb/delete.ts | 74 --------- src/endpoints/orms/workers-qb/fetch.ts | 26 ---- src/endpoints/orms/workers-qb/list.ts | 76 ---------- src/endpoints/orms/workers-qb/update.ts | 78 ---------- src/endpoints/types.ts | 9 ++ src/exceptions.ts | 2 +- src/index.ts | 10 +- src/openapi.ts | 4 + src/route.ts | 3 +- tsconfig.json | 46 +++--- 22 files changed, 478 insertions(+), 440 deletions(-) create mode 100644 src/endpoints/d1/create.ts create mode 100644 src/endpoints/d1/delete.ts create mode 100644 src/endpoints/d1/fetch.ts create mode 100644 src/endpoints/d1/list.ts create mode 100644 src/endpoints/d1/update.ts delete mode 100644 src/endpoints/orms/workers-qb/create.ts delete mode 100644 src/endpoints/orms/workers-qb/delete.ts delete mode 100644 src/endpoints/orms/workers-qb/fetch.ts delete mode 100644 src/endpoints/orms/workers-qb/list.ts delete mode 100644 src/endpoints/orms/workers-qb/update.ts diff --git a/.github/workflows/create-pullrequest-prerelease.yml b/.github/workflows/create-pullrequest-prerelease.yml index d5431a0..9add138 100644 --- a/.github/workflows/create-pullrequest-prerelease.yml +++ b/.github/workflows/create-pullrequest-prerelease.yml @@ -37,11 +37,11 @@ jobs: You can install this latest build in your project with: ```sh - npm install --save https://prerelease-registry.devprod.cloudflare.dev/chanfana/runs/${{ github.workflow_run.id }}/npm-package-chanfana-${{ github.event.number }} + npm install --save https://prerelease-registry.devprod.cloudflare.dev/chanfana/runs/${{ github.run_id }}/npm-package-chanfana-${{ github.event.number }} ``` Or you can immediately run this with `npx`: ```sh - npx https://prerelease-registry.devprod.cloudflare.dev/chanfana/runs/${{ github.workflow_run.id }}/npm-package-chanfana-${{ github.event.number }} + npx https://prerelease-registry.devprod.cloudflare.dev/chanfana/runs/${{ github.run_id }}/npm-package-chanfana-${{ github.event.number }} ``` diff --git a/biome.json b/biome.json index 961e9c5..416ca70 100644 --- a/biome.json +++ b/biome.json @@ -1,46 +1,46 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false, - "ignore": ["dist", "docs", "example"] - }, - "formatter": { + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": ["dist", "docs", "example"] + }, + "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 120 - }, - "organizeImports": { - "enabled": true - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, + }, + "organizeImports": { + "enabled": true + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, "complexity": { "noBannedTypes": "off", "noThisInStatic": "off" }, "suspicious": { - "noExplicitAny": "off", - "noImplicitAnyLet": "off" - }, - "performance": { - "noAccumulatingSpread": "off" - }, + "noExplicitAny": "off", + "noImplicitAnyLet": "off" + }, + "performance": { + "noAccumulatingSpread": "off" + }, "style": { "noParameterAssign": "off" } } - } + } } diff --git a/package.json b/package.json index f6dc84c..ccf093e 100644 --- a/package.json +++ b/package.json @@ -1,74 +1,70 @@ { - "name": "chanfana", - "version": "2.4.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", - "types": "dist/index.d.ts", - "files": [ - "dist", - "LICENSE", - "README.md" - ], - "scripts": { - "prepare": "husky", - "build": "rm -rf dist/ && tsup src/index.ts --format cjs,esm --dts --config tsconfig.json", - "lint": "npx @biomejs/biome check src/ tests/ || (npx @biomejs/biome check --write src/ tests/; exit 1)", - "test": "vitest run --root tests", - "deploy-docs": "cd docs && mkdocs build && wrangler pages deploy site --project-name chanfana --branch main" - }, - "keywords": [ - "cloudflare", - "worker", - "workers", - "serverless", - "cloudflare workers", - "router", - "openapi", - "swagger", - "openapi generator", - "cf", - "optional", - "middleware", - "parameters", - "typescript", - "npm", - "package", - "cjs", - "esm", - "umd", - "typed" - ], - "author": "Gabriel Massadas (https://github.com/g4brym)", - "license": "MIT", - "homepage": "https://chanfana.pages.dev", - "repository": { - "type": "git", - "url": "https://github.com/cloudflare/chanfana.git" - }, - "bugs": { - "url": "https://github.com/cloudflare/chanfana/issues" - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@cloudflare/vitest-pool-workers": "^0.5.26", - "@cloudflare/workers-types": "4.20241127.0", - "@types/js-yaml": "^4.0.9", - "@types/node": "22.10.1", - "@types/service-worker-mock": "^2.0.1", - "hono": "4.6.12", - "husky": "9.1.7", - "itty-router": "5.0.18", - "tsup": "8.3.5", - "typescript": "5.7.2", - "vitest": "2.1.6", - "vitest-openapi": "^1.0.3", - "wrangler": "3.91.0" - }, - "dependencies": { - "@asteasolutions/zod-to-openapi": "^7.2.0", - "js-yaml": "^4.1.0", - "openapi3-ts": "^4.4.0", - "zod": "^3.23.8" - } + "name": "chanfana", + "version": "2.4.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", + "types": "dist/index.d.ts", + "files": ["dist", "LICENSE", "README.md"], + "scripts": { + "prepare": "husky", + "build": "rm -rf dist/ && tsup src/index.ts --format cjs,esm --dts --config tsconfig.json", + "lint": "npx @biomejs/biome check src/ tests/ || (npx @biomejs/biome check --write src/ tests/; exit 1)", + "test": "vitest run --root tests", + "deploy-docs": "cd docs && mkdocs build && wrangler pages deploy site --project-name chanfana --branch main" + }, + "keywords": [ + "cloudflare", + "worker", + "workers", + "serverless", + "cloudflare workers", + "router", + "openapi", + "swagger", + "openapi generator", + "cf", + "optional", + "middleware", + "parameters", + "typescript", + "npm", + "package", + "cjs", + "esm", + "umd", + "typed" + ], + "author": "Gabriel Massadas (https://github.com/g4brym)", + "license": "MIT", + "homepage": "https://chanfana.pages.dev", + "repository": { + "type": "git", + "url": "https://github.com/cloudflare/chanfana.git" + }, + "bugs": { + "url": "https://github.com/cloudflare/chanfana/issues" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@cloudflare/vitest-pool-workers": "^0.5.26", + "@cloudflare/workers-types": "4.20241127.0", + "@types/js-yaml": "^4.0.9", + "@types/node": "22.10.1", + "@types/service-worker-mock": "^2.0.1", + "hono": "4.6.12", + "husky": "9.1.7", + "itty-router": "5.0.18", + "tsup": "8.3.5", + "typescript": "5.7.2", + "vitest": "2.1.6", + "vitest-openapi": "^1.0.3", + "wrangler": "3.91.0" + }, + "dependencies": { + "@asteasolutions/zod-to-openapi": "^7.2.0", + "js-yaml": "^4.1.0", + "openapi3-ts": "^4.4.0", + "zod": "^3.23.8" + } } diff --git a/src/adapters/hono.ts b/src/adapters/hono.ts index f1d6562..f6b8254 100644 --- a/src/adapters/hono.ts +++ b/src/adapters/hono.ts @@ -15,6 +15,10 @@ export class HonoOpenAPIHandler extends OpenAPIHandler { getUrlParams(args: any[]): Record { return args[0].req.param(); } + + getBindings(args: any[]): Record { + return args[0].env; + } } export function fromHono(router: M, options?: RouterOptions): M & HonoOpenAPIRouterType { diff --git a/src/adapters/ittyRouter.ts b/src/adapters/ittyRouter.ts index 71a5b09..09b5f50 100644 --- a/src/adapters/ittyRouter.ts +++ b/src/adapters/ittyRouter.ts @@ -9,6 +9,10 @@ export class IttyRouterOpenAPIHandler extends OpenAPIHandler { getUrlParams(args: any[]): Record { return args[0].params; } + + getBindings(args: any[]): Record { + return args[1]; + } } export function fromIttyRouter(router: M, options?: RouterOptions): M & OpenAPIRouterType { diff --git a/src/endpoints/create.ts b/src/endpoints/create.ts index 6d600a1..c7d12cb 100644 --- a/src/endpoints/create.ts +++ b/src/endpoints/create.ts @@ -11,8 +11,6 @@ export class CreateEndpoint = Array> ex return MetaGenerator(this._meta); } - defaultValues?: Record any>; // TODO: move this into model - getSchema() { const bodyParameters = this.meta.fields.omit( (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), @@ -55,14 +53,6 @@ export class CreateEndpoint = Array> ex newData[param] = (data.params as any)[param]; } - if (this.defaultValues) { - for (const [key, value] of Object.entries(this.defaultValues)) { - if (newData[key] === undefined) { - newData[key] = value(); - } - } - } - return newData; } diff --git a/src/endpoints/d1/create.ts b/src/endpoints/d1/create.ts new file mode 100644 index 0000000..c29f845 --- /dev/null +++ b/src/endpoints/d1/create.ts @@ -0,0 +1,54 @@ +import { ApiException, InputValidationException } from "../../exceptions"; +import { CreateEndpoint } from "../create"; +import type { Logger, O } from "../types"; + +export class D1CreateEndpoint = Array> extends CreateEndpoint { + dbName = "DB"; + logger?: Logger; + + getDBBinding(): D1Database { + const env = this.params.router.getBindings(this.args); + if (env[this.dbName] === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); + } + + if (env[this.dbName].prepare === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); + } + + return env[this.dbName]; + } + + async create(data: O): Promise> { + let inserted; + try { + const result = await this.getDBBinding() + .prepare( + `INSERT INTO ${this.meta.model.tableName} (${Object.keys(data).join(", ")}) VALUES (${Object.values(data) + .map(() => "?") + .join(", ")}) RETURNING *`, + ) + .bind(...Object.values(data)) + .all(); + + inserted = result.results[0] as O; + } catch (e: any) { + if (this.logger) + this.logger.error(`Caught exception while trying to create ${this.meta.model.tableName}: ${e.message}`); + if (e.message.includes("UNIQUE constraint failed")) { + if (e.message.includes(this.meta.model.tableName) && e.message.includes(this.meta.model.primaryKeys[0])) { + throw new InputValidationException(`An object with this ${this.meta.model.primaryKeys[0]} already exists`, [ + "body", + this.meta.model.primaryKeys[0], + ]); + } + } + + throw new ApiException(e.message); + } + + if (this.logger) this.logger.log(`Successfully created ${this.meta.model.tableName}`); + + return inserted; + } +} diff --git a/src/endpoints/d1/delete.ts b/src/endpoints/d1/delete.ts new file mode 100644 index 0000000..b0810fa --- /dev/null +++ b/src/endpoints/d1/delete.ts @@ -0,0 +1,78 @@ +import { ApiException } from "../../exceptions"; +import { DeleteEndpoint } from "../delete"; +import type { Filters, Logger, O } from "../types"; + +export class D1DeleteEndpoint = Array> extends DeleteEndpoint { + dbName = "DB"; + logger?: Logger; + + getDBBinding(): D1Database { + const env = this.params.router.getBindings(this.args); + if (env[this.dbName] === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); + } + + if (env[this.dbName].prepare === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); + } + + return env[this.dbName]; + } + + getSafeFilters(filters: Filters) { + const conditions: string[] = []; + const conditionsParams: string[] = []; + + for (const f of filters.filters) { + if (f.operator === "EQ") { + conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); + conditionsParams.push(f.value as any); + } else { + throw new ApiException(`operator ${f.operator} Not implemented`); + } + } + + return { conditions, conditionsParams }; + } + + async getObject(filters: Filters): Promise | null> { + const safeFilters = this.getSafeFilters(filters); + + const oldObj = await this.getDBBinding() + .prepare(`SELECT * FROM ${this.meta.model.tableName} WHERE ${safeFilters.conditions.join(" AND ")} LIMIT 1`) + .bind(...safeFilters.conditionsParams) + .all(); + + if (!oldObj.results || oldObj.results.length === 0) { + return null; + } + + return oldObj.results[0] as O; + } + + async delete(oldObj: O, filters: Filters): Promise | null> { + const safeFilters = this.getSafeFilters(filters); + + let result; + try { + result = await this.getDBBinding() + .prepare( + `DELETE FROM ${this.meta.model.tableName} WHERE ${safeFilters.conditions.join(" AND ")} RETURNING * LIMIT 1`, + ) + .bind(...safeFilters.conditionsParams) + .all(); + } catch (e: any) { + if (this.logger) + this.logger.error(`Caught exception while trying to delete ${this.meta.model.tableName}: ${e.message}`); + throw new ApiException(e.message); + } + + if (result.meta.changes === 0) { + return null; + } + + if (this.logger) this.logger.log(`Successfully deleted ${this.meta.model.tableName}`); + + return oldObj; + } +} diff --git a/src/endpoints/d1/fetch.ts b/src/endpoints/d1/fetch.ts new file mode 100644 index 0000000..03a9cc1 --- /dev/null +++ b/src/endpoints/d1/fetch.ts @@ -0,0 +1,36 @@ +import { ApiException } from "../../exceptions"; +import { FetchEndpoint } from "../fetch"; +import type { ListFilters, Logger, O } from "../types"; + +export class D1FetchEndpoint = Array> extends FetchEndpoint { + dbName = "DB"; + logger?: Logger; + + getDBBinding(): D1Database { + const env = this.params.router.getBindings(this.args); + if (env[this.dbName] === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); + } + + if (env[this.dbName].prepare === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); + } + + return env[this.dbName]; + } + + async fetch(filters: ListFilters): Promise | null> { + const conditions = filters.filters.map((obj) => `${obj.field} = ?`); + + const obj = await this.getDBBinding() + .prepare(`SELECT * FROM ${this.meta.model.tableName} WHERE ${conditions.join(" AND ")} LIMIT 1`) + .bind(...filters.filters.map((obj) => obj.value)) + .all(); + + if (!obj.results || obj.results.length === 0) { + return null; + } + + return obj.results[0] as O; + } +} diff --git a/src/endpoints/d1/list.ts b/src/endpoints/d1/list.ts new file mode 100644 index 0000000..4bfbe78 --- /dev/null +++ b/src/endpoints/d1/list.ts @@ -0,0 +1,76 @@ +import { ApiException } from "../../exceptions"; +import { ListEndpoint } from "../list"; +import type { ListFilters, ListResult, Logger, O } from "../types"; + +export class D1ListEndpoint = Array> extends ListEndpoint { + dbName = "DB"; + logger?: Logger; + + getDBBinding(): D1Database { + const env = this.params.router.getBindings(this.args); + if (env[this.dbName] === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); + } + + if (env[this.dbName].prepare === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); + } + + return env[this.dbName]; + } + + async list(filters: ListFilters): Promise> & { result_info: object }> { + const offset = (filters.options.per_page || 20) * (filters.options.page || 0) - (filters.options.per_page || 20); + const limit = filters.options.per_page; + + const conditions: string[] = []; + const conditionsParams: string[] = []; + + for (const f of filters.filters) { + if (this.searchFields && f.field === this.searchFieldName) { + const searchCondition = this.searchFields + .map((obj) => { + return `UPPER(${obj}) like UPPER(?${conditionsParams.length + 1})`; + }) + .join(" or "); + conditions.push(`(${searchCondition})`); + conditionsParams.push(`%${f.value}%`); + } else if (f.operator === "EQ") { + conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); + conditionsParams.push(f.value as any); + } else { + throw new ApiException(`operator ${f.operator} Not implemented`); + } + } + + let where = ""; + if (conditions.length > 0) { + where = `WHERE ${conditions.join(" AND ")}`; + } + + let orderBy = `ORDER BY ${this.meta.model.primaryKeys[0]} DESC`; + if (filters.options.order_by) { + orderBy = `ORDER BY ${filters.options.order_by} ${filters.options.order_by_direction || "ASC"}`; + } + + const results = await this.getDBBinding() + .prepare(`SELECT * FROM ${this.meta.model.tableName} ${where} ${orderBy} LIMIT ${limit} OFFSET ${offset}`) + .bind(...conditionsParams) + .all(); + + const total_count = await this.getDBBinding() + .prepare(`SELECT count(*) as total FROM ${this.meta.model.tableName} ${where} LIMIT ${limit}`) + .bind(...conditionsParams) + .all(); + + return { + result: results.results, + result_info: { + count: results.results.length, + page: filters.options.page, + per_page: filters.options.per_page, + total_count: total_count.results[0]?.total, + }, + }; + } +} diff --git a/src/endpoints/d1/update.ts b/src/endpoints/d1/update.ts new file mode 100644 index 0000000..a93e79d --- /dev/null +++ b/src/endpoints/d1/update.ts @@ -0,0 +1,84 @@ +import { ApiException } from "../../exceptions"; +import type { Logger, O, UpdateFilters } from "../types"; +import { UpdateEndpoint } from "../update"; + +export class D1UpdateEndpoint = Array> extends UpdateEndpoint { + dbName = "DB"; + logger?: Logger; + + getDBBinding(): D1Database { + const env = this.params.router.getBindings(this.args); + if (env[this.dbName] === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not defined in worker`); + } + + if (env[this.dbName].prepare === undefined) { + throw new ApiException(`Binding "${this.dbName}" is not a D1 binding`); + } + + return env[this.dbName]; + } + + getSafeFilters(filters: UpdateFilters) { + // Filters should only apply to primary keys + const safeFilters = filters.filters.filter((f) => { + return this.meta.model.primaryKeys.includes(f.field); + }); + + const conditions: string[] = []; + const conditionsParams: string[] = []; + + for (const f of safeFilters) { + if (f.operator === "EQ") { + conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); + conditionsParams.push(f.value as any); + } else { + throw new ApiException(`operator ${f.operator} Not implemented`); + } + } + + return { conditions, conditionsParams }; + } + + async getObject(filters: UpdateFilters): Promise { + const safeFilters = this.getSafeFilters(filters); + + const oldObj = await this.getDBBinding() + .prepare( + `SELECT * + FROM ${this.meta.model.tableName} WHERE ${safeFilters.conditions.join(" AND ")} LIMIT 1`, + ) + .bind(...safeFilters.conditionsParams) + .all(); + + if (!oldObj.results || oldObj.results.length === 0) { + return null; + } + + return oldObj.results[0] as O; + } + + async update(oldObj: O, filters: UpdateFilters): Promise> { + const safeFilters = this.getSafeFilters(filters); + + let result; + try { + const obj = await this.getDBBinding() + .prepare( + `UPDATE ${this.meta.model.tableName} SET ${Object.keys(filters.updatedData).map((key, index) => `${key} = ?${safeFilters.conditionsParams.length + index + 1}`)} WHERE ${safeFilters.conditions.join(" AND ")} RETURNING *`, + ) + .bind(...safeFilters.conditionsParams, ...Object.values(filters.updatedData)) + .all(); + + result = obj.results[0]; + } catch (e: any) { + if (this.logger) + this.logger.error(`Caught exception while trying to update ${this.meta.model.tableName}: ${e.message}`); + throw new ApiException(e.message); + } + + if (this.logger) this.logger.log(`Successfully updated ${this.meta.model.tableName}`); + + return result as O; + } +} diff --git a/src/endpoints/orms/workers-qb/create.ts b/src/endpoints/orms/workers-qb/create.ts deleted file mode 100644 index 6350aff..0000000 --- a/src/endpoints/orms/workers-qb/create.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ApiException, InputValidationException } from "../../../exceptions"; -import { CreateEndpoint } from "../../create"; -import type { O } from "../../types"; - -export class QBCreateEndpoint = Array> extends CreateEndpoint { - qb: any; // D1QB - logger?: any; - - async create(data: O): Promise> { - let inserted; - try { - inserted = await this.qb - .insert({ - tableName: this.meta.model.tableName, - data: data as any, - returning: "*", - }) - .execute(); - } catch (e: any) { - if (this.logger) - this.logger.error(`Caught exception while trying to create ${this.meta.model.tableName}: ${e.message}`); - if (e.message.includes("UNIQUE constraint failed")) { - if (e.message.includes(this.meta.model.tableName) && e.message.includes(this.meta.model.primaryKeys[0])) { - throw new InputValidationException(`An object with this ${this.meta.model.primaryKeys[0]} already exists`, [ - "body", - this.meta.model.primaryKeys[0], - ]); - } - } - - throw new ApiException(e.message); - } - - if (this.logger) this.logger.log(`Successfully created ${this.meta.model.tableName}`); - - return inserted.results; - } -} diff --git a/src/endpoints/orms/workers-qb/delete.ts b/src/endpoints/orms/workers-qb/delete.ts deleted file mode 100644 index aedab2b..0000000 --- a/src/endpoints/orms/workers-qb/delete.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ApiException } from "../../../exceptions"; -import { DeleteEndpoint } from "../../delete"; -import type { Filters, O } from "../../types"; - -export class QBDeleteEndpoint = Array> extends DeleteEndpoint { - qb: any; // D1QB - logger?: any; - - getSafeFilters(filters: Filters) { - const conditions: string[] = []; - const conditionsParams: string[] = []; - - for (const f of filters.filters) { - if (f.operator === "EQ") { - conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); - conditionsParams.push(f.value as any); - } else { - throw new ApiException(`operator ${f.operator} Not implemented`); - } - } - - return { conditions, conditionsParams }; - } - - async getObject(filters: Filters): Promise | null> { - const safeFilters = this.getSafeFilters(filters); - const oldObj = await this.qb - .fetchOne({ - tableName: this.meta.model.tableName, - fields: "*", - where: { - conditions: safeFilters.conditions, - params: safeFilters.conditionsParams, - }, - }) - .execute(); - - if (!oldObj.results) { - return null; - } - - return oldObj.results; - } - - async delete(oldObj: O, filters: Filters): Promise | null> { - const safeFilters = this.getSafeFilters(filters); - - let result; - try { - result = await this.qb - .delete({ - tableName: this.meta.model.tableName, - where: { - conditions: safeFilters.conditions, - params: safeFilters.conditionsParams, - }, - returning: "*", - }) - .execute(); - } catch (e: any) { - if (this.logger) - this.logger.error(`Caught exception while trying to delete ${this.meta.model.tableName}: ${e.message}`); - throw new ApiException(e.message); - } - - if (result.changes === 0) { - return null; - } - - if (this.logger) this.logger.log(`Successfully deleted ${this.meta.model.tableName}`); - - return oldObj; - } -} diff --git a/src/endpoints/orms/workers-qb/fetch.ts b/src/endpoints/orms/workers-qb/fetch.ts deleted file mode 100644 index 7eaffc4..0000000 --- a/src/endpoints/orms/workers-qb/fetch.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { FetchEndpoint } from "../../fetch"; -import type { ListFilters, O } from "../../types"; - -export class QBFetchEndpoint = Array> extends FetchEndpoint { - qb: any; // D1QB - logger?: any; - - async fetch(filters: ListFilters): Promise | null> { - const obj = await this.qb - .fetchOne({ - tableName: this.meta.model.tableName, - fields: "*", - where: { - conditions: filters.filters.map((obj) => `${obj.field} = ?`), // TODO: implement operator - params: filters.filters.map((obj) => obj.value), - }, - }) - .execute(); - - if (!obj.results) { - return null; - } - - return obj.results; - } -} diff --git a/src/endpoints/orms/workers-qb/list.ts b/src/endpoints/orms/workers-qb/list.ts deleted file mode 100644 index 9a6bf63..0000000 --- a/src/endpoints/orms/workers-qb/list.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ApiException } from "../../../exceptions"; -import { ListEndpoint } from "../../list"; -import type { ListFilters, ListResult, O } from "../../types"; - -export class QBListEndpoint = Array> extends ListEndpoint { - qb: any; // D1QB - logger?: any; - - async list(filters: ListFilters): Promise> & { result_info: object }> { - const offset = (filters.options.per_page || 20) * (filters.options.page || 0) - (filters.options.per_page || 20); - const limit = filters.options.per_page; - - const conditions: string[] = []; - const conditionsParams: string[] = []; - - for (const f of filters.filters) { - if (this.searchFields && f.field === this.searchFieldName) { - const searchCondition = this.searchFields - .map((obj) => { - return `UPPER(${obj}) like UPPER(?${conditionsParams.length + 1})`; - }) - .join(" or "); - conditions.push(`(${searchCondition})`); - conditionsParams.push(`%${f.value}%`); - } else if (f.operator === "EQ") { - conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); - conditionsParams.push(f.value as any); - } else { - throw new ApiException(`operator ${f.operator} Not implemented`); - } - } - - let where = null; - if (conditions.length > 0) { - where = { - conditions: conditions, - params: conditionsParams, - }; - } - - const results = await this.qb - .fetchAll({ - tableName: this.meta.model.tableName, - fields: "*", - where: where, - limit: limit, - offset: offset, - orderBy: filters.options.order_by - ? { - [filters.options.order_by]: filters.options.order_by_direction || "ASC", - } - : { - [this.meta.model.primaryKeys[0] as string]: "ASC", - }, - }) - .execute(); - - return { - result: results.results, - result_info: { - count: results.results.length, - page: filters.options.page, - per_page: filters.options.per_page, - total_count: ( - await this.qb - .fetchOne({ - tableName: this.meta.model.tableName, - fields: "count(*) as total", - where: where, - }) - .execute() - ).results.total, - }, - }; - } -} diff --git a/src/endpoints/orms/workers-qb/update.ts b/src/endpoints/orms/workers-qb/update.ts deleted file mode 100644 index 0ccff4e..0000000 --- a/src/endpoints/orms/workers-qb/update.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { ApiException } from "../../../exceptions"; -import type { O, UpdateFilters } from "../../types"; -import { UpdateEndpoint } from "../../update"; - -export class QBUpdateEndpoint = Array> extends UpdateEndpoint { - qb: any; // D1QB - logger?: any; - - getSafeFilters(filters: UpdateFilters) { - // Filters should only apply to primary keys - const safeFilters = filters.filters.filter((f) => { - return this.meta.model.primaryKeys.includes(f.field); - }); - - const conditions: string[] = []; - const conditionsParams: string[] = []; - - for (const f of safeFilters) { - if (f.operator === "EQ") { - conditions.push(`${f.field} = ?${conditionsParams.length + 1}`); - conditionsParams.push(f.value as any); - } else { - throw new ApiException(`operator ${f.operator} Not implemented`); - } - } - - return { conditions, conditionsParams }; - } - - async getObject(filters: UpdateFilters): Promise { - const safeFilters = this.getSafeFilters(filters); - const oldObj = await this.qb - .fetchOne({ - tableName: this.meta.model.tableName, - fields: "*", - where: { - conditions: safeFilters.conditions, - params: safeFilters.conditionsParams, - }, - }) - .execute(); - - if (!oldObj.results) { - return null; - } - - return oldObj.results; - } - - async update(oldObj: O, filters: UpdateFilters): Promise> { - const safeFilters = this.getSafeFilters(filters); - - let result; - try { - result = ( - await this.qb - .update({ - tableName: this.meta.model.tableName, - data: filters.updatedData, - where: { - conditions: safeFilters.conditions, - params: safeFilters.conditionsParams, - }, - returning: "*", - }) - .execute() - ).results[0]; - } catch (e: any) { - if (this.logger) - this.logger.error(`Caught exception while trying to update ${this.meta.model.tableName}: ${e.message}`); - throw new ApiException(e.message); - } - - if (this.logger) this.logger.log(`Successfully updated ${this.meta.model.tableName}`); - - return result; - } -} diff --git a/src/endpoints/types.ts b/src/endpoints/types.ts index 9cf0ec7..6017019 100644 --- a/src/endpoints/types.ts +++ b/src/endpoints/types.ts @@ -62,3 +62,12 @@ export function MetaGenerator(meta: MetaInput) { }, }; } + +export type Logger = { + log: (...args: any[]) => void; + info: (...args: any[]) => void; + warn: (...args: any[]) => void; + error: (...args: any[]) => void; + debug: (...args: any[]) => void; + trace: (...args: any[]) => void; +}; diff --git a/src/exceptions.ts b/src/exceptions.ts index 31fef0c..ef2a643 100644 --- a/src/exceptions.ts +++ b/src/exceptions.ts @@ -1,7 +1,7 @@ import { contentJson } from "./contentTypes"; export class ApiException extends Error { - isVisible = false; + isVisible = true; message: string; default_message = "Internal Error"; status = 500; diff --git a/src/index.ts b/src/index.ts index 4a7d8f9..6662d64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,8 +19,8 @@ export * from "./endpoints/fetch"; export * from "./endpoints/list"; export * from "./endpoints/update"; -export * from "./endpoints/orms/workers-qb/create"; -export * from "./endpoints/orms/workers-qb/delete"; -export * from "./endpoints/orms/workers-qb/fetch"; -export * from "./endpoints/orms/workers-qb/list"; -export * from "./endpoints/orms/workers-qb/update"; +export * from "./endpoints/d1/create"; +export * from "./endpoints/d1/delete"; +export * from "./endpoints/d1/fetch"; +export * from "./endpoints/d1/list"; +export * from "./endpoints/d1/update"; diff --git a/src/openapi.ts b/src/openapi.ts index edda031..1dba132 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -240,4 +240,8 @@ export class OpenAPIHandler { getUrlParams(args: any[]): Record { throw new Error("getUrlParams not implemented"); } + + getBindings(args: any[]): Record { + throw new Error("getBindings not implemented"); + } } diff --git a/src/route.ts b/src/route.ts index fe66160..03612da 100644 --- a/src/route.ts +++ b/src/route.ts @@ -122,10 +122,9 @@ export class OpenAPIRoute = any> { rawSchema.query = schema.request?.query; unvalidatedData.query = {}; } - console.log(schema.request); + if (schema.request?.headers) { rawSchema.headers = schema.request?.headers; - console.log(schema.request?.headers); unvalidatedData.headers = {}; } diff --git a/tsconfig.json b/tsconfig.json index 8fd4e48..f4d891d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,23 @@ { - "compilerOptions": { - /* Base Options: */ - "esModuleInterop": true, - "skipLibCheck": true, - "target": "es2022", - "verbatimModuleSyntax": true, - "allowJs": true, - "resolveJsonModule": true, - "moduleDetection": "force", - /* Strictness */ - "strict": true, - "noUncheckedIndexedAccess": true, - /* If NOT transpiling with TypeScript: */ - "moduleResolution": "Bundler", - "module": "ESNext", - "noEmit": true, - /* If your code runs in the DOM: */ - "lib": ["es2022", "dom", "dom.iterable"], - "types": [ - "@types/node", - "@types/service-worker-mock", - "@cloudflare/workers-types/experimental" - ] - }, - "include": ["src"] + "compilerOptions": { + /* Base Options: */ + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "verbatimModuleSyntax": true, + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + /* Strictness */ + "strict": true, + "noUncheckedIndexedAccess": true, + /* If NOT transpiling with TypeScript: */ + "moduleResolution": "Bundler", + "module": "ESNext", + "noEmit": true, + /* If your code runs in the DOM: */ + "lib": ["es2022", "dom", "dom.iterable"], + "types": ["@types/node", "@types/service-worker-mock", "@cloudflare/workers-types/experimental"] + }, + "include": ["src"] }