Compare commits

..

2 commits

Author SHA1 Message Date
qpismont
e607d8b53f switch woodpecker matrix
Some checks failed
ci/woodpecker/pr/build/3 Pipeline is pending
ci/woodpecker/pr/build/4 Pipeline is pending
ci/woodpecker/pr/build/5 Pipeline is pending
ci/woodpecker/pr/lint Pipeline is pending
ci/woodpecker/pr/test/1 Pipeline is pending
ci/woodpecker/pr/test/2 Pipeline is pending
ci/woodpecker/pr/test/3 Pipeline is pending
ci/woodpecker/pr/test/4 Pipeline is pending
ci/woodpecker/pr/test/5 Pipeline is pending
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/5 Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/test/1 Pipeline was successful
ci/woodpecker/push/test/2 Pipeline was successful
ci/woodpecker/push/test/3 Pipeline was successful
ci/woodpecker/push/test/4 Pipeline failed
ci/woodpecker/pull_request_closed/build/1 Pipeline was successful
ci/woodpecker/pull_request_closed/build/2 Pipeline was successful
ci/woodpecker/pull_request_closed/build/3 Pipeline was successful
ci/woodpecker/pull_request_closed/build/4 Pipeline was successful
ci/woodpecker/pull_request_closed/build/5 Pipeline was successful
ci/woodpecker/pull_request_closed/lint Pipeline was successful
ci/woodpecker/pull_request_closed/test/1 Pipeline was successful
ci/woodpecker/pull_request_closed/test/2 Pipeline was successful
ci/woodpecker/pull_request_closed/test/3 Pipeline was successful
ci/woodpecker/pull_request_closed/test/4 Pipeline was successful
ci/woodpecker/pull_request_closed/test/5 Pipeline was successful
2024-01-26 15:02:33 +01:00
qpismont
98945a4087 upgrade to deno 1.40
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint/1 Pipeline was successful
ci/woodpecker/push/lint/2 Pipeline was successful
ci/woodpecker/push/lint/3 Pipeline was successful
ci/woodpecker/push/lint/4 Pipeline was successful
ci/woodpecker/push/lint/5 Pipeline was successful
ci/woodpecker/push/test/1 Pipeline was successful
ci/woodpecker/push/test/2 Pipeline was successful
ci/woodpecker/push/test/3 Pipeline was successful
ci/woodpecker/push/test/4 Pipeline was successful
ci/woodpecker/push/test/5 Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/lint/1 Pipeline was successful
ci/woodpecker/pr/lint/2 Pipeline was successful
ci/woodpecker/pr/lint/3 Pipeline was successful
ci/woodpecker/pr/lint/4 Pipeline was successful
ci/woodpecker/pr/lint/5 Pipeline was successful
ci/woodpecker/pr/test/1 Pipeline was successful
ci/woodpecker/pr/test/2 Pipeline was successful
ci/woodpecker/pr/test/3 Pipeline was successful
ci/woodpecker/pr/test/4 Pipeline was successful
ci/woodpecker/pr/test/5 Pipeline was successful
2024-01-26 13:45:08 +01:00
24 changed files with 428 additions and 568 deletions

177
.gitignore vendored
View file

@ -1,177 +1,2 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/ node_modules/
jspm_packages/ npm/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
.npmrc

View file

@ -1,8 +1,3 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true
"editor.defaultFormatter": "biomejs.biome", }
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib"
}

View file

@ -1,9 +1,16 @@
when:
event: [push]
steps: steps:
build: build:
image: oven/bun:1.1.10-slim image: denoland/deno:${DENO_VERSION}
commands: commands:
- bun install - apt update
- bun run build - apt install curl -y
- curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get install -y nodejs
- deno task build
matrix:
DENO_VERSION:
- 1.39.2
- 1.39.3
- 1.39.4
- 1.40.0
- 1.40.1

View file

@ -1,10 +1,5 @@
when:
event: [push]
steps: steps:
lint: lint:
image: oven/bun:1.1.10-slim image: denoland/deno:1.40.1
commands: commands:
- bun install - deno lint
- bun run ci

View file

@ -4,7 +4,7 @@ when:
steps: steps:
publish: publish:
image: denoland/deno:1.39.2 image: denoland/deno:1.40.1
commands: commands:
- apt update - apt update
- apt install curl -y - apt install curl -y

