mirror of
https://github.com/hexolan/panels.git
synced 2026-03-26 12:40:21 +00:00
init user-service
This commit is contained in:
1
services/user-service/.dockerignore
Normal file
1
services/user-service/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
3
services/user-service/.env.example
Normal file
3
services/user-service/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
MONGODB_URI=mongodb://mongo:mongo@localhost:27017/
|
||||
|
||||
KAFKA_BROKERS=localhost:9092
|
||||
33
services/user-service/.eslintrc.js
Normal file
33
services/user-service/.eslintrc.js
Normal file
@@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"files": [
|
||||
".eslintrc.{js,cjs}"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
13
services/user-service/Dockerfile
Normal file
13
services/user-service/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM node:20-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install requirements
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy files
|
||||
COPY . ./
|
||||
|
||||
EXPOSE 9090
|
||||
CMD ["npm", "run", "start"]
|
||||
30
services/user-service/README.md
Normal file
30
services/user-service/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# User Service
|
||||
|
||||
## Event Documentation
|
||||
|
||||
* Events Produced:
|
||||
* **Topic:** "``user``" | **Schema:** "``UserEvent``" protobuf
|
||||
* Type: ``"created"`` | Data: ``User``
|
||||
* Type: ``"updated"`` | Data: ``User``
|
||||
* Type: ``"deleted"`` | Data: ``User``
|
||||
|
||||
* Events Consumed:
|
||||
* N/A
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**MongoDB:**
|
||||
|
||||
``MONGODB_URI`` (Required)
|
||||
|
||||
* e.g. "mongodb://mongo:mongo@localhost:27017/"
|
||||
|
||||
---
|
||||
|
||||
**Kafka:**
|
||||
|
||||
``KAFKA_BROKERS`` (Required)
|
||||
|
||||
* e.g. "localhost:9092" or "localhost:9092,localhost:9093"
|
||||
8
services/user-service/buf.gen.yaml
Normal file
8
services/user-service/buf.gen.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
version: v1
|
||||
plugins:
|
||||
- plugin: es
|
||||
opt: target=ts
|
||||
out: src/proto
|
||||
- plugin: connect-es
|
||||
opt: target=ts
|
||||
out: src/proto
|
||||
4314
services/user-service/package-lock.json
generated
Normal file
4314
services/user-service/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
services/user-service/package.json
Normal file
35
services/user-service/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "user-service",
|
||||
"description": "Panels - User Service",
|
||||
"version": "1.0.0",
|
||||
"author": "Declan <declan@hexolan.dev>",
|
||||
"license": "Apache-2.0",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "tsx ./src/main.ts",
|
||||
"dev": "tsx watch ./src/main.ts",
|
||||
"lint": "eslint .",
|
||||
"protobufs-compile": "npx buf generate ../../protobufs/user.proto"
|
||||
},
|
||||
"homepage": "https://github.com/hexolan/panels",
|
||||
"dependencies": {
|
||||
"@bufbuild/buf": "^1.26.1",
|
||||
"@bufbuild/protobuf": "^1.3.1",
|
||||
"@bufbuild/protoc-gen-es": "^1.3.1",
|
||||
"@connectrpc/connect": "^0.13.2",
|
||||
"@connectrpc/connect-fastify": "^0.13.2",
|
||||
"@connectrpc/protoc-gen-connect-es": "^0.13.2",
|
||||
"@types/mongoose-unique-validator": "^1.0.7",
|
||||
"fastify": "^4.22.0",
|
||||
"kafkajs": "^2.2.4",
|
||||
"mongoose": "^7.5.0",
|
||||
"mongoose-unique-validator": "^4.0.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"eslint": "^8.50.0"
|
||||
}
|
||||
}
|
||||
20
services/user-service/src/config.ts
Normal file
20
services/user-service/src/config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
function getMongoURI(): string {
|
||||
if (process.env.MONGODB_URI === undefined) {
|
||||
throw new Error("mongodb_uri configuration not provided");
|
||||
} else {
|
||||
return process.env.MONGODB_URI
|
||||
}
|
||||
}
|
||||
|
||||
function getKafkaBrokers(): string[] {
|
||||
if (process.env.KAFKA_BROKERS === undefined) {
|
||||
throw new Error("kafka_brokers configuration not provided");
|
||||
} else {
|
||||
return process.env.KAFKA_BROKERS.split(",")
|
||||
}
|
||||
}
|
||||
|
||||
const mongodbUri = getMongoURI();
|
||||
const kafkaBrokers = getKafkaBrokers();
|
||||
|
||||
export { mongodbUri, kafkaBrokers }
|
||||
73
services/user-service/src/kafka/producer.ts
Normal file
73
services/user-service/src/kafka/producer.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Kafka, Message, Producer } from "kafkajs";
|
||||
|
||||
import { kafkaBrokers } from "../config";
|
||||
import { IUser } from "../mongo/User";
|
||||
import { UserEvent } from "../proto/user_pb";
|
||||
import { userToProtoUser } from "../proto/convert";
|
||||
|
||||
class UserProducer {
|
||||
private producer: Producer
|
||||
|
||||
constructor() {
|
||||
const kafka = new Kafka({
|
||||
clientId: "user-service",
|
||||
brokers: kafkaBrokers,
|
||||
})
|
||||
|
||||
this.producer = kafka.producer()
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
try {
|
||||
await this.producer.connect()
|
||||
} catch (error) {
|
||||
console.log("error connecting to kafka producer ", error);
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async disconnect(): Promise<void> {
|
||||
await this.producer.disconnect()
|
||||
}
|
||||
|
||||
public async sendCreatedEvent(user: IUser): Promise<void> {
|
||||
const event = new UserEvent({
|
||||
type: "created",
|
||||
data: userToProtoUser(user)
|
||||
})
|
||||
|
||||
this.sendEvent(event);
|
||||
}
|
||||
|
||||
public async sendUpdatedEvent(user: IUser): Promise<void> {
|
||||
const event = new UserEvent({
|
||||
type: "updated",
|
||||
data: userToProtoUser(user)
|
||||
})
|
||||
|
||||
this.sendEvent(event);
|
||||
}
|
||||
|
||||
public async sendDeletedEvent(user: IUser): Promise<void> {
|
||||
const event = new UserEvent({
|
||||
type: "deleted",
|
||||
data: userToProtoUser(user)
|
||||
})
|
||||
|
||||
this.sendEvent(event);
|
||||
}
|
||||
|
||||
private async sendEvent(event: UserEvent): Promise<void> {
|
||||
const msg: Message = {
|
||||
value: Buffer.from(event.toBinary())
|
||||
}
|
||||
|
||||
await this.producer.send({
|
||||
topic: "user",
|
||||
messages: [msg]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const userProducer = new UserProducer();
|
||||
export default userProducer;
|
||||
14
services/user-service/src/main.ts
Normal file
14
services/user-service/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
import { mongodbUri } from "./config";
|
||||
import userProducer from "./kafka/producer";
|
||||
import serveRPC from "./rpc/server";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await mongoose.connect(mongodbUri);
|
||||
await userProducer.connect()
|
||||
|
||||
serveRPC();
|
||||
}
|
||||
|
||||
main();
|
||||
25
services/user-service/src/mongo/User.ts
Normal file
25
services/user-service/src/mongo/User.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Document, Schema, model } from "mongoose";
|
||||
import uniqueValidator from "mongoose-unique-validator";
|
||||
|
||||
const userSchema = new Schema(
|
||||
{
|
||||
username: { type: String, required: true, lowercase: true, unique: true },
|
||||
isAdmin: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
userSchema.plugin(uniqueValidator);
|
||||
|
||||
interface IUser extends Document {
|
||||
username: string;
|
||||
isAdmin?: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const User = model<IUser>("User", userSchema);
|
||||
|
||||
export { User, IUser }
|
||||
19
services/user-service/src/proto/convert.ts
Normal file
19
services/user-service/src/proto/convert.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Timestamp } from "@bufbuild/protobuf";
|
||||
|
||||
import { User as ProtoUser } from "../proto/user_pb";
|
||||
import { IUser } from "../mongo/User";
|
||||
|
||||
function timeToProtoTimestamp(time: Date | undefined): Timestamp | undefined {
|
||||
if (!time) return undefined;
|
||||
return Timestamp.fromDate(time);
|
||||
}
|
||||
|
||||
export function userToProtoUser(user: IUser): ProtoUser {
|
||||
return new ProtoUser({
|
||||
id: user._id.toString(),
|
||||
username: user.username,
|
||||
isAdmin: (user.isAdmin ? user.isAdmin : false),
|
||||
createdAt: timeToProtoTimestamp(user.createdAt),
|
||||
updatedAt: timeToProtoTimestamp(user.updatedAt)
|
||||
});
|
||||
}
|
||||
35
services/user-service/src/proto/grpc_health_connect.ts
Normal file
35
services/user-service/src/proto/grpc_health_connect.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// @generated by protoc-gen-connect-es v0.13.2 with parameter "target=ts"
|
||||
// @generated from file grpc_health.proto (package grpc.health.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { HealthCheckRequest, HealthCheckResponse } from "./grpc_health_pb.js";
|
||||
import { MethodKind } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* @generated from service grpc.health.v1.Health
|
||||
*/
|
||||
export const Health = {
|
||||
typeName: "grpc.health.v1.Health",
|
||||
methods: {
|
||||
/**
|
||||
* @generated from rpc grpc.health.v1.Health.Check
|
||||
*/
|
||||
check: {
|
||||
name: "Check",
|
||||
I: HealthCheckRequest,
|
||||
O: HealthCheckResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc grpc.health.v1.Health.Watch
|
||||
*/
|
||||
watch: {
|
||||
name: "Watch",
|
||||
I: HealthCheckRequest,
|
||||
O: HealthCheckResponse,
|
||||
kind: MethodKind.ServerStreaming,
|
||||
},
|
||||
}
|
||||
} as const;
|
||||
|
||||
116
services/user-service/src/proto/grpc_health_pb.ts
Normal file
116
services/user-service/src/proto/grpc_health_pb.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// @generated by protoc-gen-es v1.3.1 with parameter "target=ts"
|
||||
// @generated from file grpc_health.proto (package grpc.health.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
|
||||
import { Message, proto3 } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* @generated from message grpc.health.v1.HealthCheckRequest
|
||||
*/
|
||||
export class HealthCheckRequest extends Message<HealthCheckRequest> {
|
||||
/**
|
||||
* @generated from field: string service = 1;
|
||||
*/
|
||||
service = "";
|
||||
|
||||
constructor(data?: PartialMessage<HealthCheckRequest>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "grpc.health.v1.HealthCheckRequest";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "service", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): HealthCheckRequest {
|
||||
return new HealthCheckRequest().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): HealthCheckRequest {
|
||||
return new HealthCheckRequest().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): HealthCheckRequest {
|
||||
return new HealthCheckRequest().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: HealthCheckRequest | PlainMessage<HealthCheckRequest> | undefined, b: HealthCheckRequest | PlainMessage<HealthCheckRequest> | undefined): boolean {
|
||||
return proto3.util.equals(HealthCheckRequest, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message grpc.health.v1.HealthCheckResponse
|
||||
*/
|
||||
export class HealthCheckResponse extends Message<HealthCheckResponse> {
|
||||
/**
|
||||
* @generated from field: grpc.health.v1.HealthCheckResponse.ServingStatus status = 1;
|
||||
*/
|
||||
status = HealthCheckResponse_ServingStatus.UNKNOWN;
|
||||
|
||||
constructor(data?: PartialMessage<HealthCheckResponse>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "grpc.health.v1.HealthCheckResponse";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "status", kind: "enum", T: proto3.getEnumType(HealthCheckResponse_ServingStatus) },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): HealthCheckResponse {
|
||||
return new HealthCheckResponse().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): HealthCheckResponse {
|
||||
return new HealthCheckResponse().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): HealthCheckResponse {
|
||||
return new HealthCheckResponse().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: HealthCheckResponse | PlainMessage<HealthCheckResponse> | undefined, b: HealthCheckResponse | PlainMessage<HealthCheckResponse> | undefined): boolean {
|
||||
return proto3.util.equals(HealthCheckResponse, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from enum grpc.health.v1.HealthCheckResponse.ServingStatus
|
||||
*/
|
||||
export enum HealthCheckResponse_ServingStatus {
|
||||
/**
|
||||
* @generated from enum value: UNKNOWN = 0;
|
||||
*/
|
||||
UNKNOWN = 0,
|
||||
|
||||
/**
|
||||
* @generated from enum value: SERVING = 1;
|
||||
*/
|
||||
SERVING = 1,
|
||||
|
||||
/**
|
||||
* @generated from enum value: NOT_SERVING = 2;
|
||||
*/
|
||||
NOT_SERVING = 2,
|
||||
|
||||
/**
|
||||
* Used only by the Watch method.
|
||||
*
|
||||
* @generated from enum value: SERVICE_UNKNOWN = 3;
|
||||
*/
|
||||
SERVICE_UNKNOWN = 3,
|
||||
}
|
||||
// Retrieve enum metadata with: proto3.getEnumType(HealthCheckResponse_ServingStatus)
|
||||
proto3.util.setEnumType(HealthCheckResponse_ServingStatus, "grpc.health.v1.HealthCheckResponse.ServingStatus", [
|
||||
{ no: 0, name: "UNKNOWN" },
|
||||
{ no: 1, name: "SERVING" },
|
||||
{ no: 2, name: "NOT_SERVING" },
|
||||
{ no: 3, name: "SERVICE_UNKNOWN" },
|
||||
]);
|
||||
|
||||
80
services/user-service/src/proto/user_connect.ts
Normal file
80
services/user-service/src/proto/user_connect.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// @generated by protoc-gen-connect-es v0.13.2 with parameter "target=ts"
|
||||
// @generated from file user.proto (package panels.user.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { CreateUserRequest, DeleteUserByIdRequest, DeleteUserByNameRequest, GetUserByIdRequest, GetUserByNameRequest, UpdateUserByIdRequest, UpdateUserByNameRequest, User } from "./user_pb.js";
|
||||
import { Empty, MethodKind } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* @generated from service panels.user.v1.UserService
|
||||
*/
|
||||
export const UserService = {
|
||||
typeName: "panels.user.v1.UserService",
|
||||
methods: {
|
||||
/**
|
||||
* @generated from rpc panels.user.v1.UserService.CreateUser
|
||||
*/
|
||||
createUser: {
|
||||
name: "CreateUser",
|
||||
I: CreateUserRequest,
|
||||
O: User,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc panels.user.v1.UserService.GetUser
|
||||
*/
|
||||
getUser: {
|
||||
name: "GetUser",
|
||||
I: GetUserByIdRequest,
|
||||
O: User,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc panels.user.v1.UserService.GetUserByName
|
||||
*/
|
||||
getUserByName: {
|
||||
name: "GetUserByName",
|
||||
I: GetUserByNameRequest,
|
||||
O: User,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc panels.user.v1.UserService.UpdateUser
|
||||
*/
|
||||
updateUser: {
|
||||
name: "UpdateUser",
|
||||
I: UpdateUserByIdRequest,
|
||||
O: User,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc panels.user.v1.UserService.UpdateUserByName
|
||||
*/
|
||||
updateUserByName: {
|
||||
name: "UpdateUserByName",
|
||||
I: UpdateUserByNameRequest,
|
||||
O: User,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc panels.user.v1.UserService.DeleteUser
|
||||
*/
|
||||
deleteUser: {
|
||||
name: "DeleteUser",
|
||||
I: DeleteUserByIdRequest,
|
||||
O: Empty,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
/**
|
||||
* @generated from rpc panels.user.v1.UserService.DeleteUserByName
|
||||
*/
|
||||
deleteUserByName: {
|
||||
name: "DeleteUserByName",
|
||||
I: DeleteUserByNameRequest,
|
||||
O: Empty,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
}
|
||||
} as const;
|
||||
|
||||
422
services/user-service/src/proto/user_pb.ts
Normal file
422
services/user-service/src/proto/user_pb.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
// @generated by protoc-gen-es v1.3.1 with parameter "target=ts"
|
||||
// @generated from file user.proto (package panels.user.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
|
||||
import { Message, proto3, Timestamp } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* @generated from message panels.user.v1.User
|
||||
*/
|
||||
export class User extends Message<User> {
|
||||
/**
|
||||
* @generated from field: string id = 1;
|
||||
*/
|
||||
id = "";
|
||||
|
||||
/**
|
||||
* @generated from field: string username = 2;
|
||||
*/
|
||||
username = "";
|
||||
|
||||
/**
|
||||
* @generated from field: bool is_admin = 3;
|
||||
*/
|
||||
isAdmin = false;
|
||||
|
||||
/**
|
||||
* @generated from field: google.protobuf.Timestamp created_at = 4;
|
||||
*/
|
||||
createdAt?: Timestamp;
|
||||
|
||||
/**
|
||||
* @generated from field: google.protobuf.Timestamp updated_at = 5;
|
||||
*/
|
||||
updatedAt?: Timestamp;
|
||||
|
||||
constructor(data?: PartialMessage<User>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.User";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 2, name: "username", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 3, name: "is_admin", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
|
||||
{ no: 4, name: "created_at", kind: "message", T: Timestamp },
|
||||
{ no: 5, name: "updated_at", kind: "message", T: Timestamp },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): User {
|
||||
return new User().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): User {
|
||||
return new User().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): User {
|
||||
return new User().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: User | PlainMessage<User> | undefined, b: User | PlainMessage<User> | undefined): boolean {
|
||||
return proto3.util.equals(User, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message panels.user.v1.UserMutable
|
||||
*/
|
||||
export class UserMutable extends Message<UserMutable> {
|
||||
/**
|
||||
* @generated from field: optional string username = 1;
|
||||
*/
|
||||
username?: string;
|
||||
|
||||
constructor(data?: PartialMessage<UserMutable>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.UserMutable";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "username", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UserMutable {
|
||||
return new UserMutable().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UserMutable {
|
||||
return new UserMutable().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UserMutable {
|
||||
return new UserMutable().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: UserMutable | PlainMessage<UserMutable> | undefined, b: UserMutable | PlainMessage<UserMutable> | undefined): boolean {
|
||||
return proto3.util.equals(UserMutable, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message panels.user.v1.CreateUserRequest
|
||||
*/
|
||||
export class CreateUserRequest extends Message<CreateUserRequest> {
|
||||
/**
|
||||
* @generated from field: panels.user.v1.UserMutable data = 1;
|
||||
*/
|
||||
data?: UserMutable;
|
||||
|
||||
constructor(data?: PartialMessage<CreateUserRequest>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.CreateUserRequest";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "data", kind: "message", T: UserMutable },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateUserRequest {
|
||||
return new CreateUserRequest().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateUserRequest {
|
||||
return new CreateUserRequest().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateUserRequest {
|
||||
return new CreateUserRequest().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: CreateUserRequest | PlainMessage<CreateUserRequest> | undefined, b: CreateUserRequest | PlainMessage<CreateUserRequest> | undefined): boolean {
|
||||
return proto3.util.equals(CreateUserRequest, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message panels.user.v1.GetUserByIdRequest
|
||||
*/
|
||||
export class GetUserByIdRequest extends Message<GetUserByIdRequest> {
|
||||
/**
|
||||
* @generated from field: string id = 1;
|
||||
*/
|
||||
id = "";
|
||||
|
||||
constructor(data?: PartialMessage<GetUserByIdRequest>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.GetUserByIdRequest";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetUserByIdRequest {
|
||||
return new GetUserByIdRequest().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetUserByIdRequest {
|
||||
return new GetUserByIdRequest().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetUserByIdRequest {
|
||||
return new GetUserByIdRequest().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: GetUserByIdRequest | PlainMessage<GetUserByIdRequest> | undefined, b: GetUserByIdRequest | PlainMessage<GetUserByIdRequest> | undefined): boolean {
|
||||
return proto3.util.equals(GetUserByIdRequest, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message panels.user.v1.GetUserByNameRequest
|
||||
*/
|
||||
export class GetUserByNameRequest extends Message<GetUserByNameRequest> {
|
||||
/**
|
||||
* @generated from field: string username = 1;
|
||||
*/
|
||||
username = "";
|
||||
|
||||
constructor(data?: PartialMessage<GetUserByNameRequest>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.GetUserByNameRequest";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "username", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetUserByNameRequest {
|
||||
return new GetUserByNameRequest().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetUserByNameRequest {
|
||||
return new GetUserByNameRequest().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetUserByNameRequest {
|
||||
return new GetUserByNameRequest().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: GetUserByNameRequest | PlainMessage<GetUserByNameRequest> | undefined, b: GetUserByNameRequest | PlainMessage<GetUserByNameRequest> | undefined): boolean {
|
||||
return proto3.util.equals(GetUserByNameRequest, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message panels.user.v1.UpdateUserByIdRequest
|
||||
*/
|
||||
export class UpdateUserByIdRequest extends Message<UpdateUserByIdRequest> {
|
||||
/**
|
||||
* @generated from field: string id = 1;
|
||||
*/
|
||||
id = "";
|
||||
|
||||
/**
|
||||
* @generated from field: panels.user.v1.UserMutable data = 2;
|
||||
*/
|
||||
data?: UserMutable;
|
||||
|
||||
constructor(data?: PartialMessage<UpdateUserByIdRequest>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.UpdateUserByIdRequest";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 2, name: "data", kind: "message", T: UserMutable },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateUserByIdRequest {
|
||||
return new UpdateUserByIdRequest().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateUserByIdRequest {
|
||||
return new UpdateUserByIdRequest().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateUserByIdRequest {
|
||||
return new UpdateUserByIdRequest().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: UpdateUserByIdRequest | PlainMessage<UpdateUserByIdRequest> | undefined, b: UpdateUserByIdRequest | PlainMessage<UpdateUserByIdRequest> | undefined): boolean {
|
||||
return proto3.util.equals(UpdateUserByIdRequest, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message panels.user.v1.UpdateUserByNameRequest
|
||||
*/
|
||||
export class UpdateUserByNameRequest extends Message<UpdateUserByNameRequest> {
|
||||
/**
|
||||
* @generated from field: string username = 1;
|
||||
*/
|
||||
username = "";
|
||||
|
||||
/**
|
||||
* @generated from field: panels.user.v1.UserMutable data = 2;
|
||||
*/
|
||||
data?: UserMutable;
|
||||
|
||||
constructor(data?: PartialMessage<UpdateUserByNameRequest>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.UpdateUserByNameRequest";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "username", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 2, name: "data", kind: "message", T: UserMutable },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateUserByNameRequest {
|
||||
return new UpdateUserByNameRequest().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateUserByNameRequest {
|
||||
return new UpdateUserByNameRequest().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateUserByNameRequest {
|
||||
return new UpdateUserByNameRequest().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: UpdateUserByNameRequest | PlainMessage<UpdateUserByNameRequest> | undefined, b: UpdateUserByNameRequest | PlainMessage<UpdateUserByNameRequest> | undefined): boolean {
|
||||
return proto3.util.equals(UpdateUserByNameRequest, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message panels.user.v1.DeleteUserByIdRequest
|
||||
*/
|
||||
export class DeleteUserByIdRequest extends Message<DeleteUserByIdRequest> {
|
||||
/**
|
||||
* @generated from field: string id = 1;
|
||||
*/
|
||||
id = "";
|
||||
|
||||
constructor(data?: PartialMessage<DeleteUserByIdRequest>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.DeleteUserByIdRequest";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteUserByIdRequest {
|
||||
return new DeleteUserByIdRequest().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteUserByIdRequest {
|
||||
return new DeleteUserByIdRequest().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteUserByIdRequest {
|
||||
return new DeleteUserByIdRequest().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: DeleteUserByIdRequest | PlainMessage<DeleteUserByIdRequest> | undefined, b: DeleteUserByIdRequest | PlainMessage<DeleteUserByIdRequest> | undefined): boolean {
|
||||
return proto3.util.equals(DeleteUserByIdRequest, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message panels.user.v1.DeleteUserByNameRequest
|
||||
*/
|
||||
export class DeleteUserByNameRequest extends Message<DeleteUserByNameRequest> {
|
||||
/**
|
||||
* @generated from field: string username = 1;
|
||||
*/
|
||||
username = "";
|
||||
|
||||
constructor(data?: PartialMessage<DeleteUserByNameRequest>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.DeleteUserByNameRequest";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "username", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): DeleteUserByNameRequest {
|
||||
return new DeleteUserByNameRequest().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): DeleteUserByNameRequest {
|
||||
return new DeleteUserByNameRequest().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): DeleteUserByNameRequest {
|
||||
return new DeleteUserByNameRequest().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: DeleteUserByNameRequest | PlainMessage<DeleteUserByNameRequest> | undefined, b: DeleteUserByNameRequest | PlainMessage<DeleteUserByNameRequest> | undefined): boolean {
|
||||
return proto3.util.equals(DeleteUserByNameRequest, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kafka Event Schema
|
||||
*
|
||||
* @generated from message panels.user.v1.UserEvent
|
||||
*/
|
||||
export class UserEvent extends Message<UserEvent> {
|
||||
/**
|
||||
* @generated from field: string type = 1;
|
||||
*/
|
||||
type = "";
|
||||
|
||||
/**
|
||||
* @generated from field: panels.user.v1.User data = 2;
|
||||
*/
|
||||
data?: User;
|
||||
|
||||
constructor(data?: PartialMessage<UserEvent>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "panels.user.v1.UserEvent";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "type", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 2, name: "data", kind: "message", T: User },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UserEvent {
|
||||
return new UserEvent().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UserEvent {
|
||||
return new UserEvent().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UserEvent {
|
||||
return new UserEvent().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: UserEvent | PlainMessage<UserEvent> | undefined, b: UserEvent | PlainMessage<UserEvent> | undefined): boolean {
|
||||
return proto3.util.equals(UserEvent, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
134
services/user-service/src/rpc/connect.ts
Normal file
134
services/user-service/src/rpc/connect.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Empty } from "@bufbuild/protobuf"
|
||||
import { ConnectRouter, ConnectError, Code } from "@connectrpc/connect"
|
||||
|
||||
import { createUser, getUserById, getUserByUsername, updateUserById, updateUserByUsername, deleteUserById, deleteUserByUsername } from "../service"
|
||||
import { userToProtoUser } from "../proto/convert"
|
||||
import { UserService } from "../proto/user_connect"
|
||||
import { Health } from "../proto/grpc_health_connect"
|
||||
|
||||
import {
|
||||
HealthCheckRequest,
|
||||
HealthCheckResponse,
|
||||
HealthCheckResponse_ServingStatus
|
||||
} from "../proto/grpc_health_pb"
|
||||
|
||||
import {
|
||||
CreateUserRequest,
|
||||
GetUserByIdRequest,
|
||||
GetUserByNameRequest,
|
||||
UpdateUserByIdRequest,
|
||||
UpdateUserByNameRequest,
|
||||
DeleteUserByIdRequest,
|
||||
DeleteUserByNameRequest,
|
||||
User as ProtoUser
|
||||
} from "../proto/user_pb"
|
||||
|
||||
export default (router: ConnectRouter) => {
|
||||
router.service(UserService, {
|
||||
async createUser(req: CreateUserRequest): Promise<ProtoUser> {
|
||||
// validate inputs
|
||||
if (req.data === undefined) {
|
||||
throw new ConnectError("no values provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
if (req.data.username === undefined || req.data.username === "") {
|
||||
throw new ConnectError("no username provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
// attempt to create user
|
||||
const user = await createUser(req.data.username);
|
||||
return userToProtoUser(user);
|
||||
},
|
||||
|
||||
async getUser(req: GetUserByIdRequest): Promise<ProtoUser> {
|
||||
const user = await getUserById(req.id);
|
||||
return userToProtoUser(user);
|
||||
},
|
||||
|
||||
async getUserByName(req: GetUserByNameRequest): Promise<ProtoUser> {
|
||||
const user = await getUserByUsername(req.username);
|
||||
return userToProtoUser(user);
|
||||
},
|
||||
|
||||
async updateUser(req: UpdateUserByIdRequest): Promise<ProtoUser> {
|
||||
// validate inputs
|
||||
if (req.id === "") {
|
||||
throw new ConnectError("no user id provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
if (req.data === undefined) {
|
||||
throw new ConnectError("no values provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
if (req.data.username === undefined) {
|
||||
throw new ConnectError("no username value provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
// attempt to update user
|
||||
const user = await updateUserById(req.id, req.data.username);
|
||||
return userToProtoUser(user);
|
||||
},
|
||||
|
||||
async updateUserByName(req: UpdateUserByNameRequest): Promise<ProtoUser> {
|
||||
// validate inputs
|
||||
if (req.username === "") {
|
||||
throw new ConnectError("no username provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
if (req.data === undefined) {
|
||||
throw new ConnectError("no values provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
if (req.data.username === undefined) {
|
||||
throw new ConnectError("no username value provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
// attempt to update user
|
||||
const user = await updateUserByUsername(req.username, req.data.username);
|
||||
return userToProtoUser(user);
|
||||
},
|
||||
|
||||
async deleteUser(req: DeleteUserByIdRequest): Promise<Empty> {
|
||||
// validate input
|
||||
if (req.id === "") {
|
||||
throw new ConnectError("no user id provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
// attempt to delete the user
|
||||
await deleteUserById(req.id);
|
||||
return new Empty();
|
||||
},
|
||||
|
||||
async deleteUserByName(req: DeleteUserByNameRequest): Promise<Empty> {
|
||||
// validate input
|
||||
if (req.username === "") {
|
||||
throw new ConnectError("no username provided", Code.InvalidArgument);
|
||||
}
|
||||
|
||||
// attempt to delete the user
|
||||
await deleteUserByUsername(req.username);
|
||||
return new Empty();
|
||||
}
|
||||
});
|
||||
|
||||
// Health gRPC Service
|
||||
router.service(Health, {
|
||||
check(): HealthCheckResponse {
|
||||
const healthyResponse = new HealthCheckResponse({
|
||||
status: HealthCheckResponse_ServingStatus.SERVING,
|
||||
});
|
||||
return healthyResponse;
|
||||
},
|
||||
|
||||
async *watch(req: HealthCheckRequest): AsyncGenerator<HealthCheckResponse> {
|
||||
const healthyResponse = new HealthCheckResponse({
|
||||
status: HealthCheckResponse_ServingStatus.SERVING,
|
||||
});
|
||||
|
||||
while (req) {
|
||||
yield healthyResponse;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
12
services/user-service/src/rpc/server.ts
Normal file
12
services/user-service/src/rpc/server.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { fastify } from "fastify";
|
||||
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
|
||||
|
||||
import routes from "./connect";
|
||||
|
||||
export default async function serveRPC(): Promise<void> {
|
||||
const server = fastify({ http2: true, logger: true });
|
||||
await server.register(fastifyConnectPlugin, { routes: routes });
|
||||
|
||||
await server.listen({ host: "0.0.0.0", port: 9090 });
|
||||
console.log("rpc server is listening at", server.addresses());
|
||||
}
|
||||
161
services/user-service/src/service.ts
Normal file
161
services/user-service/src/service.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Types } from "mongoose";
|
||||
import { ConnectError, Code } from "@connectrpc/connect";
|
||||
|
||||
import { User, IUser } from "./mongo/User";
|
||||
import userProducer from "./kafka/producer";
|
||||
|
||||
function isValidUsername(username: string): boolean {
|
||||
const length = username.length;
|
||||
if (length < 3 || length > 32) {
|
||||
return false
|
||||
}
|
||||
|
||||
const regexCheck = new RegExp("^[^_]\\w+[^_]$");
|
||||
if (!regexCheck.test(username)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function createUser(username: string): Promise<IUser> {
|
||||
if (!isValidUsername(username)) {
|
||||
throw new ConnectError("invalid username", Code.InvalidArgument)
|
||||
}
|
||||
|
||||
const newUser = new User({ username: username })
|
||||
|
||||
const user = await newUser.save().then(async (user) => {
|
||||
await userProducer.sendCreatedEvent(user)
|
||||
return user
|
||||
}).catch(() => {
|
||||
// todo: ensure error is a result of unique constraint violation
|
||||
throw new ConnectError("username already exists", Code.AlreadyExists)
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function getUserById(id: string): Promise<IUser> {
|
||||
// ensure id is valid
|
||||
if (!Types.ObjectId.isValid(id)) {
|
||||
throw new ConnectError("invalid id provided", Code.InvalidArgument)
|
||||
}
|
||||
|
||||
// attempt to get the user document
|
||||
const user = await User.findById(id).exec()
|
||||
if (user === null) {
|
||||
throw new ConnectError("user not found", Code.NotFound)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
async function getUserByUsername(username: string): Promise<IUser> {
|
||||
// ensure username is valid
|
||||
if (username === "") {
|
||||
throw new ConnectError("invalid username", Code.InvalidArgument)
|
||||
}
|
||||
|
||||
// attempt to find the document
|
||||
const user = await User.findOne({ username: username })
|
||||
if (user === null) {
|
||||
throw new ConnectError("user not found", Code.NotFound)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
async function updateUserById(id: string, newUsername: string): Promise<IUser> {
|
||||
if (!isValidUsername(newUsername)) {
|
||||
throw new ConnectError("invalid username value", Code.InvalidArgument)
|
||||
}
|
||||
|
||||
// ensure id is valid
|
||||
if (!Types.ObjectId.isValid(id)) {
|
||||
throw new ConnectError("invalid id provided", Code.InvalidArgument)
|
||||
}
|
||||
|
||||
// attempt to update the user
|
||||
const updatedUser = await User.findByIdAndUpdate(
|
||||
id,
|
||||
{ username: newUsername },
|
||||
{ new: true }
|
||||
).then(async (updatedUser) => {
|
||||
if (!updatedUser) {
|
||||
throw new ConnectError("something unexpected went wrong", Code.Internal)
|
||||
}
|
||||
|
||||
await userProducer.sendUpdatedEvent(updatedUser)
|
||||
return updatedUser
|
||||
}).catch(() => {
|
||||
throw new ConnectError("user not found", Code.NotFound)
|
||||
})
|
||||
|
||||
if (updatedUser === null) {
|
||||
throw new ConnectError("something unexpected went wrong", Code.Internal)
|
||||
}
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
async function updateUserByUsername(username: string, newUsername: string): Promise<IUser> {
|
||||
if (!isValidUsername(newUsername)) {
|
||||
throw new ConnectError("invalid username value", Code.InvalidArgument)
|
||||
}
|
||||
|
||||
// attempt to update the user
|
||||
const updatedUser = await User.findOneAndUpdate(
|
||||
{ username: username },
|
||||
{ username: newUsername },
|
||||
{ new: true }
|
||||
).then(async (updatedUser) => {
|
||||
if (!updatedUser) {
|
||||
throw new ConnectError("something unexpected went wrong", Code.Internal)
|
||||
}
|
||||
|
||||
await userProducer.sendUpdatedEvent(updatedUser)
|
||||
return updatedUser
|
||||
}).catch(() => {
|
||||
throw new ConnectError("user not found", Code.NotFound)
|
||||
})
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
async function deleteUserById(id: string): Promise<void> {
|
||||
// ensure id is valid
|
||||
if (!Types.ObjectId.isValid(id)) {
|
||||
throw new ConnectError("invalid id provided", Code.InvalidArgument)
|
||||
}
|
||||
|
||||
// atempt to delete the user
|
||||
await User.findByIdAndDelete(id).then(async (deletedUser) => {
|
||||
if (!deletedUser) {
|
||||
throw new ConnectError("user not found", Code.NotFound)
|
||||
}
|
||||
|
||||
await userProducer.sendDeletedEvent(deletedUser)
|
||||
return deletedUser
|
||||
}).catch(() => {
|
||||
throw new ConnectError("user not found", Code.NotFound)
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteUserByUsername(username: string): Promise<void> {
|
||||
// attempt to delete the user
|
||||
await User.findOneAndDelete({
|
||||
username: username
|
||||
}).then(async (deletedUser) => {
|
||||
if (!deletedUser) {
|
||||
throw new ConnectError("user not found", Code.NotFound)
|
||||
}
|
||||
|
||||
await userProducer.sendDeletedEvent(deletedUser)
|
||||
return deletedUser
|
||||
}).catch(() => {
|
||||
throw new ConnectError("user not found", Code.NotFound)
|
||||
})
|
||||
}
|
||||
|
||||
export { createUser, getUserById, getUserByUsername, updateUserById, updateUserByUsername, deleteUserById, deleteUserByUsername }
|
||||
17
services/user-service/tsconfig.json
Normal file
17
services/user-service/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Language and Environment */
|
||||
"target": "es2016",
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs",
|
||||
|
||||
/* Interop Constraints */
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user