morio_bridge 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+
2
+ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
3
+ /* eslint-disable */
4
+ // @ts-nocheck
5
+ /**
6
+ * This is a barrel export file for all models and their related types.
7
+ *
8
+ * 🟢 You can import this file directly.
9
+ */
10
+ export type * from './models/User.ts'
11
+ export type * from './commonInputTypes.ts'
@@ -0,0 +1,24 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ previewFeatures = ["queryCompiler", "driverAdapters"]
4
+ output = "./client"
5
+ runtime = "bun"
6
+ moduleFormat = "esm"
7
+ }
8
+
9
+ datasource db {
10
+ provider = "postgresql"
11
+ url = env("DATABASE_URL")
12
+ }
13
+
14
+ model User {
15
+ id String @id @default(uuid())
16
+ email String
17
+ password String
18
+ createdAt DateTime @default(now())
19
+ updatedAt DateTime @updatedAt
20
+
21
+ @@map("users")
22
+ }
23
+
24
+
@@ -0,0 +1,95 @@
1
+ import { spawn } from "child_process";
2
+ import kleur from "kleur";
3
+ import path from "path";
4
+
5
+ const commands = [
6
+ {
7
+ command: "prisma migrate reset --schema=./db/pg-dev/schema.prisma --force",
8
+ verbose: false,
9
+ label: "orm",
10
+ description: "resetting database",
11
+ },
12
+ {
13
+ command:
14
+ "prisma migrate dev --schema=./db/pg-dev/schema.prisma --name=init",
15
+ verbose: false,
16
+ label: "orm",
17
+ description: "running migrations",
18
+ },
19
+ ];
20
+
21
+ const runCommand = ({
22
+ command,
23
+ verbose = true,
24
+ label = "cmd",
25
+ description = "",
26
+ }: {
27
+ command: string;
28
+ verbose?: boolean;
29
+ label?: string;
30
+ description?: string;
31
+ }) => {
32
+ return new Promise((resolve, reject) => {
33
+ const commandSplit = command.split(" ");
34
+ const base = commandSplit[0];
35
+ const args = commandSplit.slice(1) || [];
36
+
37
+ if (!base) {
38
+ throw new Error("Invalid command");
39
+ }
40
+
41
+ const basePath = path.basename(base);
42
+
43
+ // can i catch the error if the command is not found?
44
+
45
+ const proc = spawn(base, args, {
46
+ env: {
47
+ ...process.env,
48
+ },
49
+ });
50
+
51
+ if (label && verbose) {
52
+ const message = `[${label}] ${basePath} ${args.join(" ")}\n`;
53
+ process.stdout.write(kleur.gray(message));
54
+ }
55
+
56
+ if (label && !verbose && description) {
57
+ const message = `[${label}] ${description}\n`;
58
+ process.stdout.write(kleur.gray(message));
59
+ }
60
+
61
+ proc.on("error", (error) => {
62
+ const message = `[${label}] ${error.message}\n`;
63
+ process.stderr.write(kleur.red(message));
64
+ });
65
+
66
+ proc.stdout.on("data", (data) => {
67
+ if (verbose) {
68
+ process.stdout.write(kleur.gray(data));
69
+ }
70
+ });
71
+
72
+ proc.stderr.on("data", (data) => {
73
+ process.stderr.write(kleur.red(data));
74
+ });
75
+
76
+ proc.on("close", (code) => {
77
+ if (code === 0) {
78
+ if (verbose) {
79
+ const message = `[${label}] exited with code ${code}\n`;
80
+ process.stdout.write(kleur.gray(message));
81
+ }
82
+ resolve(true);
83
+ } else {
84
+ const message = `[${label}] exited with code ${code}\n`;
85
+ process.stderr.write(kleur.red(message));
86
+ // reject(new Error(`Command failed with code ${code}\n`));
87
+ process.exit(code);
88
+ }
89
+ });
90
+ });
91
+ };
92
+
93
+ for (const command of commands) {
94
+ await runCommand(command);
95
+ }
@@ -0,0 +1,21 @@
1
+ import { plugins } from "../plugins";
2
+
3
+ const request = {
4
+ command: "database",
5
+ model: "user",
6
+ action: "create",
7
+ payload: {
8
+ data: {
9
+ email: "johanambuguah@gmail.com",
10
+ password: "jmm",
11
+ createdAt: new Date().toISOString(),
12
+ updatedAt: new Date().toISOString(),
13
+ },
14
+ },
15
+ };
16
+
17
+ const response = await plugins
18
+ .find((plugin: any) => plugin.command && plugin.command === request?.command)
19
+ ?.run(request);
20
+
21
+ console.log({ response });
data/server/index.ts ADDED
@@ -0,0 +1,349 @@
1
+ import { existsSync, unlinkSync } from "fs";
2
+ import { createServer, Server, Socket } from "net";
3
+ import { plugins } from "./plugins";
4
+
5
+ interface ParsedRequest {
6
+ method: string;
7
+ path: string;
8
+ body: string;
9
+ }
10
+
11
+ enum REPLY_TYPE {
12
+ Health = "health",
13
+ Invalid = "invalid_request",
14
+ Error = "internal_server_error",
15
+ Success = "success",
16
+ }
17
+
18
+ // Sweet spot server settings( keepalive probe delay and nagle algorithm disabling for lower latency )
19
+ const KEEPALIVE_PROBE_DELAY_MS = 50;
20
+ const DISABLE_NAGLE_ALGORITHM = true;
21
+
22
+ class UnixSocket {
23
+ private server: Server | null = null;
24
+ private bufferSize = 65536; // bytes
25
+ private isReady: boolean = false;
26
+
27
+ constructor(
28
+ private readonly socketPath: string,
29
+ private messageHandler: (data: string) => Promise<any>
30
+ ) {}
31
+
32
+ /**
33
+ * [ Lifecycle]
34
+ */
35
+ public async start(): Promise<void> {
36
+ const maxConnectionAttempts = 20;
37
+ const retryDelayMs = 50;
38
+ let connectionAttempts = 0;
39
+
40
+ this.createServer();
41
+
42
+ while (connectionAttempts < maxConnectionAttempts) {
43
+ this.deleteSocket(this.socketPath);
44
+
45
+ if (connectionAttempts > 0) {
46
+ const socketName = this.socketPath.split("/").pop();
47
+ console.log({
48
+ message: `connecting to ${socketName}`,
49
+ attempts: connectionAttempts,
50
+ });
51
+ }
52
+
53
+ try {
54
+ await new Promise<void>((resolve, reject) => {
55
+ // server not initialized - retry connection
56
+ if (!this.server) {
57
+ reject(new Error("Server not initialized"));
58
+ return;
59
+ }
60
+
61
+ // server is initialized - listen for connections
62
+ this.server.listen(this.socketPath, () => {
63
+ this.isReady = true;
64
+ resolve();
65
+ });
66
+
67
+ // register error handler - check if the socket file exists
68
+ // and delete it if it does then retry connection
69
+ this.server.once("error", (e: any) => {
70
+ const existsError = e.code === "EEXIST";
71
+ const socketFileExists = existsSync(this.socketPath);
72
+
73
+ if (existsError && socketFileExists) unlinkSync(this.socketPath);
74
+ reject(e);
75
+ });
76
+ });
77
+ return;
78
+ } catch (e: any) {
79
+ if (connectionAttempts < maxConnectionAttempts - 1) {
80
+ connectionAttempts++;
81
+ await this.sleep(retryDelayMs);
82
+ continue;
83
+ }
84
+ throw new Error(
85
+ `Failed to listen on ${this.socketPath} after ${maxConnectionAttempts} attempts: ${e.message}`
86
+ );
87
+ }
88
+ }
89
+ }
90
+
91
+ public stop(): void {
92
+ this.server?.close();
93
+ this.isReady = false;
94
+ this.deleteSocket(this.socketPath);
95
+ }
96
+
97
+ /**
98
+ * [ message handler ]
99
+ */
100
+ private async onMessage(
101
+ socket: Socket,
102
+ method: string,
103
+ path: string,
104
+ body: string
105
+ ) {
106
+ try {
107
+ const healthCheck = method === "GET" && path === "/health";
108
+ const invalidRequest = method !== "POST" || path !== "/prisma/0/call";
109
+
110
+ switch (true) {
111
+ case healthCheck:
112
+ await this.reply({ socket, type: REPLY_TYPE.Health });
113
+ break;
114
+ case invalidRequest:
115
+ await this.reply({ socket, type: REPLY_TYPE.Invalid });
116
+ break;
117
+ default:
118
+ // Success callbacks can be handled here
119
+ const response = await this.messageHandler(body);
120
+ await this.reply({
121
+ socket,
122
+ type: REPLY_TYPE.Success,
123
+ body: response,
124
+ });
125
+ }
126
+ } catch (e) {
127
+ // Error callbacks can be handled here
128
+ await this.reply({
129
+ socket,
130
+ type: REPLY_TYPE.Error,
131
+ error: e instanceof Error ? e.message : "Unknown error",
132
+ });
133
+ }
134
+ }
135
+
136
+ /**
137
+ * [ unix server socket helpers ]
138
+ */
139
+ private createServer() {
140
+ this.server = createServer(
141
+ {
142
+ highWaterMark: this.bufferSize,
143
+ },
144
+ (socket: Socket) => {
145
+ // socket options
146
+ socket.setEncoding("utf8");
147
+ socket.setKeepAlive(true, KEEPALIVE_PROBE_DELAY_MS);
148
+ socket.setNoDelay(DISABLE_NAGLE_ALGORITHM);
149
+
150
+ this.setupSocketListeners(socket);
151
+ }
152
+ );
153
+ }
154
+
155
+ private async setupSocketListeners(socket: Socket): Promise<void> {
156
+ // buffer incoming data
157
+ let buffer = "";
158
+
159
+ socket.on("data", async (data: string) => {
160
+ buffer += data;
161
+
162
+ const done = buffer.includes("\r\n\r\n");
163
+ if (!done) return;
164
+ const { method, path, body } = await this.fromSocketFormat(buffer);
165
+
166
+ await this.onMessage(socket, method, path, body);
167
+
168
+ // reset buffer
169
+ buffer = "";
170
+ });
171
+
172
+ socket.on("error", (e: Error) => {
173
+ console.error(
174
+ `Socket error on ${this.socketPath}: ${e.message}, stack: ${e.stack}`
175
+ );
176
+ });
177
+
178
+ socket.on("drain", () => {
179
+ socket.resume();
180
+ });
181
+
182
+ socket.on("close", () => {});
183
+ }
184
+
185
+ private deleteSocket(socketPath: string) {
186
+ // delete the socket if it exists because the server might have crashed
187
+ // and left the socket file behind
188
+
189
+ try {
190
+ const socketExists = existsSync(socketPath);
191
+ if (socketExists) unlinkSync(socketPath);
192
+ } catch (e: any) {
193
+ if (e.code !== "ENOENT")
194
+ console.warn(`Failed to delete socket ${socketPath}:`, e);
195
+ }
196
+ }
197
+
198
+ private sleep(ms: number) {
199
+ return new Promise((resolve) => setTimeout(resolve, ms));
200
+ }
201
+
202
+ /**
203
+ * [ request/reply helpers ]
204
+ */
205
+ private async writeToSocket({
206
+ socket,
207
+ code,
208
+ status,
209
+ body,
210
+ }: {
211
+ socket: Socket;
212
+ code: number;
213
+ status: string;
214
+ body: any;
215
+ }): Promise<void> {
216
+ const data = await this.toSocketFormat(code, status, body);
217
+
218
+ return new Promise((resolve) => {
219
+ const isWritable = socket.write(data);
220
+
221
+ if (!isWritable) {
222
+ socket.once("drain", () => {
223
+ socket.resume();
224
+ resolve();
225
+ });
226
+ } else resolve();
227
+ });
228
+ }
229
+
230
+ private async reply({
231
+ socket,
232
+ type,
233
+ body,
234
+ error,
235
+ }: {
236
+ socket: Socket;
237
+ type: REPLY_TYPE;
238
+ body?: any;
239
+ error?: string;
240
+ }) {
241
+ switch (type) {
242
+ case REPLY_TYPE.Health:
243
+ await this.writeToSocket({
244
+ socket,
245
+ code: 200,
246
+ status: "OK",
247
+ body: { status: "healthy" },
248
+ });
249
+ break;
250
+ case REPLY_TYPE.Invalid:
251
+ await this.writeToSocket({
252
+ socket,
253
+ code: 400,
254
+ status: "Bad Request",
255
+ body: { error: "Invalid request" },
256
+ });
257
+ break;
258
+ case REPLY_TYPE.Error:
259
+ await this.writeToSocket({
260
+ socket,
261
+ code: 500,
262
+ status: "Internal Server Error",
263
+ body: { error },
264
+ });
265
+ break;
266
+ case REPLY_TYPE.Success:
267
+ await this.writeToSocket({
268
+ socket,
269
+ code: 200,
270
+ status: "OK",
271
+ body,
272
+ });
273
+ break;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * [ socket format helpers ]
279
+ */
280
+ private async fromSocketFormat(data: string): Promise<ParsedRequest> {
281
+ const headerEnd = data.indexOf("\r\n\r\n");
282
+ if (headerEnd === -1) {
283
+ throw new Error("Invalid request: no header-body separator");
284
+ }
285
+
286
+ const headers = data.slice(0, headerEnd).split("\r\n");
287
+ const [method, path] = headers[0]?.split(" ") ?? ["", ""];
288
+ const body = data.slice(headerEnd + 4);
289
+ return { method: method ?? "", path: path ?? "", body };
290
+ }
291
+
292
+ private async toSocketFormat(
293
+ statusCode: number,
294
+ statusText: string,
295
+ body: any
296
+ ): Promise<string> {
297
+ const bodyStr = await JSON.stringify(body);
298
+ return `HTTP/1.1 ${statusCode} ${statusText}\r\nContent-Type: application/json\r\nContent-Length: ${bodyStr.length}\r\n\r\n${bodyStr}`;
299
+ }
300
+ }
301
+
302
+ class SocketCluster {
303
+ private servers: UnixSocket[] = [];
304
+
305
+ constructor(
306
+ private readonly config: {
307
+ socketPaths: string;
308
+ messageHandler: (body: string) => Promise<any>;
309
+ }
310
+ ) {
311
+ this.servers = this.config.socketPaths
312
+ .split(",")
313
+ .map((path) => new UnixSocket(path, this.config.messageHandler));
314
+ }
315
+
316
+ public async init() {
317
+ for (const server of this.servers) {
318
+ await server.start();
319
+ }
320
+
321
+ this.servers.forEach((server) => {
322
+ process.on("SIGTERM", () => {
323
+ server.stop();
324
+ process.exit(0);
325
+ });
326
+ });
327
+ }
328
+ }
329
+
330
+ new SocketCluster({
331
+ socketPaths: process.env.MORIO_RB_BUN_SOCKETS || "/tmp/prisma.sock",
332
+ messageHandler: async (body: string) => {
333
+ const request = await JSON.parse(body);
334
+
335
+ const response = plugins
336
+ .find(
337
+ (plugin: any) => plugin.command && plugin.command === request?.command
338
+ )
339
+ ?.run(request);
340
+
341
+ if (response) return response;
342
+
343
+ return {
344
+ from: "bun-server-xxx",
345
+ request,
346
+ response: request,
347
+ };
348
+ },
349
+ }).init();
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "unix",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "start": "bun run index.ts",
7
+ "ptest": "tsx test.ts",
8
+ "prisma:generate": "prisma generate --schema=./db/pg-dev/schema.prisma",
9
+ "prisma:studio": "prisma studio",
10
+ "prisma:migrate:dev": "prisma migrate dev --schema=./db/pg-dev/schema.prisma --name=init",
11
+ "prisma:migrate:reset": "prisma migrate reset --schema=./db/pg-dev/schema.prisma --force",
12
+ "prisma:db:pull": "prisma db pull --schema=./db/pg-dev/schema.prisma",
13
+ "prisma:db:push": "prisma db push --schema=./db/pg-dev/schema.prisma",
14
+ "prisma:validate": "prisma validate --schema=./db/pg-dev/schema.prisma",
15
+ "prisma:format": "prisma format --schema=./db/pg-dev/schema.prisma",
16
+ "prisma:version": "prisma version",
17
+ "prisma:debug": "prisma debug",
18
+ "prisma:help": "prisma --help",
19
+ "to-exe": "bun build --compile --outfile ../unix_server index.ts"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "latest",
23
+ "@types/pg": "8.15.2"
24
+ },
25
+ "peerDependencies": {
26
+ "typescript": "^5"
27
+ },
28
+ "dependencies": {
29
+ "@prisma/adapter-pg": "6.8.2",
30
+ "@prisma/client": "6.8.2",
31
+ "fastest-validator": "1.19.1",
32
+ "kleur": "^4.1.5",
33
+ "pg": "8.16.0",
34
+ "prisma": "6.8.2",
35
+ "yaml": "^2.8.0"
36
+ },
37
+ "trustedDependencies": [
38
+ "better-sqlite3",
39
+ "fastest-validator",
40
+ "pg",
41
+ "prisma",
42
+ "@prisma/client",
43
+ "@prisma/adapter-pg"
44
+ ]
45
+ }
@@ -0,0 +1,26 @@
1
+ // import { MorioOrm } from "./orm";
2
+ import { parseConfigs } from "./lib/util";
3
+ import { MorioValidator } from "./validator";
4
+
5
+ const configs = await parseConfigs();
6
+
7
+ console.log("Hi, my name is Morio");
8
+ console.log(JSON.stringify({ configs }));
9
+
10
+ const validator = new MorioValidator();
11
+ // const orm = await new MorioOrm().init();
12
+
13
+ export const plugins = [
14
+ {
15
+ name: "validator",
16
+ command: "validate",
17
+ description: "A validator plugin",
18
+ run: (options: any) => validator.run(options),
19
+ },
20
+ // {
21
+ // name: "orm",
22
+ // command: "database",
23
+ // description: "A orm plugin",
24
+ // run: (options: any) => orm.run(options),
25
+ // },
26
+ ];
@@ -0,0 +1,131 @@
1
+ datasource db {
2
+ provider = "postgres"
3
+ url = env("UWAZI_SPRING_DATABASE_URL")
4
+ }
5
+
6
+ generator client {
7
+ provider = "prisma-client-js"
8
+ }
9
+
10
+ model AuditTrail {
11
+ id Int @id @default(autoincrement())
12
+ log_ref String @db.VarChar(50)
13
+ user_name String @db.VarChar(50)
14
+ active_page String @db.VarChar(255)
15
+ activity_done String @db.VarChar(255)
16
+ system_module String @db.VarChar(100)
17
+ audit_date DateTime @default(now())
18
+ ip_address String @db.VarChar(50)
19
+ @@map("audit_trails")
20
+ }
21
+
22
+ model Treatment {
23
+ id Int @id @default(autoincrement())
24
+ claims Claim[]
25
+ // Add other Treatment fields as needed
26
+ @@map("treatments")
27
+ }
28
+
29
+ model Role {
30
+ id Int @id @default(autoincrement())
31
+ users User[]
32
+ // Add other Role fields as needed
33
+ @@map("roles")
34
+ }
35
+ model Claim {
36
+ id Int @id @default(autoincrement())
37
+ claim_reference String @db.VarChar(50)
38
+ @@index([claim_reference])
39
+ invoice_number String @unique @db.VarChar(50)
40
+ @@index([invoice_number])
41
+ policy_number String @db.VarChar(50)
42
+ invoice_amount Decimal @db.Decimal(15, 2)
43
+ min_cost Decimal @db.Decimal(15, 2)
44
+ maximum_cost Decimal @db.Decimal(15, 2)
45
+ risk_classification String @db.VarChar(50)
46
+ claim_narration String @db.VarChar(500)
47
+ created_by String @db.VarChar(100)
48
+ approved_by String @db.VarChar(100)
49
+ created_at DateTime @default(now())
50
+ date_approved DateTime
51
+ approval_remarks String @db.VarChar(255)
52
+ status_code String @db.VarChar(50)
53
+ status_description String
54
+ treatment_id Int
55
+ treatment Treatment @relation(fields: [treatment_id], references: [id], onDelete: Cascade)
56
+ hospital_id Int
57
+ hospital_claims Organisation @relation("HospitalClaims", fields: [hospital_id], references: [id], onDelete: Cascade)
58
+ insured_id Int
59
+ insurer_claims Organisation @relation("InsuredClaims", fields: [insured_id], references: [id], onDelete: Cascade)
60
+ @@map("claims")
61
+ }
62
+
63
+ model Organisation {
64
+ id Int @id @default(autoincrement())
65
+ code String @unique @db.VarChar(50)
66
+ @@index([code])
67
+ name String @db.VarChar(255)
68
+ type String @db.VarChar(50)
69
+ kra_pin String? @db.VarChar(50)
70
+ head_quarter_location String?
71
+ email_address String? @unique @db.VarChar(255)
72
+ mobile_number String? @db.VarChar(20)
73
+ hospital_category String? @db.VarChar(50)
74
+ created_at DateTime @default(now())
75
+ updated_at DateTime
76
+ created_by String @db.VarChar(100)
77
+ approved_by String @db.VarChar(100)
78
+ approved_at DateTime @default(now())
79
+ status_code String @db.VarChar(50)
80
+ status_description String?
81
+ hospital_claims Claim[] @relation("HospitalClaims")
82
+ insured_claims Claim[] @relation("InsuredClaims")
83
+ users User[] @relation("OrganisationUsers")
84
+ @@map("organisations")
85
+ }
86
+
87
+ model User {
88
+ id Int @id @default(autoincrement())
89
+ user_name String @unique @db.VarChar(100)
90
+ @@index([user_name])
91
+ first_name String @db.VarChar(100)
92
+ second_name String? @db.VarChar(100)
93
+ last_name String @db.VarChar(100)
94
+ national_id String? @unique @db.VarChar(50)
95
+ gender String? @db.VarChar(10)
96
+ dob DateTime?
97
+ mobile_number String? @unique @db.VarChar(20)
98
+ email String @unique @db.VarChar(255)
99
+ @@index([email])
100
+ password_hash String @db.VarChar(255)
101
+ last_login_date DateTime?
102
+ login_trials Int? @default(3)
103
+ login_ip String? @db.VarChar(50)
104
+ created_by String @db.VarChar(100)
105
+ created_at DateTime? @default(now())
106
+ approved_by String? @db.VarChar(100)
107
+ approved_at DateTime? @default(now())
108
+ updated_at DateTime?
109
+ status_code String @db.VarChar(50)
110
+ status_description String?
111
+
112
+ // relations
113
+ role_id Int
114
+ role Role @relation(fields: [role_id], references: [id])
115
+
116
+ organisation_id Int
117
+ organisation Organisation @relation("OrganisationUsers", fields: [organisation_id], references: [id])
118
+
119
+ // indexes
120
+ @@index([user_name, email], name: "user_name_email_idx")
121
+ @@unique([user_name, email])
122
+
123
+
124
+ @@index([mobile_number, national_id], name: "mobile_national_id_idx")
125
+
126
+ @@index([last_login_date, status_code], name: "login_perf_idx")
127
+
128
+ // mapped
129
+ @@map("users")
130
+ }
131
+