View file

@ -1,21 +1,19 @@
when:
event: [push]
steps: steps:
test: test:
image: oven/bun:${BUN_VERSION}-slim image: denoland/deno:${DENO_VERSION}
environment: environment:
- NATS_HOST=nats:4222 - NATS_HOST=nats:4222
commands: commands:
- bun install - deno task test
- bun run test
matrix: matrix:
BUN_VERSION: DENO_VERSION:
- 1.1.8 - 1.39.2
- 1.1.9 - 1.39.3
- 1.1.10 - 1.39.4
- 1.40.0
- 1.40.1
services: services:
nats: nats:
image: nats:2-alpine image: nats

View file

@ -1,20 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true
},
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
}
}

BIN
bun.lockb

Binary file not shown.

16
deno.json Normal file
View file

@ -0,0 +1,16 @@
{
"imports": {
"nats": "npm:nats",
"zod": "npm:zod",
"std/": "https://deno.land/std@0.213.0/",
"dnt": "https://deno.land/x/dnt@0.39.0/mod.ts"
},
"tasks": {
"test": "deno test --allow-net --allow-env --parallel",
"build": "deno run -A scripts/build_npm.ts 0.1.0",
"publish": "cd npm && npm publish"
},
"lint": {
"include": ["src/"]
}
}

73
deno.lock Normal file
View file

@ -0,0 +1,73 @@
{
"version": "3",
"packages": {
"specifiers": {
"npm:nats": "npm:nats@2.18.0",
"npm:zod": "npm:zod@3.22.4"
},
"npm": {
"nats@2.18.0": {
"integrity": "sha512-zZF004ejzf67Za0Tva+xphxoxBMNc5IMLqbZ7Ho0j9TMuisjpo+qCd1EktXRCLNxmrZ8O6Tbm1dBsZYNF6yR1A==",
"dependencies": {
"nkeys.js": "nkeys.js@1.0.5"
}
},
"nkeys.js@1.0.5": {
"integrity": "sha512-u25YnRPHiGVsNzwyHnn+PT90sgAhnS8jUJ1nxmkHMFYCJ6+Ic0lv291w7uhRBpJVJ3PH2GWbYqA151lGCRrB5g==",
"dependencies": {
"tweetnacl": "tweetnacl@1.0.3"
}
},
"tweetnacl@1.0.3": {
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"dependencies": {}
},
"zod@3.22.4": {
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"dependencies": {}
}
}
},
"remote": {
"https://deno.land/std@0.211.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.211.0/assert/_diff.ts": "6a2d68f2c42d73a1e31818a4195f40598d672c7f02ac75c7f1b1e6789852c2bc",
"https://deno.land/std@0.211.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
"https://deno.land/std@0.211.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
"https://deno.land/std@0.211.0/assert/assert_almost_equals.ts": "648ea72678296a5ad86d3bbb66904335fa97de3133223f44ca4596b225cdcbef",
"https://deno.land/std@0.211.0/assert/assert_array_includes.ts": "dbb461c20681807a884ad84d873f9e4daead380859531b1e7f27fa4e8f8bf431",
"https://deno.land/std@0.211.0/assert/assert_equals.ts": "b3b33ae8a85ae22a0754c61a7486d4ae870e8938830a94f5cacecba3a9b0442a",
"https://deno.land/std@0.211.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
"https://deno.land/std@0.211.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769",
"https://deno.land/std@0.211.0/assert/assert_greater.ts": "8dfcf082d2bcffcaab3bd0dab48d41e41c26266529567246de47bd6864936f6d",
"https://deno.land/std@0.211.0/assert/assert_greater_or_equal.ts": "9e02ef89f32563f539f7e66556930033418728847aefcca4e3806a735b5f122e",
"https://deno.land/std@0.211.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444",
"https://deno.land/std@0.211.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
"https://deno.land/std@0.211.0/assert/assert_less.ts": "91a6fed705f9c39bbd683b62aa9dfc42547bc886c29f696997e681cafb886b16",
"https://deno.land/std@0.211.0/assert/assert_less_or_equal.ts": "7a3c2e554eb20aa6af9dd4a410e550bcee9e8a28102d51f5f40cb1b8d141e4e1",
"https://deno.land/std@0.211.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1",
"https://deno.land/std@0.211.0/assert/assert_not_equals.ts": "cb78bf9a4357d69673c87b634491bc6b840412c8b55efe472af9877ef6f0a29b",
"https://deno.land/std@0.211.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931",
"https://deno.land/std@0.211.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f",
"https://deno.land/std@0.211.0/assert/assert_not_strict_equals.ts": "89ba25e1da5233404ac4c01651c088759b7977c51034eefc6050fe3fc2d10c46",
"https://deno.land/std@0.211.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49",
"https://deno.land/std@0.211.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54",
"https://deno.land/std@0.211.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366",
"https://deno.land/std@0.211.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7",
"https://deno.land/std@0.211.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7",
"https://deno.land/std@0.211.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
"https://deno.land/std@0.211.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
"https://deno.land/std@0.211.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c",
"https://deno.land/std@0.211.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b",
"https://deno.land/std@0.211.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd",
"https://deno.land/std@0.211.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145",
"https://deno.land/std@0.211.0/fmt/colors.ts": "be082d6a6bbb2980ae7b2bf8c23c6bb2811ba90a06a9bcb861344a71784c5a99",
"https://deno.land/std@0.211.0/testing/_test_suite.ts": "f10a8a6338b60c403f07a76f3f46bdc9f1e1a820c0a1decddeb2949f7a8a0546",
"https://deno.land/std@0.211.0/testing/bdd.ts": "3cbd17bd35f629a76ce63446238dfb4632240dd46b3b205027c45fa3dd67e554"
},
"workspace": {
"dependencies": [
"npm:nats",
"npm:zod"
]
}
}

