@rws-framework/db 1.0.1

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.
package/.bin/add-v.sh ADDED
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+
3
+ export VERSION=$1
4
+
5
+ git add .
6
+ git commit -m "v$VERSION"
7
+ git tag $VERSION
8
+ git push
9
+ git push origin $VERSION
10
+ npm publish
package/.bin/emerge.sh ADDED
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+
3
+ if ! command -v emerge >/dev/null 2>&1; then
4
+ echo "emerge command does not exist. Installing."
5
+ apt-get install graphviz graphviz-dev
6
+ pip install emerge-viz
7
+ fi
8
+
9
+ mkdir -p ./.emerge-vis-output/rws-server
10
+
11
+ emerge -c ./.emerge-typescript-template.yaml
package/.eslintrc.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "env": {
3
+ "browser": false,
4
+ "es2021": true
5
+ },
6
+ "plugins": ["@typescript-eslint", "unused-imports"],
7
+ "extends": [
8
+ "eslint:recommended",
9
+ "plugin:@typescript-eslint/recommended"
10
+ ],
11
+ "parser": "@typescript-eslint/parser",
12
+ "parserOptions": {
13
+ "ecmaVersion": "latest",
14
+ "sourceType": "module",
15
+ "project": "./tsconfig.json"
16
+ },
17
+ "ignorePatterns": ["*.js"],
18
+ "rules": {
19
+ "no-unused-vars": "off",
20
+ "no-case-declarations": "off",
21
+ "no-prototype-builtins": "off",
22
+ "unused-imports/no-unused-imports": "error",
23
+ "unused-imports/no-unused-vars": ["warn", { "vars": "all", "args": "none"}],
24
+ "@typescript-eslint/no-var-requires": "off",
25
+ "@typescript-eslint/semi": ["error", "always"],
26
+ "@typescript-eslint/no-unused-vars": "off",
27
+ "@typescript-eslint/no-explicit-any": "off",
28
+ "@typescript-eslint/no-this-alias": "off",
29
+ "@typescript-eslint/type-annotation-spacing": ["error", {
30
+ "before": false,
31
+ "after": true,
32
+ "overrides": {
33
+ "arrow": {
34
+ "before": true, // Space before the arrow function's arrow
35
+ "after": true // Space after the arrow function's arrow
36
+ }
37
+ }
38
+ }],
39
+ "indent": [
40
+ "error",
41
+ 4
42
+ ],
43
+ "linebreak-style": [
44
+ "error",
45
+ "unix"
46
+ ],
47
+ "quotes": [
48
+ "error",
49
+ "single"
50
+ ],
51
+ "semi": "off"
52
+ }
53
+ }
package/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # Models
2
+
3
+ RWS models are converted to Prisma schemas and are wrapping around generated PrismaClient providing complete typing and better Relation handling + TimeSeries in future minor versions.
4
+
5
+ ## Models index file
6
+
7
+ ```typescript
8
+ import ApiKey from "./ApiKey";
9
+ import User from "./User";
10
+
11
+ export const models = [ User, ApiKey];
12
+ ```
13
+
14
+
15
+ ## Example user model
16
+
17
+ ```typescript
18
+ import { RWSannotations, RWSModel } from '@rws-framework/server';
19
+
20
+ import IUser from './interfaces/IUser';
21
+ import 'reflect-metadata';
22
+
23
+ import ApiKey from './ApiKey';
24
+ import IApiKey from './interfaces/IApiKey';
25
+ const { RWSTrackType, InverseRelation } = RWSannotations.modelAnnotations;
26
+
27
+ class User extends RWSModel<User> implements IUser {
28
+ @RWSTrackType(String)
29
+ username: string;
30
+
31
+ @RWSTrackType(String) // Can also handle Object and Number
32
+ passwd: string;
33
+
34
+ @RWSTrackType(Boolean)
35
+ active: boolean;
36
+
37
+ @RWSTrackType(Date, { required: true })
38
+ created_at: Date;
39
+
40
+ @RWSTrackType(Date)
41
+ updated_at: Date;
42
+
43
+ /**
44
+ * Every relation and inverse relation decorator
45
+ * uses arrow function model passing
46
+ **/
47
+ @InverseRelation(() => ApiKey, () => User)
48
+ apiKeys: IApiKey[];
49
+
50
+ static _collection = 'user';
51
+
52
+ static _RELATIONS = {
53
+ transcriptions: true,
54
+ apiKeys: true
55
+ };
56
+
57
+ static _CUT_KEYS = ['passwd'];
58
+
59
+ constructor(data?: IUser) {
60
+ super(data);
61
+
62
+ if(!this.created_at){
63
+ this.created_at = new Date();
64
+ }
65
+ }
66
+
67
+ addMessage(message: string){
68
+ this.messages.push(message);
69
+ }
70
+ }
71
+
72
+ //Must export default for automated DI / build work.
73
+ export default User;
74
+ ```
75
+
76
+ ## Relations
77
+
78
+ ***Basic many to one relation***
79
+ ```typescript
80
+ import { RWSannotations, RWSModel } from '@rws-framework/server';
81
+
82
+ import 'reflect-metadata';
83
+ import User from './User';
84
+ import IApiKey from './interfaces/IApiKey';
85
+ const { RWSTrackType, Relation } = RWSannotations.modelAnnotations;
86
+
87
+ class ApiKey extends RWSModel<ApiKey> implements IApiKey {
88
+ static _RELATIONS = {
89
+ user: true,
90
+ };
91
+
92
+ @Relation(() => User, true) // second attribute is required = false
93
+ user: User;
94
+
95
+ @RWSTrackType(Object)
96
+ keyval: string;
97
+
98
+ @RWSTrackType(Date, { required: true })
99
+ created_at: Date;
100
+
101
+ @RWSTrackType(Date)
102
+ updated_at: Date;
103
+
104
+ static _collection = 'api_keys';
105
+
106
+ constructor(data?: IApiKey) {
107
+ super(data);
108
+
109
+ if(!this.created_at){
110
+ this.created_at = new Date();
111
+ }
112
+
113
+ this.updated_at = new Date();
114
+ }
115
+ }
116
+
117
+ export default ApiKey;
118
+ ```
119
+
120
+ ***Relation decorator*** (many-to-one)
121
+
122
+ ```typescript
123
+ import 'reflect-metadata';
124
+ import Model, { OpModelType } from '../_model';
125
+
126
+ interface IRelationOpts {
127
+ required?: boolean
128
+ key?: string
129
+ relationField?: string
130
+ relatedToField?: string
131
+ relatedTo: OpModelType<Model<any>>
132
+ }
133
+
134
+ function Relation(theModel: () => OpModelType<Model<any>>, required: boolean = false, relationField: string = null, relatedToField: string = 'id') {
135
+ return function(target: any, key: string) {
136
+ // Store the promise in metadata immediately
137
+ const metadataPromise = Promise.resolve().then(() => {
138
+ const relatedTo = theModel();
139
+ const metaOpts: IRelationOpts = {required, relatedTo, relatedToField};
140
+ if(!relationField){
141
+ metaOpts.relationField = relatedTo._collection + '_id';
142
+ } else{
143
+ metaOpts.relationField = relationField;
144
+ }
145
+ metaOpts.key = key;
146
+ return metaOpts;
147
+ });
148
+
149
+ // Store both the promise and the key information
150
+ Reflect.defineMetadata(`Relation:${key}`, {
151
+ promise: metadataPromise,
152
+ key
153
+ }, target);
154
+ };
155
+ }
156
+
157
+
158
+ export default Relation;
159
+ export {IRelationOpts};
160
+ ```
161
+
162
+ ***Inverse relation decorator*** (one-to-many)
163
+ ```typescript
164
+ import 'reflect-metadata';
165
+ import Model, { OpModelType } from '../_model';
166
+
167
+ interface InverseRelationOpts{
168
+ key: string,
169
+ inversionModel: OpModelType<Model<any>>,
170
+ foreignKey: string
171
+ }
172
+
173
+ function InverseRelation(inversionModel: () => OpModelType<Model<any>>, sourceModel: () => OpModelType<Model<any>>, foreignKey: string = null) {
174
+ return function(target: any, key: string) {
175
+ // Store the promise in metadata immediately
176
+ const metadataPromise = Promise.resolve().then(() => {
177
+ const model = inversionModel();
178
+ const source = sourceModel();
179
+
180
+ const metaOpts: InverseRelationOpts = {
181
+ key,
182
+ inversionModel: model,
183
+ foreignKey: foreignKey ? foreignKey : `${source._collection}_id`
184
+ };
185
+
186
+ return metaOpts;
187
+ });
188
+
189
+ // Store both the promise and the key information
190
+ Reflect.defineMetadata(`InverseRelation:${key}`, {
191
+ promise: metadataPromise,
192
+ key
193
+ }, target);
194
+ };
195
+ }
196
+
197
+ export default InverseRelation;
198
+ export {InverseRelationOpts};
199
+ ```
200
+
201
+
202
+ ## RWS Model to prisma conversion
203
+
204
+
205
+ ### Init CLI
206
+ Basic CLI command that executes **generateModelSections()** from conversion script is
207
+
208
+ ```bash
209
+ yarn rws init
210
+ ```
211
+
212
+
213
+ Code for RWS to prisma conversion from "@rws-framework/server" package:
214
+
215
+ ```typescript
216
+ async function generateModelSections<T extends Model<T>>(model: OpModelType<T>): Promise<string> {
217
+ let section = '';
218
+ const modelMetadatas: Record<string, {annotationType: string, metadata: any}> = await Model.getModelAnnotations(model);
219
+
220
+ const modelName: string = (model as any)._collection;
221
+
222
+ section += `model ${modelName} {\n`;
223
+ section += '\tid String @map("_id") @id @default(auto()) @db.ObjectId\n';
224
+
225
+ for (const key in modelMetadatas) {
226
+ const modelMetadata: IMetaOpts = modelMetadatas[key].metadata;
227
+ const requiredString = modelMetadata.required ? '' : '?';
228
+ const annotationType: string = modelMetadatas[key].annotationType;
229
+
230
+ if(key === 'id'){
231
+ continue;
232
+ }
233
+
234
+ if(annotationType === 'Relation'){
235
+ const relatedModel = modelMetadata.relatedTo as OpModelType<T>;
236
+ // Handle direct relation (many-to-one or one-to-one)
237
+ section += `\t${key} ${relatedModel._collection}${requiredString} @relation("${modelName}_${relatedModel._collection}", fields: [${modelMetadata.relationField}], references: [${modelMetadata.relatedToField}], onDelete: Cascade)\n`;
238
+ section += `\t${modelMetadata.relationField} String${requiredString} @db.ObjectId\n`;
239
+ } else if (annotationType === 'InverseRelation'){
240
+ // Handle inverse relation (one-to-many or one-to-one)
241
+ section += `\t${key} ${modelMetadata.inversionModel._collection}[] @relation("${modelMetadata.inversionModel._collection}_${modelName}")\n`;
242
+ } else if (annotationType === 'InverseTimeSeries'){
243
+ section += `\t${key} String[] @db.ObjectId\n`;
244
+ } else if (annotationType === 'TrackType'){
245
+ const tags: string[] = modelMetadata.tags.map((item: string) => '@' + item);
246
+ section += `\t${key} ${toConfigCase(modelMetadata)}${requiredString} ${tags.join(' ')}\n`;
247
+ }
248
+ }
249
+
250
+ section += '}\n';
251
+ return section;
252
+ }
253
+
254
+ function toConfigCase(modelType: any): string {
255
+ const type = modelType.type;
256
+ const input = type.name;
257
+
258
+ if(input == 'Number'){
259
+ return 'Int';
260
+ }
261
+
262
+ if(input == 'Object'){
263
+ return 'Json';
264
+ }
265
+
266
+ if(input == 'Date'){
267
+ return 'DateTime';
268
+ }
269
+
270
+
271
+ const firstChar = input.charAt(0).toUpperCase();
272
+ const restOfString = input.slice(1);
273
+ return firstChar + restOfString;
274
+ }
275
+ ```
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@rws-framework/db",
3
+ "private": false,
4
+ "version": "1.0.1",
5
+ "description": "",
6
+ "main": "src/index.ts",
7
+ "scripts": {},
8
+ "author": "papablack",
9
+ "license": "ISC",
10
+ "dependencies": {
11
+ "@rws-framework/console": "*",
12
+ "@prisma/client": "^5.1.1",
13
+ "@mongodb-js/zstd": "^1.2.0",
14
+ "mongodb": "^6.8.1",
15
+ "mongodb-client-encryption": "^6.1.1",
16
+ "xml2js": "^0.6.2"
17
+ },
18
+ "devDependencies": {
19
+ "@types/xml2js": "^0.4.14"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/rws-framework/db.git"
24
+ }
25
+ }
@@ -0,0 +1,35 @@
1
+ import 'reflect-metadata';
2
+ import { RWSModel, OpModelType } from '../models/_model';
3
+
4
+ interface InverseRelationOpts{
5
+ key: string,
6
+ inversionModel: OpModelType<RWSModel<any>>,
7
+ foreignKey: string
8
+ }
9
+
10
+ function InverseRelation(inversionModel: () => OpModelType<RWSModel<any>>, sourceModel: () => OpModelType<RWSModel<any>>, foreignKey: string = null) {
11
+ return function(target: any, key: string) {
12
+ // Store the promise in metadata immediately
13
+ const metadataPromise = Promise.resolve().then(() => {
14
+ const model = inversionModel();
15
+ const source = sourceModel();
16
+
17
+ const metaOpts: InverseRelationOpts = {
18
+ key,
19
+ inversionModel: model,
20
+ foreignKey: foreignKey ? foreignKey : `${source._collection}_id`
21
+ };
22
+
23
+ return metaOpts;
24
+ });
25
+
26
+ // Store both the promise and the key information
27
+ Reflect.defineMetadata(`InverseRelation:${key}`, {
28
+ promise: metadataPromise,
29
+ key
30
+ }, target);
31
+ };
32
+ }
33
+
34
+ export default InverseRelation;
35
+ export {InverseRelationOpts};
@@ -0,0 +1,22 @@
1
+ import 'reflect-metadata';
2
+
3
+ interface InverseTimeSeriesOpts{
4
+ timeSeriesModel: string
5
+ hydrationField: string
6
+ }
7
+
8
+ function InverseTimeSeries(timeSeriesModel: string, hydrationField: string) {
9
+
10
+ const metaOpts: InverseTimeSeriesOpts = {
11
+ timeSeriesModel: timeSeriesModel,
12
+ hydrationField: hydrationField
13
+ };
14
+
15
+
16
+ return function(target: any, key: string) {
17
+ Reflect.defineMetadata(`InverseTimeSeries:${key}`, metaOpts, target);
18
+ };
19
+ }
20
+
21
+ export default InverseTimeSeries;
22
+ export {InverseTimeSeriesOpts};
@@ -0,0 +1,37 @@
1
+ import 'reflect-metadata';
2
+ import { RWSModel, OpModelType } from '../models/_model';
3
+
4
+ interface IRelationOpts {
5
+ required?: boolean
6
+ key?: string
7
+ relationField?: string
8
+ relatedToField?: string
9
+ relatedTo: OpModelType<RWSModel<any>>
10
+ }
11
+
12
+ function Relation(theModel: () => OpModelType<RWSModel<any>>, required: boolean = false, relationField: string = null, relatedToField: string = 'id') {
13
+ return function(target: any, key: string) {
14
+ // Store the promise in metadata immediately
15
+ const metadataPromise = Promise.resolve().then(() => {
16
+ const relatedTo = theModel();
17
+ const metaOpts: IRelationOpts = {required, relatedTo, relatedToField};
18
+ if(!relationField){
19
+ metaOpts.relationField = relatedTo._collection + '_id';
20
+ } else{
21
+ metaOpts.relationField = relationField;
22
+ }
23
+ metaOpts.key = key;
24
+ return metaOpts;
25
+ });
26
+
27
+ // Store both the promise and the key information
28
+ Reflect.defineMetadata(`Relation:${key}`, {
29
+ promise: metadataPromise,
30
+ key
31
+ }, target);
32
+ };
33
+ }
34
+
35
+
36
+ export default Relation;
37
+ export {IRelationOpts};
@@ -0,0 +1,56 @@
1
+ import 'reflect-metadata';
2
+ import { OpModelType } from '../models/_model';
3
+
4
+ interface ITrackerOpts{
5
+ required?: boolean,
6
+ relationField?: string
7
+ relatedToField?: string,
8
+ relatedTo?: OpModelType<any>,
9
+ inversionModel?: OpModelType<any>,
10
+ relationName?: string
11
+ }
12
+
13
+ interface IMetaOpts extends ITrackerOpts{
14
+ type: any,
15
+ tags: string[]
16
+ }
17
+
18
+ function TrackType(type: any, opts: ITrackerOpts | null = null, tags: string[] = []) {
19
+ if(!opts){
20
+ opts = {
21
+ required: false
22
+ };
23
+ }
24
+
25
+ const required = opts.required;
26
+
27
+ const metaOpts: IMetaOpts = {type, tags, required};
28
+
29
+ if(opts.relatedToField && opts.relatedTo){
30
+ metaOpts.relatedToField = opts.relatedToField;
31
+ metaOpts.relatedTo = opts.relatedTo;
32
+
33
+ if(!opts.relationField){
34
+ metaOpts.relationField = opts.relatedTo + '_id';
35
+ } else{
36
+ metaOpts.relationField = opts.relationField;
37
+ }
38
+ }
39
+
40
+ if(opts.inversionModel){
41
+ metaOpts.inversionModel = opts.inversionModel;
42
+ }
43
+
44
+ //const resolvedType = typeof type === 'function' ? type() : type;
45
+
46
+ if(type._collection){
47
+ metaOpts.type = (type as any);
48
+ }
49
+
50
+ return function(target: any, key: string) {
51
+ Reflect.defineMetadata(`TrackType:${key}`, metaOpts, target);
52
+ };
53
+ }
54
+
55
+ export default TrackType;
56
+ export {IMetaOpts, ITrackerOpts};
@@ -0,0 +1,8 @@
1
+ import InverseRelation from './InverseRelation';
2
+ import Relation from './Relation';
3
+ import TrackType, { IMetaOpts } from './TrackType';
4
+ import InverseTimeSeries from './InverseTimeSeries';
5
+
6
+ export {
7
+ InverseRelation, Relation, TrackType, InverseTimeSeries, IMetaOpts
8
+ };
@@ -0,0 +1,138 @@
1
+ import { rwsShell, rwsPath } from '@rws-framework/console';
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+
6
+ import { IDbConfigHandler } from '../types/DbConfigHandler';
7
+ import { IMetaOpts, OpModelType, RWSModel } from '../models/_model';
8
+ import TimeSeriesModel from '../models/TimeSeriesModel';
9
+ import { DBService } from '../services/DBService';
10
+
11
+ const log = console.log;
12
+ const workspaceRoot = rwsPath.findRootWorkspacePath();
13
+ const moduleDir = path.resolve(workspaceRoot, 'node_modules', '@rws-framework', 'db');
14
+
15
+ export class DbHelper {
16
+ static async installPrisma(configService: IDbConfigHandler, dbService: DBService, leaveFile = false): Promise<void>
17
+ {
18
+ const dbUrl = configService.get('mongo_url');
19
+ const dbType = 'mongodb';
20
+
21
+ let template: string = `generator client {\n
22
+ provider = "prisma-client-js"\n
23
+ }\n\n`;
24
+
25
+ template += `\ndatasource db {\n
26
+ provider = "${dbType}"\n
27
+ url = env("DATABASE_URL")\n
28
+ }\n\n`;
29
+
30
+ const dbModels: OpModelType<unknown>[] | null = configService.get('db_models');
31
+
32
+ if(dbModels){
33
+
34
+ for (const model of dbModels){
35
+ const modelSection = await DbHelper.generateModelSections(model);
36
+
37
+ template += '\n\n' + modelSection;
38
+
39
+ log('RWS SCHEMA BUILD', chalk.blue('Building DB Model'), model.name);
40
+
41
+ if(RWSModel.isSubclass(model as any, TimeSeriesModel)){
42
+ dbService.collectionExists(model._collection).then((exists: boolean) => {
43
+ if (exists){
44
+ return;
45
+ }
46
+
47
+ log(chalk.green('[RWS Init]') + ` creating TimeSeries type collection from ${model} model`);
48
+
49
+ dbService.createTimeSeriesCollection(model._collection);
50
+ });
51
+ }
52
+ }
53
+
54
+ const schemaDir = path.join(moduleDir, 'prisma');
55
+ const schemaPath = path.join(schemaDir, 'schema.prisma');
56
+
57
+ if(!fs.existsSync(schemaDir)){
58
+ fs.mkdirSync(schemaDir);
59
+ }
60
+
61
+ if(fs.existsSync(schemaPath)){
62
+ fs.unlinkSync(schemaPath);
63
+ }
64
+
65
+ fs.writeFileSync(schemaPath, template);
66
+ process.env.DB_URL = dbUrl;
67
+ const endPrisma = 'npx prisma';
68
+ await rwsShell.runCommand(`${endPrisma} generate --schema=${schemaPath}`, process.cwd());
69
+
70
+ // leaveFile = true;
71
+ log(chalk.green('[RWS Init]') + ' prisma schema generated from ', schemaPath);
72
+
73
+ if(!leaveFile){
74
+ fs.unlinkSync(schemaPath);
75
+ }
76
+ }
77
+ }
78
+
79
+ static async generateModelSections<T extends unknown>(model: OpModelType<T>): Promise<string> {
80
+ let section = '';
81
+ const modelMetadatas: Record<string, {annotationType: string, metadata: any}> = await RWSModel.getModelAnnotations(model);
82
+
83
+ const modelName: string = (model as any)._collection;
84
+
85
+ section += `model ${modelName} {\n`;
86
+ section += '\tid String @map("_id") @id @default(auto()) @db.ObjectId\n';
87
+
88
+ for (const key in modelMetadatas) {
89
+ const modelMetadata: IMetaOpts = modelMetadatas[key].metadata;
90
+ const requiredString = modelMetadata.required ? '' : '?';
91
+ const annotationType: string = modelMetadatas[key].annotationType;
92
+
93
+ if(key === 'id'){
94
+ continue;
95
+ }
96
+
97
+ if(annotationType === 'Relation'){
98
+ const relatedModel = modelMetadata.relatedTo as OpModelType<T>;
99
+ // Handle direct relation (many-to-one or one-to-one)
100
+ section += `\t${key} ${relatedModel._collection}${requiredString} @relation("${modelName}_${relatedModel._collection}", fields: [${modelMetadata.relationField}], references: [${modelMetadata.relatedToField}], onDelete: Cascade)\n`;
101
+ section += `\t${modelMetadata.relationField} String${requiredString} @db.ObjectId\n`;
102
+ } else if (annotationType === 'InverseRelation'){
103
+ // Handle inverse relation (one-to-many or one-to-one)
104
+ section += `\t${key} ${modelMetadata.inversionModel._collection}[] @relation("${modelMetadata.inversionModel._collection}_${modelName}")\n`;
105
+ } else if (annotationType === 'InverseTimeSeries'){
106
+ section += `\t${key} String[] @db.ObjectId\n`;
107
+ } else if (annotationType === 'TrackType'){
108
+ const tags: string[] = modelMetadata.tags.map((item: string) => '@' + item);
109
+ section += `\t${key} ${DbHelper.toConfigCase(modelMetadata)}${requiredString} ${tags.join(' ')}\n`;
110
+ }
111
+ }
112
+
113
+ section += '}\n';
114
+ return section;
115
+ }
116
+
117
+ static toConfigCase(modelType: any): string {
118
+ const type = modelType.type;
119
+ const input = type.name;
120
+
121
+ if(input == 'Number'){
122
+ return 'Int';
123
+ }
124
+
125
+ if(input == 'Object'){
126
+ return 'Json';
127
+ }
128
+
129
+ if(input == 'Date'){
130
+ return 'DateTime';
131
+ }
132
+
133
+
134
+ const firstChar = input.charAt(0).toUpperCase();
135
+ const restOfString = input.slice(1);
136
+ return firstChar + restOfString;
137
+ }
138
+ }