init user-service

This commit is contained in:
2023-09-27 16:51:22 +01:00
parent 0341a938fd
commit b725dae0f9
21 changed files with 5565 additions and 0 deletions

View 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 }

View 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;

View 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();

View 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 }

View 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)
});
}

View 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;

View 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" },
]);

View 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;

View 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);
}
}

View 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));
}
}
});
}

View 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());
}

View 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 }