4
mod.ts Normal file
View file

@ -0,0 +1,4 @@
export * from "./src/service.ts";
export * from "./src/adaptors/nats.ts";
export * from "./src/error.ts";
export type { Request, Response } from "./src/messages.ts";

View file

@ -1,26 +0,0 @@
{
"name": "@qpismont/nano-service",
"version": "0.0.3",
"module": "index.ts",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["/dist"],
"scripts": {
"ci": "biome ci . --error-on-warnings",
"test": "bun test",
"build": "tsc --project tsconfig.build.json"
},
"devDependencies": {
"@biomejs/biome": "1.3.3",
"@types/bun": "latest",
"typescript": "^5.5.4"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"nats": "^2.28.2",
"zod": "^3.23.8"
}
}

27
scripts/build_npm.ts Normal file
View file

@ -0,0 +1,27 @@
import { build, emptyDir } from "dnt";
await emptyDir("./npm");
await build({
entryPoints: ["./mod.ts"],
outDir: "./npm",
shims: {
deno: true,
},
test: false,
importMap: "deno.json",
package: {
name: "@qpismont/nano-service",
version: Deno.args[0],
description: "Your package.",
license: "MIT",
repository: {
"type": "git",
"url": "https://gitea.qpismont.fr/qpismont/nano-service",
},
},
postBuild() {
Deno.copyFileSync("LICENSE", "npm/LICENSE");
Deno.copyFileSync("README.md", "npm/README.md");
},
});

View file

@ -1,8 +1,8 @@
import type { AdaptorSubscribeTypeFn } from "../types.ts"; import { AdaptorSubscribeTypeFn } from "../types.ts";
export default interface Adaptor { export default interface Adaptor {
subscribe(subject: string, fn: AdaptorSubscribeTypeFn): void; subscribe(subject: string, fn: AdaptorSubscribeTypeFn): void;
request(subject: string, req: string): Promise<string>; request(subject: string, req: string): Promise<string>;
listen(serviceName: string): Promise<void>; listen(serviceName: string): Promise<void>;
stop(): Promise<void>; stop(): Promise<void>;
} }

View file

