From e652336485ed38394389568c84c4344238c6089d Mon Sep 17 00:00:00 2001 From: Julien Oculi Date: Tue, 16 Jul 2024 15:08:11 +0200 Subject: [PATCH] feat(api): :sparkles: implement basic rest api draft --- .vscode/settings.json | 2 +- deno.json | 2 +- src/api/{routes/users.ts => hander.ts} | 9 +-- src/api/server.ts | 81 ++++++++++++++++++++++++++ src/api/types.ts | 29 +++++++++ src/api/utils.ts | 38 ++++++++++++ 6 files changed, 155 insertions(+), 6 deletions(-) rename src/api/{routes/users.ts => hander.ts} (84%) create mode 100644 src/api/server.ts create mode 100644 src/api/types.ts create mode 100644 src/api/utils.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index e44f09f..b536174 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "conventionalCommits.scopes": ["model", "chore", "db", "task"] + "conventionalCommits.scopes": ["model", "chore", "db", "task", "api"] } diff --git a/deno.json b/deno.json index 8a08e21..660cf84 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@cohabit/resources-manager", - "version": "0.1.2", + "version": "0.2.0", "exports": { "./models": "./src/models/mod.ts", "./db": "./src/db/mod.ts", diff --git a/src/api/routes/users.ts b/src/api/hander.ts similarity index 84% rename from src/api/routes/users.ts rename to src/api/hander.ts index c546d55..e7a1215 100644 --- a/src/api/routes/users.ts +++ b/src/api/hander.ts @@ -1,7 +1,8 @@ -import type { Db, Resource } from '../../../mod.ts' -import type { UUID } from '../../../types.ts' -import type { ResourceBuilder, ResourceType } from '../types.ts' -import { respondJson } from '../utils.ts' +import type { Db } from '../db/mod.ts' +import type { Resource } from '../models/mod.ts' +import type { UUID } from '../../types.ts' +import type { ResourceBuilder, ResourceType } from './types.ts' +import { respondJson } from './utils.ts' export async function resourceHandler( type: ResourceType, diff --git a/src/api/server.ts b/src/api/server.ts new file mode 100644 index 0000000..71ae64c --- /dev/null +++ b/src/api/server.ts @@ -0,0 +1,81 @@ +// /(users|groups|machines)/:?id +import { stripTypeS } from '../api/utils.ts' +import type { Db } from '../db/mod.ts' +import { Credential, Group, Machine, Service, User } from '../models/mod.ts' +import type { CredentialCategory } from '../models/src/credential.ts' +import { resourceHandler } from './hander.ts' + +export class Api { + static init(db: Db, { base = '/' }: { base: `/${string}` }) { + return new Api({ db, base }) + } + + #db: Db + #base: `/${string}` + + private constructor({ db, base }: { db: Db; base: `/${string}` }) { + this.#db = db + this.#base = base + } + + serve() { + return Deno.serve((req) => { + const url = new URL(req.url) + + if (url.pathname.startsWith(this.#base)) { + const base = new URL(this.#base, req.url) + return router(req, base, this.#db) + } + + return new Response(null, { + status: 301, + statusText: 'Not allowed base url', + }) + }) + } +} + +function router(req: Request, base: URL, db: Db): Promise { + const pattern = new URLPattern({ pathname: '/:type/:id?' }, base.href) + const match = pattern.exec(req.url) + + if (!match) { + return Promise.resolve( + new Response(null, { + status: 400, + statusText: 'Bad request', + }), + ) + } + + const { type, id } = match.pathname.groups as { + type: string + id: string | undefined + } + + switch (type) { + case 'credentials': + return resourceHandler>( + stripTypeS(type), + Credential, + db, + req, + id, + ) + case 'groups': + return resourceHandler(stripTypeS(type), Group, db, req, id) + case 'machines': + return resourceHandler(stripTypeS(type), Machine, db, req, id) + case 'services': + return resourceHandler(stripTypeS(type), Service, db, req, id) + case 'users': + return resourceHandler(stripTypeS(type), User, db, req, id) + default: + return Promise.resolve( + new Response(null, { + status: 400, + statusText: 'Bad request', + }), + ) + } +} diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..8834651 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,29 @@ +import type { + Credential, + Group, + Machine, + Resource, + Service, + User, +} from '../../src/models/mod.ts' +import type { JsonValue } from '@std/json' +import type { CredentialCategory } from '../models/src/credential.ts' + +export type ResourceBuilder = T extends + Credential ? typeof Credential + : T extends Group ? typeof Group + : T extends Machine ? typeof Machine + : T extends Service ? typeof Service + : T extends User ? typeof User + : never + +export type ResourceType = T extends + Credential ? 'credential' + : T extends Group ? 'group' + : T extends Machine ? 'machine' + : T extends Service ? 'service' + : T extends User ? 'user' + : never + +export type JsonObject = Record +export type JsonStringifiable = { toJSON(): JsonObject } | JsonObject diff --git a/src/api/utils.ts b/src/api/utils.ts new file mode 100644 index 0000000..593aa2f --- /dev/null +++ b/src/api/utils.ts @@ -0,0 +1,38 @@ +import type { JsonStringifiable } from '../api/types.ts' + +export function respondJson( + valueOrError: T, +): Response { + if (valueOrError instanceof Error) { + const { name, cause, message } = valueOrError + return new Response( + JSON.stringify({ + result: 'error', + error: { name, cause, message }, + }), + { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + status: 500, + statusText: valueOrError.message, + }, + ) + } + + return new Response( + JSON.stringify({ + result: 'ok', + value: valueOrError, + }), + { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }, + ) +} + +export function stripTypeS(typeS: `${T}s`): T { + return typeS.slice(0, -1) as T +}