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,469 @@
1
+ import { execSync } from "child_process";
2
+
3
+ // Prisma scalar types
4
+ type PrismaScalarType =
5
+ | "String"
6
+ | "Int"
7
+ | "BigInt"
8
+ | "Float"
9
+ | "Decimal"
10
+ | "Boolean"
11
+ | "DateTime"
12
+ | "Json"
13
+ | "Bytes";
14
+
15
+ // DSL field type
16
+ interface DSLField {
17
+ name: string;
18
+ type: PrismaScalarType;
19
+ primaryKey?: boolean;
20
+ nullable?: boolean;
21
+ autoIncrement?: boolean;
22
+ unique?: boolean;
23
+ default?: string | number | boolean;
24
+ length?: number;
25
+ precision?: number;
26
+ index?: boolean;
27
+ }
28
+
29
+ // DSL relation type
30
+ interface DSLRelation {
31
+ foreignKey?: string;
32
+ relatedTo: string;
33
+ relationType: "ManyToOne" | "OneToMany" | "OneToOne" | "ManyToMany";
34
+ onDelete?: "CASCADE" | "SET NULL" | "RESTRICT" | "NO ACTION";
35
+ relationName?: string;
36
+ }
37
+
38
+ // DSL model type
39
+ interface DSLModel {
40
+ model: string;
41
+ tableName?: string;
42
+ fields: DSLField[];
43
+ relations?: DSLRelation[];
44
+ indexes?: Record<
45
+ string,
46
+ { name: string; fields: string[]; unique?: boolean }
47
+ >;
48
+ }
49
+
50
+ // DSL structure
51
+ interface DSL {
52
+ database: {
53
+ name: string;
54
+ type: string;
55
+ environment?: string;
56
+ enabled?: boolean;
57
+ connectionStringEnv: string;
58
+ };
59
+ schemas: DSLModel[];
60
+ }
61
+
62
+ const validatePrismaSchema = (schemaPath: string) => {
63
+ execSync(`prisma format --schema=${schemaPath}`, { stdio: "inherit" });
64
+ };
65
+
66
+ // Standalone method to convert DSL to Prisma schema
67
+ const dslToPrismaSchema = async ({
68
+ dsl,
69
+ outputPath,
70
+ }: {
71
+ dsl: DSL;
72
+ outputPath: string;
73
+ }): Promise<string> => {
74
+ if (!dsl.database || !dsl.schemas) {
75
+ throw new Error('Invalid DSL: Missing "database" or "schemas" section');
76
+ }
77
+
78
+ let schema = "";
79
+
80
+ schema += `datasource db {\n`;
81
+ schema += ` provider = "${dsl.database.type}"\n`;
82
+ schema += ` url = env("${dsl.database.connectionStringEnv}")\n`;
83
+ schema += `}\n\n`;
84
+
85
+ schema += `generator client {\n`;
86
+ schema += ` provider = "prisma-client-js"\n`;
87
+ schema += `}\n\n`;
88
+
89
+ dsl.schemas.forEach((model: DSLModel) => {
90
+ schema += `model ${model.model} {\n`;
91
+
92
+ // Fields
93
+ model.fields.forEach((field: DSLField) => {
94
+ let fieldDef = ` ${field.name} ${field.type}${
95
+ field.nullable ? "?" : ""
96
+ }`;
97
+
98
+ if (field.primaryKey) {
99
+ fieldDef += " @id";
100
+ if (field.autoIncrement) fieldDef += " @default(autoincrement())";
101
+ }
102
+ if (field.unique) fieldDef += " @unique";
103
+ if (field.default !== undefined) {
104
+ fieldDef += ` @default(${
105
+ field.default === "now" ? "now()" : field.default
106
+ })`;
107
+ }
108
+ // Apply length only to String types as VarChar
109
+ if (field.type === "String" && field.length) {
110
+ fieldDef += ` @db.VarChar(${field.length})`;
111
+ }
112
+ // Apply length and precision to Decimal types
113
+ if (field.type === "Decimal" && (field.length || field.precision)) {
114
+ fieldDef += ` @db.Decimal(${field.length || 15}, ${
115
+ field.precision || 0
116
+ })`;
117
+ }
118
+ if (field.index) {
119
+ schema += `${fieldDef}\n`;
120
+ schema += ` @@index([${field.name}])\n`;
121
+ return;
122
+ }
123
+
124
+ schema += `${fieldDef}\n`;
125
+ });
126
+
127
+ // Relations
128
+ if (model.relations) {
129
+ model.relations.forEach((rel: DSLRelation) => {
130
+ const relationName = rel.relationName
131
+ ? `"${rel.relationName}"`
132
+ : undefined;
133
+ const relatedFieldName = rel.relatedTo.toLowerCase();
134
+
135
+ switch (rel.relationType) {
136
+ case "ManyToOne":
137
+ schema += ` ${rel.foreignKey} Int\n`;
138
+ schema += ` ${relatedFieldName} ${rel.relatedTo} @relation(${
139
+ relationName ? `${relationName}, ` : ""
140
+ }fields: [${rel.foreignKey}], references: [id]${
141
+ rel.onDelete ? `, onDelete: ${rel.onDelete}` : ""
142
+ })\n`;
143
+ break;
144
+ case "OneToMany":
145
+ schema += ` ${relatedFieldName} ${rel.relatedTo}[]${
146
+ relationName ? ` @relation(${relationName})` : ""
147
+ }\n`;
148
+ break;
149
+ case "OneToOne":
150
+ if (rel.foreignKey) {
151
+ schema += ` ${rel.foreignKey} Int @unique\n`;
152
+ schema += ` ${relatedFieldName} ${rel.relatedTo} @relation(${
153
+ relationName ? `${relationName}, ` : ""
154
+ }fields: [${rel.foreignKey}], references: [id]${
155
+ rel.onDelete ? `, onDelete: ${rel.onDelete}` : ""
156
+ })\n`;
157
+ } else {
158
+ schema += ` ${relatedFieldName} ${rel.relatedTo}?${
159
+ relationName ? ` @relation(${relationName})` : ""
160
+ }\n`;
161
+ }
162
+ break;
163
+ case "ManyToMany":
164
+ throw new Error(
165
+ "ManyToMany relations require explicit join table definition in DSL"
166
+ );
167
+ default:
168
+ throw new Error(`Unsupported relation type: ${rel.relationType}`);
169
+ }
170
+ });
171
+ }
172
+
173
+ // Indexes
174
+ if (model.indexes) {
175
+ Object.values(model.indexes).forEach((idx) => {
176
+ const fields = idx.fields.join(", ");
177
+ schema += ` @@index([${fields}], name: "${idx.name}"${
178
+ idx.unique ? ", unique: true" : ""
179
+ })\n`;
180
+ });
181
+ }
182
+
183
+ // map table names
184
+ if (model.tableName) {
185
+ schema += ` @@map("${model.tableName}")\n`;
186
+ }
187
+
188
+ schema += `}\n\n`;
189
+ });
190
+
191
+ try {
192
+ await Bun.write(outputPath, schema, { createPath: true });
193
+ validatePrismaSchema(outputPath);
194
+ } catch (error: any) {
195
+ throw new Error(`Failed to format schema: ${error.message}`);
196
+ }
197
+
198
+ return schema;
199
+ };
200
+
201
+ // Example DSL with inverse OneToMany and custom relation names
202
+ await dslToPrismaSchema({
203
+ dsl: {
204
+ database: {
205
+ name: "uwazi_spring_db",
206
+ type: "postgres",
207
+ environment: "prod",
208
+ enabled: true,
209
+ connectionStringEnv: "UWAZI_SPRING_DATABASE_URL",
210
+ },
211
+ schemas: [
212
+ {
213
+ model: "AuditTrail",
214
+ tableName: "audit_trails",
215
+ fields: [
216
+ {
217
+ name: "id",
218
+ type: "Int",
219
+ primaryKey: true,
220
+ nullable: false,
221
+ autoIncrement: true,
222
+ },
223
+ { name: "log_ref", type: "String", length: 50, nullable: false },
224
+ { name: "user_name", type: "String", length: 50 },
225
+ { name: "active_page", type: "String", length: 255 },
226
+ { name: "activity_done", type: "String", length: 255 },
227
+ { name: "system_module", type: "String", length: 100 },
228
+ { name: "audit_date", type: "DateTime", default: "now" },
229
+ { name: "ip_address", type: "String", length: 50 },
230
+ ],
231
+ },
232
+ {
233
+ model: "Claim",
234
+ tableName: "claims",
235
+ fields: [
236
+ {
237
+ name: "id",
238
+ type: "Int",
239
+ primaryKey: true,
240
+ nullable: false,
241
+ autoIncrement: true,
242
+ },
243
+ {
244
+ name: "claim_reference",
245
+ type: "String",
246
+ length: 50,
247
+ nullable: false,
248
+ index: true,
249
+ },
250
+ {
251
+ name: "invoice_number",
252
+ type: "String",
253
+ length: 50,
254
+ unique: true,
255
+ nullable: false,
256
+ index: true,
257
+ },
258
+ { name: "policy_number", type: "String", length: 50 },
259
+ {
260
+ name: "invoice_amount",
261
+ type: "Decimal",
262
+ nullable: false,
263
+ length: 15,
264
+ precision: 2,
265
+ },
266
+ { name: "min_cost", type: "Decimal", length: 15, precision: 2 },
267
+ { name: "maximum_cost", type: "Decimal", length: 15, precision: 2 },
268
+ { name: "risk_classification", type: "String", length: 50 },
269
+ { name: "claim_narration", type: "String", length: 500 },
270
+ { name: "created_by", type: "String", length: 100 },
271
+ { name: "approved_by", type: "String", length: 100 },
272
+ { name: "created_at", type: "DateTime", default: "now" },
273
+ { name: "date_approved", type: "DateTime" },
274
+ { name: "approval_remarks", type: "String", length: 255 },
275
+ { name: "status_code", type: "String", length: 50 },
276
+ { name: "status_description", type: "String" }, // Changed from Text to String
277
+ ],
278
+ relations: [
279
+ {
280
+ foreignKey: "treatment_id",
281
+ relatedTo: "Treatment",
282
+ relationType: "ManyToOne",
283
+ onDelete: "CASCADE",
284
+ },
285
+ {
286
+ foreignKey: "hospital_id",
287
+ relatedTo: "Organisation",
288
+ relationType: "ManyToOne",
289
+ onDelete: "CASCADE",
290
+ relationName: "HospitalClaims",
291
+ },
292
+ {
293
+ foreignKey: "insured_id",
294
+ relatedTo: "Organisation",
295
+ relationType: "ManyToOne",
296
+ onDelete: "CASCADE",
297
+ relationName: "InsuredClaims",
298
+ },
299
+ ],
300
+ },
301
+ {
302
+ model: "Organisation",
303
+ tableName: "organisations",
304
+ fields: [
305
+ {
306
+ name: "id",
307
+ type: "Int",
308
+ primaryKey: true,
309
+ autoIncrement: true,
310
+ nullable: false,
311
+ },
312
+ {
313
+ name: "code",
314
+ type: "String",
315
+ length: 50,
316
+ unique: true,
317
+ nullable: false,
318
+ index: true,
319
+ },
320
+ { name: "name", type: "String", length: 255, nullable: false },
321
+ { name: "type", type: "String", length: 50, nullable: false },
322
+ { name: "kra_pin", type: "String", length: 50, nullable: true },
323
+ { name: "head_quarter_location", type: "String", nullable: true },
324
+ {
325
+ name: "email_address",
326
+ type: "String",
327
+ length: 255,
328
+ unique: true,
329
+ nullable: true,
330
+ },
331
+ { name: "mobile_number", type: "String", length: 20, nullable: true },
332
+ {
333
+ name: "hospital_category",
334
+ type: "String",
335
+ length: 50,
336
+ nullable: true,
337
+ },
338
+ { name: "created_at", type: "DateTime", default: "now" },
339
+ { name: "updated_at", type: "DateTime" },
340
+ { name: "created_by", type: "String", length: 100 },
341
+ { name: "approved_by", type: "String", length: 100 },
342
+ { name: "approved_at", type: "DateTime", default: "now" },
343
+ { name: "status_code", type: "String", length: 50, nullable: false },
344
+ { name: "status_description", type: "String", nullable: true }, // Changed from Text to String
345
+ ],
346
+ relations: [
347
+ {
348
+ relatedTo: "Claim",
349
+ relationType: "OneToMany",
350
+ relationName: "HospitalClaims",
351
+ },
352
+ {
353
+ relatedTo: "Claim",
354
+ relationType: "OneToMany",
355
+ relationName: "InsuredClaims",
356
+ },
357
+ {
358
+ relatedTo: "User",
359
+ relationType: "OneToMany",
360
+ relationName: "OrganisationUsers",
361
+ },
362
+ ],
363
+ },
364
+ {
365
+ model: "User",
366
+ tableName: "users",
367
+ fields: [
368
+ {
369
+ name: "id",
370
+ type: "Int",
371
+ primaryKey: true,
372
+ nullable: false,
373
+ autoIncrement: true,
374
+ },
375
+ {
376
+ name: "user_name",
377
+ type: "String",
378
+ length: 100,
379
+ unique: true,
380
+ nullable: false,
381
+ index: true,
382
+ },
383
+ { name: "first_name", type: "String", length: 100, nullable: false },
384
+ { name: "second_name", type: "String", length: 100, nullable: true },
385
+ { name: "last_name", type: "String", length: 100, nullable: false },
386
+ {
387
+ name: "national_id",
388
+ type: "String",
389
+ length: 50,
390
+ nullable: true,
391
+ unique: true,
392
+ },
393
+ { name: "gender", type: "String", length: 10, nullable: true },
394
+ { name: "dob", type: "DateTime", nullable: true }, // Changed from Date to DateTime
395
+ {
396
+ name: "mobile_number",
397
+ type: "String",
398
+ length: 20,
399
+ unique: true,
400
+ nullable: true,
401
+ },
402
+ {
403
+ name: "email",
404
+ type: "String",
405
+ length: 255,
406
+ unique: true,
407
+ nullable: false,
408
+ index: true,
409
+ },
410
+ {
411
+ name: "password_hash",
412
+ type: "String",
413
+ length: 255,
414
+ nullable: false,
415
+ },
416
+ { name: "last_login_date", type: "DateTime", nullable: true },
417
+ { name: "login_trials", type: "Int", default: 3, nullable: true },
418
+ { name: "login_ip", type: "String", length: 50, nullable: true },
419
+ { name: "created_by", type: "String", length: 100, nullable: false },
420
+ {
421
+ name: "created_at",
422
+ type: "DateTime",
423
+ default: "now",
424
+ nullable: true,
425
+ },
426
+ { name: "approved_by", type: "String", length: 100, nullable: true },
427
+ {
428
+ name: "approved_at",
429
+ type: "DateTime",
430
+ default: "now",
431
+ nullable: true,
432
+ },
433
+ { name: "updated_at", type: "DateTime", nullable: true },
434
+ { name: "status_code", type: "String", length: 50, nullable: false },
435
+ { name: "status_description", type: "String", nullable: true }, // Changed from Text to String
436
+ ],
437
+ relations: [
438
+ {
439
+ foreignKey: "role_id",
440
+ relatedTo: "Role",
441
+ relationType: "ManyToOne",
442
+ },
443
+ {
444
+ foreignKey: "organisation_id",
445
+ relatedTo: "Organisation",
446
+ relationType: "ManyToOne",
447
+ relationName: "OrganisationUsers",
448
+ },
449
+ ],
450
+ indexes: {
451
+ user_name_email_idx: {
452
+ name: "user_name_email_idx",
453
+ fields: ["user_name", "email"],
454
+ unique: true,
455
+ },
456
+ mobile_national_id_idx: {
457
+ name: "mobile_national_id_idx",
458
+ fields: ["mobile_number", "national_id"],
459
+ },
460
+ login_perf_idx: {
461
+ name: "login_perf_idx",
462
+ fields: ["last_login_date", "status_code"],
463
+ },
464
+ },
465
+ },
466
+ ],
467
+ },
468
+ outputPath: "./schema.prisma",
469
+ });
@@ -0,0 +1,51 @@
1
+ import path from "path";
2
+ import YAML from "yaml";
3
+
4
+ type ParsedConfig = {
5
+ parsed: Record<string, any[]>;
6
+ folder: string;
7
+ mask: string;
8
+ };
9
+
10
+ const parseConfigs = async (): Promise<ParsedConfig> => {
11
+ const configFolder = path.join(process.cwd(), "config");
12
+ const mask = "**/*{database,.database}.yml";
13
+
14
+ // Get all yml files in one glob which accepts database.yml or *.database.yml
15
+ const glob = new Bun.Glob(mask);
16
+ const files = await Array.fromAsync(glob.scan(configFolder));
17
+
18
+ // Group files by their parent directory
19
+ const filesByFolder = files.reduce((acc, file) => {
20
+ const [folder] = file.split("/");
21
+ if (!folder) return acc;
22
+
23
+ if (!acc[folder]) acc[folder] = [];
24
+ acc[folder].push(file);
25
+ return acc;
26
+ }, {} as Record<string, string[]>);
27
+
28
+ // Process each folder's files in parallel
29
+ const results = await Promise.all(
30
+ Object.entries(filesByFolder).map(async ([folder, files]) => {
31
+ // Process all files in this folder in parallel
32
+ const configs = await Promise.all(
33
+ files.map(async (file) => {
34
+ const fullPath = path.join(configFolder, file);
35
+ const content = await Bun.file(fullPath).text();
36
+ return YAML.parse(content);
37
+ })
38
+ );
39
+ return [folder, configs] as const;
40
+ })
41
+ );
42
+
43
+ // Convert results back to record
44
+ return {
45
+ folder: configFolder,
46
+ mask,
47
+ parsed: Object.fromEntries(results),
48
+ };
49
+ };
50
+
51
+ export { parseConfigs };
@@ -0,0 +1,32 @@
1
+ import { PrismaPg } from "@prisma/adapter-pg";
2
+
3
+ export class MorioOrm {
4
+ private prisma: any;
5
+
6
+ constructor() {}
7
+
8
+ public async init(): Promise<MorioOrm> {
9
+ // we need to require the prisma client dynamically
10
+ const connectionString: string = process.env.DATABASE_URL || "";
11
+ const { PrismaClient } = await import("../db/pg-dev/client");
12
+ const adapter = new PrismaPg({ connectionString });
13
+ this.prisma = new PrismaClient({ adapter });
14
+
15
+ return this;
16
+ }
17
+
18
+ public async run(options: any) {
19
+ try {
20
+ if (!this.prisma) throw new Error("ORM not initialized");
21
+ return await (this.prisma as any)[options.model][options.action](
22
+ options.payload
23
+ );
24
+ } catch (error) {
25
+ if (error instanceof Error) {
26
+ return {
27
+ error: error.message,
28
+ };
29
+ }
30
+ }
31
+ }
32
+ }