@ -1,58 +1,57 @@
import { AdaptorSubscribeTypeFn } from "../types.ts";
import Adaptor from "./adaptor.ts";
import { import {
type Codec, Codec,
type ConnectionOptions, connect,
type NatsConnection, ConnectionOptions,
StringCodec, NatsConnection,
type Subscription, StringCodec,
connect, Subscription,
} from "nats"; } from "nats";
import type { AdaptorSubscribeTypeFn } from "../types.ts";
import type Adaptor from "./adaptor.ts";
export default class NatsAdaptor implements Adaptor { export default class NatsAdaptor implements Adaptor {
private options: ConnectionOptions; private options: ConnectionOptions;
private nc?: NatsConnection; private nc?: NatsConnection;
private callbacks: Record<string, AdaptorSubscribeTypeFn>; private callbacks: Record<string, AdaptorSubscribeTypeFn>;
private sc: Codec<string>; private sc: Codec<string>;
private sub?: Subscription; private sub?: Subscription;
constructor(options: ConnectionOptions) { constructor(options: ConnectionOptions) {
this.options = options; this.options = options;
this.sc = StringCodec(); this.sc = StringCodec();
this.callbacks = {}; this.callbacks = {};
} }
async listen(serviceName: string) { async listen(serviceName: string) {
this.nc = await connect(this.options); this.nc = await connect(this.options);
this.sub = this.nc.subscribe(`${serviceName}.*`); this.sub = this.nc.subscribe(`${serviceName}.*`);
(async (sub: Subscription) => { (async (sub: Subscription) => {
for await (const msg of sub) { for await (const msg of sub) {
const res = await this.callbacks[msg.subject](this.sc.decode(msg.data)); const res = await this.callbacks[msg.subject](this.sc.decode(msg.data));
msg.respond(this.sc.encode(res)); msg.respond(this.sc.encode(res));
} }
})(this.sub); })(this.sub);
} }
subscribe(subject: string, fn: AdaptorSubscribeTypeFn) { subscribe(subject: string, fn: AdaptorSubscribeTypeFn) {
this.callbacks[subject] = fn; this.callbacks[subject] = fn;
} }
async request(subject: string, req: string): Promise<string> { async request(subject: string, req: string): Promise<string> {
if (this.nc) { if (this.nc) {
const msg = await this.nc.request(subject, this.sc.encode(req)); const msg = await this.nc.request(subject, this.sc.encode(req));
return this.sc.decode(msg.data); return this.sc.decode(msg.data);
} }
throw new Error("nats connection is not initialized"); throw new Error("nats connection is not initialized");
} }
async stop(): Promise<void> { async stop(): Promise<void> {
this.sub?.unsubscribe(); await this.nc?.drain();
await this.nc?.drain(); }
}
} }

View file

@ -1,9 +1,9 @@
export class RequestError extends Error { export class RequestError extends Error {
public statusCode: number; public statusCode: number;
constructor(msg: string, statusCode: number) { constructor(msg: string, statusCode: number) {
super(msg); super(msg);
this.statusCode = statusCode; this.statusCode = statusCode;
} }
} }

View file

@ -1,4 +0,0 @@
export * from "./service";
export * from "./adaptors/nats";
export * from "./error";
export type { Request, Response } from "./messages";

View file

@ -1,25 +1,25 @@
import { z } from "zod"; import { z } from "zod";
export interface Request<T> { export interface Request<T> {
service: string; service: string;
subject: string; subject: string;
data?: T; data?: T;
} }
export const InternalRequestSchema = z.object({ export const InternalRequestSchema = z.object({
from: z.string(), from: z.string(),
data: z.optional(z.any()), data: z.optional(z.any()),
}); });
export const InternalResponseSchema = z.object({ export const InternalResponseSchema = z.object({
data: z.optional(z.any()), data: z.optional(z.any()),
statusCode: z.number(), statusCode: z.number(),
}); });
export type InternalRequest = z.infer<typeof InternalRequestSchema>; export type InternalRequest = z.infer<typeof InternalRequestSchema>;
export type InternalResponse = z.infer<typeof InternalResponseSchema>; export type InternalResponse = z.infer<typeof InternalResponseSchema>;
export interface Response<T> { export interface Response<T> {
data?: T; data?: T;
statusCode: number; statusCode: number;
} }

View file

@ -1,160 +1,166 @@
import type { z } from "zod"; import Adaptor from "./adaptors/adaptor.ts";
import type Adaptor from "./adaptors/adaptor"; import { RequestError } from "./error.ts";
import { RequestError } from "./error";
import { import {
type InternalRequest, InternalRequest,
InternalRequestSchema, InternalRequestSchema,
type InternalResponse, InternalResponse,
InternalResponseSchema, InternalResponseSchema,
type Request, Request,
type Response, Response,
} from "./messages"; } from "./messages.ts";
import type { RouteSubscribeTypeFn } from "./types"; import { RouteSubscribeTypeFn } from "./types.ts";
import { z } from "zod";
export default class Service { export default class Service {
private name: string; private name: string;
private adaptors: Record<string, Adaptor>; private adaptors: Record<string, Adaptor>;
constructor(name: string) { constructor(name: string) {
this.name = name; this.name = name;
this.adaptors = {}; this.adaptors = {};
} }
public addAdaptor(name: string, adaptor: Adaptor) { public addAdaptor(name: string, adaptor: Adaptor) {
this.adaptors[name] = adaptor; this.adaptors[name] = adaptor;
} }
public subscribe<T extends z.ZodType<O>, O, U>( public subscribe<T extends z.ZodType<O>, O, U>(
adaptor: string, adaptor: string,
subject: string, subject: string,
fn: RouteSubscribeTypeFn<z.infer<T>, U>, fn: RouteSubscribeTypeFn<z.infer<T>, U>,
schema?: T, schema: T | undefined = undefined,
) { ) {
this.adaptors[adaptor].subscribe( this.adaptors[adaptor].subscribe(
`${this.name}.${subject}`, `${this.name}.${subject}`,
async (rawReq) => { async (rawReq) => {
const rawReqJson = JSON.parse(rawReq); const rawReqJson = JSON.parse(rawReq);
const internalRequestJson = InternalRequestSchema.safeParse(rawReqJson); const internalRequestJson = InternalRequestSchema.safeParse(rawReqJson);
if (!internalRequestJson.success) {
return JSON.stringify(
{
statusCode: 400,
data: "bad request structure",
} satisfies InternalResponse,
);
}
if (!internalRequestJson.success) { const internalRequest = internalRequestJson.data;
return JSON.stringify({ const req = {
statusCode: 400, service: internalRequest.from,
data: "bad request structure", subject: subject,
} satisfies InternalResponse); } as Request<z.infer<T>>;
}
const internalRequest = internalRequestJson.data; if (internalRequest.data && schema) {
const req = { const validate = schema.safeParse(internalRequest.data);
service: internalRequest.from, if (!validate.success) {
subject: subject, return JSON.stringify(
} as Request<z.infer<T>>; {
statusCode: 400,
data: validate.error,
} satisfies InternalResponse,
);
} else {
req.data = validate.data;
}
}
if (internalRequest.data && schema) { try {
const validate = schema.safeParse(internalRequest.data); const res = await fn(req);
const internalResponse = {
statusCode: res.statusCode,
data: res.data,
} satisfies InternalResponse;
if (!validate.success) { return JSON.stringify(internalResponse);
return JSON.stringify({ } catch (err) {
statusCode: 400, if (err instanceof RequestError) {
data: validate.error, return JSON.stringify(
} satisfies InternalResponse); {
} statusCode: err.statusCode,
data: err.message,
} satisfies InternalResponse,
);
} else {
return JSON.stringify(
{
statusCode: 500,
data: err?.message || "unknow error apend",
} satisfies InternalResponse,
);
}
}
},
);
}
req.data = validate.data; public async request<T, O, U extends z.ZodType<O>>(
} adaptor: string,
req: Request<T>,
schema?: z.ZodType<O>,
): Promise<Response<z.infer<U>>> {
if (!this.adaptors[adaptor]) {
throw new Error(`${adaptor} adaptor not exist`);
}
try { const internalRequest = {
const res = await fn(this, req); from: this.name,
const internalResponse = { data: req.data,
statusCode: res.statusCode, } satisfies InternalRequest;
data: res.data, const internalRequestJson = JSON.stringify(internalRequest);
} satisfies InternalResponse;
return JSON.stringify(internalResponse); try {
} catch (err) { const rawRes = await this.adaptors[adaptor].request(
if (err instanceof RequestError) { `${req.service}.${req.subject}`,
return JSON.stringify({ internalRequestJson,
statusCode: err.statusCode, );
data: err.message, const rawResJson: unknown = JSON.parse(rawRes);
} satisfies InternalResponse); const internalResponseJson = InternalResponseSchema.safeParse(rawResJson);
} if (!internalResponseJson.success) {
throw new RequestError(internalResponseJson.error.toString(), 500);
}
return JSON.stringify({ const internalResponse = internalResponseJson.data;
statusCode: 500, if (
data: "unknow error apend", internalResponse.statusCode < 200 || internalResponse.statusCode >= 299
} satisfies InternalResponse); ) {
} throw new RequestError(
}, internalResponse.data,
); internalResponse.statusCode,
} );
}
const res: Response<z.infer<U>> = {
statusCode: internalResponse.statusCode,
};
if (internalResponse.data && schema) {
const validate = schema.safeParse(internalResponse.data);
public async request<T, O, U extends z.ZodType<O>>( if (!validate.success) {
adaptor: string, throw new RequestError(validate.error.message, 400);
req: Request<T>, } else {
schema?: z.ZodType<O>, res.data = validate.data;
): Promise<Response<z.infer<U>>> { }
if (!this.adaptors[adaptor]) { }
throw new Error(`${adaptor} adaptor not exist`);
}
const internalRequest = { return res;
from: this.name, } catch (err) {
data: req.data, if (err instanceof RequestError) {
} satisfies InternalRequest; throw err;
const internalRequestJson = JSON.stringify(internalRequest); }
try { throw new RequestError("unexpected error", 500);
const rawRes = await this.adaptors[adaptor].request( }
`${req.service}.${req.subject}`, }
internalRequestJson,
);
const rawResJson: unknown = JSON.parse(rawRes);
const internalResponseJson = InternalResponseSchema.safeParse(rawResJson);
if (!internalResponseJson.success) {
throw new RequestError(internalResponseJson.error.toString(), 500);
}
const internalResponse = internalResponseJson.data; public async listen() {
if ( for (const index in this.adaptors) {
internalResponse.statusCode < 200 || await this.adaptors[index].listen(this.name);
internalResponse.statusCode >= 299 }
) { }
throw new RequestError(
internalResponse.data,
internalResponse.statusCode,
);
}
const res: Response<z.infer<U>> = {
statusCode: internalResponse.statusCode,
};
if (internalResponse.data && schema) {
const validate = schema.safeParse(internalResponse.data);
if (!validate.success) { public async stop() {
throw new RequestError(validate.error.message, 400); for (const index in this.adaptors) {
} await this.adaptors[index].stop();
res.data = validate.data; }
} }
return res;
} catch (err) {
if (err instanceof RequestError) {
throw err;
}
throw new RequestError("unexpected error", 500);
}
}
public async listen() {
for (const index in this.adaptors) {
await this.adaptors[index].listen(this.name);
}
}
public async stop() {
for (const index in this.adaptors) {
await this.adaptors[index].stop();
}
}
} }

View file

@ -1,8 +1,6 @@
import type { Request, Response } from "./messages.ts"; import { Request, Response } from "./messages.ts";
import type Service from "./service.ts";
export type AdaptorSubscribeTypeFn = (msg: string) => Promise<string>; export type AdaptorSubscribeTypeFn = (msg: string) => Promise<string>;
export type RouteSubscribeTypeFn<T, U> = ( export type RouteSubscribeTypeFn<T, U> = (
srv: Service, msg: Request<T>,
msg: Request<T>,
) => Promise<Response<U>>; ) => Promise<Response<U>>;

View file

@ -1,12 +1,12 @@
import { expect, test } from "bun:test"; import { RequestError } from "../src/error.ts";
import { RequestError } from "../src/error"; import { assertEquals } from "std/assert/mod.ts";
test("request error", () => { Deno.test("request error", () => {
const expectedMsg = "This is my err"; const expectedMsg = "This is my err";
const expectedStatusCode = 500; const expectedStatusCode = 500;
const err = new RequestError(expectedMsg, expectedStatusCode); const err = new RequestError(expectedMsg, expectedStatusCode);
expect(err.message).toBe(expectedMsg); assertEquals(err.message, expectedMsg);
expect(err.statusCode).toBe(expectedStatusCode); assertEquals(err.statusCode, expectedStatusCode);
}); });

View file

@ -1,92 +1,92 @@
import { afterEach, beforeEach, expect, test } from "bun:test";
import { z } from "zod"; import { z } from "zod";
import NatsAdaptor from "../src/adaptors/nats"; import NatsAdaptor from "../src/adaptors/nats.ts";
import { RequestError } from "../src/error"; import Service from "../src/service.ts";
import Service from "../src/service"; import { assertEquals, assertRejects, assertThrows } from "std/assert/mod.ts";
import { afterEach, beforeEach, it } from "std/testing/bdd.ts";
import { RequestError } from "../src/error.ts";
let srv!: Service; let srv!: Service;
const natsServer = Bun.env.NATS_HOST || "127.0.0.1:4222"; const natsServer = Deno.env.get("NATS_HOST") || "127.0.0.1:4222";
const serviceName = "test-service"; const serviceName = "test-service";
const adaptorName = "nats"; const adaptorName = "nats";
const subject = "test"; const subject = "test";
beforeEach(() => { beforeEach(() => {
srv = new Service(serviceName); srv = new Service(serviceName);
}); });
afterEach(async () => { afterEach(async () => {
await srv.stop(); await srv.stop();
}); });
test("request success", async () => { it("request success", {
const dataExpected = "hello success"; sanitizeExit: false,
const statusCodeExpected = 200; sanitizeOps: false,
sanitizeResources: false,
}, async () => {
const dataExpected = "hello success";
const statusCodeExpected = 200;
srv.addAdaptor(adaptorName, new NatsAdaptor({ servers: [natsServer] })); srv.addAdaptor(adaptorName, new NatsAdaptor({ servers: [natsServer] }));
srv.subscribe( srv.subscribe(adaptorName, subject, async (msg) => {
adaptorName, return { data: msg.data, statusCode: statusCodeExpected };
subject, }, z.string());
async (srv, msg) => {
return { data: msg.data, statusCode: statusCodeExpected };
},
z.string(),
);
await srv.listen(); await srv.listen();
const res = await srv.request( const res = await srv.request(adaptorName, {
adaptorName, service: serviceName,
{ subject: subject,
service: serviceName, data: dataExpected,
subject: subject, }, z.string());
data: dataExpected,
},
z.string(),
);
expect(res.data).toBe(dataExpected); assertEquals(res.data, dataExpected);
expect(res.statusCode).toBe(statusCodeExpected); assertEquals(res.statusCode, statusCodeExpected);
}); });
test("request error", async () => { it("request error", {
const dataExpected = "hello error"; sanitizeExit: false,
const statusCodeExpected = 500; sanitizeOps: false,
sanitizeResources: false,
}, async () => {
const dataExpected = "hello error";
const statusCodeExpected = 500;
srv.addAdaptor(adaptorName, new NatsAdaptor({ servers: [natsServer] })); srv.addAdaptor(adaptorName, new NatsAdaptor({ servers: [natsServer] }));
srv.subscribe(adaptorName, subject, async (srv, msg) => { srv.subscribe(adaptorName, subject, async (msg) => {
throw new RequestError("request error", 500); throw new RequestError("request error", 500);
}); }, z.string());
await srv.listen(); await srv.listen();
expect(() => { assertRejects(() => {
return srv.request( return srv.request(adaptorName, {
adaptorName, service: serviceName,
{ subject: subject,
service: serviceName, data: dataExpected,
subject: subject, }, z.string());
data: dataExpected, });
},
z.string(),
);
}).toThrow();
}); });
test("request adaptor not found", async () => { it("request adaptor not found", {
const dataExpected = "hello success"; sanitizeExit: false,
const statusCodeExpected = 200; sanitizeOps: false,
sanitizeResources: false,
}, async () => {
const dataExpected = "hello success";
const statusCodeExpected = 200;
srv.addAdaptor(adaptorName, new NatsAdaptor({ servers: [natsServer] })); srv.addAdaptor(adaptorName, new NatsAdaptor({ servers: [natsServer] }));
srv.subscribe(adaptorName, subject, async (srv, msg) => { srv.subscribe(adaptorName, subject, async (msg) => {
return { data: msg.data, statusCode: statusCodeExpected }; return { data: msg.data, statusCode: statusCodeExpected };
}); });
await srv.listen(); await srv.listen();
expect(() => { assertRejects(() => {
return srv.request("bad adaptor", { return srv.request("bad adaptor", {
service: serviceName, service: serviceName,
subject: subject, subject: subject,
data: dataExpected, data: dataExpected,
}); });
}).toThrow(); });
}); });

View file

@ -1,11 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"noEmitOnError": true,
"outDir": "dist/",
"allowImportingTsExtensions": false,
"declaration": true
},
"include": ["./src/**/*"]
}

View file

@ -1,22 +0,0 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
}