├── .gitignore ├── DEFAULT_CONFIG.js ├── DOCS.md ├── Field.js ├── LICENSE ├── Model.js ├── README.md ├── Relation.js ├── TODO.md ├── index.js ├── package-lock.json ├── package.json ├── path.js ├── types ├── Field.d.ts ├── Model.d.ts ├── Relation.d.ts └── index.d.ts └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* -------------------------------------------------------------------------------- /DEFAULT_CONFIG.js: -------------------------------------------------------------------------------- 1 | export default { 2 | datapath: "./data", 3 | prettyfy: false, 4 | indent: 2, 5 | }; 6 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Table of Contents 4 | 5 | - [Installation](#installation) 6 | - [Configuration](#configuration) 7 | - [Usage](#usage) 8 | - [Model](#model) 9 | - [Model Definition](#model-definition) 10 | - [Methods](#methods) 11 | - [Instance](#instance) 12 | - [Methods](#methods-1) 13 | - [Properties](#properties) 14 | - [Fields](#fields) 15 | - [Methods](#methods-2) 16 | - [Relationships](#relationships) 17 | - [Definition](#definition) 18 | - [Methods](#methods-3) 19 | - [One to One](#one-to-one) 20 | - [One to Many](#one-to-many) 21 | - [Many to Many](#many-to-many) 22 | 23 | ## Installation 24 | 25 | ```bash 26 | npm install json-modelizer 27 | ``` 28 | 29 | ## Configuration 30 | 31 | Add these variables to your `.env` file: 32 | 33 | ```env 34 | JSON_MODELIZER_DATAPATH=./data 35 | JSON_MODELIZER_PRETTYFY=false 36 | JSON_MODELIZER_INDENT=2 37 | ``` 38 | 39 | - **DATAPATH**: is the path to the directory where your JSON data files will be stored. The default value is `./data`. 40 | - **PRETTYFY**: is a boolean value that determines whether the JSON data files should be prettyfied or not. The default value is `false`. 41 | - **INDENT**: is the number of spaces to use for indentation when prettyfying the JSON data files. The default value is `2`. Only applicable if `prettyfy` is set to `true`. 42 | 43 | ## Usage 44 | 45 | ### Model 46 | 47 | The `Model` class is the main class of the ORM (Object-Relational Mapping) library, providing various static methods to interact with JSON data. It represents a model for a specific type of JSON data, allowing you to define attributes and relationships for the data. 48 | 49 | #### Model Definition 50 | 51 | ```javascript 52 | import { Field, Model } from "json-modelizer"; 53 | 54 | class User extends Model { 55 | static _table = "users"; 56 | static schema = { 57 | name: Field.String, 58 | age: Field.Number.Nullable, 59 | }; 60 | } 61 | ``` 62 | 63 | #### Methods 64 | 65 | 1. `Model.create(obj) => Model`: 66 | 67 | - Description: Create a new record for the model in the JSON data. 68 | - Parameters: 69 | - `obj` (Object): An object representing the data for the new record. 70 | - Returns: A Model instance representing the newly created record. 71 | 72 | ```javascript 73 | const user = User.create({ name: "John Doe", email: "john@doe.com" }); 74 | console.log(user.name); // John Doe 75 | console.log(user.email); // john@doe 76 | console.log(user.id); // 1 77 | ``` 78 | 79 | 2. `Model.all() => Model[]`: 80 | 81 | - Description: Retrieve all records of the model from the JSON data. 82 | - Returns: An array of Model instances, representing all records of the model. 83 | 84 | ```javascript 85 | const users = User.all(); 86 | console.log(users); // [{ name: "John Doe", email: "john@doe"}] 87 | ``` 88 | 89 | 3. `Model.paginate(page, limit) => Model[]`: 90 | 91 | - Description: Paginate all records of the model. 92 | - Parameters: 93 | - `page` (number): the page number 94 | - `limit` (number): how much data per page 95 | - `filterFunction` (Function): a function that takes a Model instance as an argument and returns `true` to include the record in the result or `false` to exclude it. 96 | - Returns: An array of Model instances where `0 <= length <= limit` 97 | 98 | ```javascript 99 | const page = 2; 100 | const limit = 5; 101 | const users = User.paginate(page, limit, (user) => 102 | user.name.includes("Doe") 103 | ); 104 | console.log(users); 105 | /* 106 | [ 107 | { name: "John Doe", email: "john@doe"}, 108 | ..., 109 | {name: "Jane Doe", email: "jane@doe"}, 110 | ] 111 | */ 112 | ``` 113 | 114 | 4. `Model.find(id) => Model`: 115 | 116 | - Description: Find a record of the model by its ID from the JSON data. 117 | - Parameters: 118 | - `id` (string): The ID of the record to find. 119 | - Returns: A Model instance representing the found record, or `null` if not found. 120 | 121 | ```javascript 122 | const user = User.find(1); 123 | console.log(user.name); // John Doe 124 | console.log(user.email); // john@doe 125 | console.log(user.id); // 1 126 | ``` 127 | 128 | 5. `Model.findBy(key, value) => Model`: 129 | 130 | - Description: Find the first record of the model that matches the given key-value pair from the JSON data. 131 | - Parameters: 132 | - `key` (string): The attribute key to search for. 133 | - `value` (any): The value to match for the given attribute key. 134 | - Returns: A Model instance representing the found record, or `null` if not found. 135 | 136 | ```javascript 137 | const user = User.findBy("email", "john@doe.com"); 138 | console.log(user.name); // John Doe 139 | console.log(user.email); // john@doe 140 | console.log(user.id); // 1 141 | ``` 142 | 143 | 6. `Model.where(key, value) => Model[]`: 144 | 145 | - Description: Find all records of the model that match the given key-value pair from the JSON data. 146 | - Parameters: 147 | - `key` (string): The attribute key to search for. 148 | - `value` (any): The value to match for the given attribute key. 149 | - Returns: An array of Model instances representing the found records. 150 | 151 | ```javascript 152 | const users = User.where("email", "john@doe.com"); 153 | console.log(users); // [{ name: "John Doe", email: "john@doe"}] 154 | ``` 155 | 156 | 7. `Model.delete(id) => Model`: 157 | 158 | - Description: Delete a record of the model by its ID from the JSON data. 159 | - Parameters: 160 | - `id` (string): The ID of the record to delete. 161 | - Returns: A Model instance representing the deleted record, or `null` if not found. 162 | 163 | ```javascript 164 | const deletedUser = User.delete(1); 165 | console.log(deletedUser.name); // John Doe 166 | console.log(deletedUser.email); // john@doe 167 | console.log(deletedUser.id); // 1 168 | ``` 169 | 170 | 8. `Model.update(id, obj) => Model`: 171 | 172 | - Description: Update a record of the model by its ID with the provided data in the JSON data. 173 | - Parameters: 174 | - `id` (string): The ID of the record to update. 175 | - `obj` (Object): An object representing the data to update for the record. 176 | - Returns: A Model instance representing the updated record, or `null` if not found. 177 | 178 | ```javascript 179 | const updatedUser = User.update(1, { name: "Jane Doe" }); 180 | console.log(updatedUser.name); // Jane Doe 181 | console.log(updatedUser.email); // john@doe 182 | console.log(updatedUser.id); // 1 183 | ``` 184 | 185 | 9. `Model.first() => Model`: 186 | 187 | - Description: Retrieve the first record of the model from the JSON data. 188 | - Returns: A Model instance representing the first record, or `null` if no records exist. 189 | 190 | ```javascript 191 | const firstUser = User.first(); 192 | console.log(firstUser.name); // John Doe 193 | console.log(firstUser.email); // john@doe 194 | console.log(firstUser.id); // 1 195 | ``` 196 | 197 | 10. `Model.last() => Model`: 198 | 199 | - Description: Retrieve the last record of the model from the JSON data. 200 | - Returns: A Model instance representing the last record, or `null` if no records exist. 201 | 202 | ```javascript 203 | const lastUser = User.last(); 204 | console.log(lastUser.name); // John Doe 205 | console.log(lastUser.email); // john@doe 206 | console.log(lastUser.id); // 1 207 | ``` 208 | 209 | 11. `Model.filter(callback) => Model[]`: 210 | 211 | - Description: Filter records of the model based on a provided callback function from the JSON data. 212 | - Parameters: 213 | - `callback` (Function): A function that takes a Model instance as an argument and returns `true` to include the record in the result or `false` to exclude it. 214 | - Returns: An array of Model instances representing the filtered records. 215 | 216 | ```javascript 217 | const users = User.filter((user) => user.name.includes("Doe")); 218 | console.log(users); // [{ name: "John Doe", email: "john@doe"}] 219 | ``` 220 | 221 | 12. `Model.clear() => Model[]`: 222 | 223 | - Description: Delete all records of the model from the JSON data. 224 | - Returns: An array of Model instances representing the deleted records. 225 | 226 | ```javascript 227 | const deletedUsers = User.clear(); 228 | console.log(deletedUsers); // [{ name: "John Doe", email: "john@doe"}] 229 | ``` 230 | 231 | #### Instance 232 | 233 | A model instance represents a single record of the model. It provides various methods to interact with the record. 234 | 235 | ##### Methods 236 | 237 | 1. `Model#update(obj) => Model`: 238 | 239 | - Description: Update the record with the provided data in the JSON data. 240 | - Parameters: 241 | - `obj` (Object): An object representing the data to update for the record. 242 | - Returns: A Model instance representing the updated record. 243 | 244 | ```javascript 245 | const user = User.find(1); 246 | const updatedUser = user.update({ name: "Jane Doe" }); 247 | console.log(updatedUser.name); // Jane Doe 248 | console.log(updatedUser.email); // john@doe 249 | console.log(updatedUser.id); // 1 250 | ``` 251 | 252 | 2. `Model#delete() => Model`: 253 | 254 | - Description: Delete the record from the JSON data. 255 | - Returns: A Model instance representing the deleted record. 256 | 257 | ```javascript 258 | const user = User.find(1); 259 | const deletedUser = user.delete(); 260 | console.log(deletedUser.name); // John Doe 261 | console.log(deletedUser.email); // john@doe 262 | console.log(deletedUser.id); // 1 263 | ``` 264 | 265 | ##### Properties 266 | 267 | 1. `Model#id` (getter => number): The ID of the record. 268 | 269 | ```javascript 270 | const user = User.find(1); 271 | console.log(user.id); // 1 272 | ``` 273 | 274 | 2. `Model#table` (getter => string): The table name of the model. 275 | 276 | ```javascript 277 | const user = User.find(1); 278 | console.log(user.table); // users 279 | ``` 280 | 281 | 3. `Model#createdAt` (getter => Date): The date the record was created. 282 | 283 | ```javascript 284 | const user = User.find(1); 285 | console.log(user.createdAt); // 2021-01-01T00:00:00.000Z 286 | ``` 287 | 288 | 4. `Model#updatedAt` (getter => Date): The date the record was last updated. 289 | 290 | ```javascript 291 | const user = User.find(1); 292 | console.log(user.updatedAt); // 2021-01-01T00:00:00.000Z 293 | ``` 294 | 295 | ### Fields 296 | 297 | The `Field` class provides various methods to define attributes for a model. The following types of attributes are supported: 298 | 299 | - Number 300 | - String 301 | - Text 302 | - Boolean 303 | - Date 304 | 305 | ```javascript 306 | import { Field, Model } from "json-modelizer"; 307 | 308 | class User extends Model { 309 | static _table = "users"; 310 | static schema = { 311 | name: Field.String, 312 | age: Field.Number.Nullable, 313 | }; 314 | } 315 | ``` 316 | 317 | The `Field` class provides the following methods to define attributes: 318 | 319 | 1. `Field.Number => Field`: 320 | 321 | - Description: Define a number attribute. 322 | - Returns: A Field instance representing the number attribute. 323 | 324 | ```javascript 325 | import { Field, Model } from "json-modelizer"; 326 | 327 | class User extends Model { 328 | static _table = "users"; 329 | static schema = { 330 | age: Field.Number.Nullable, 331 | }; 332 | } 333 | ``` 334 | 335 | 2. `Field.String => Field`: 336 | 337 | - Description: Define a string attribute. 338 | - Returns: A Field instance representing the string attribute. 339 | 340 | ```javascript 341 | import { Field, Model } from "json-modelizer"; 342 | 343 | class User extends Model { 344 | static _table = "users"; 345 | static schema = { 346 | name: Field.String, 347 | }; 348 | } 349 | ``` 350 | 351 | 3. `Field.Text => Field`: 352 | 353 | - Description: Define a text attribute. 354 | - Returns: A Field instance representing the text attribute. 355 | 356 | ```javascript 357 | import { Field, Model } from "json-modelizer"; 358 | 359 | class User extends Model { 360 | static _table = "users"; 361 | static schema = { 362 | bio: Field.Text, 363 | }; 364 | } 365 | ``` 366 | 367 | 4. `Field.Boolean => Field`: 368 | 369 | - Description: Define a boolean attribute. 370 | - Returns: A Field instance representing the boolean attribute. 371 | 372 | ```javascript 373 | import { Field, Model } from "json-modelizer"; 374 | 375 | class User extends Model { 376 | static _table = "users"; 377 | static schema = { 378 | isVerified: Field.Boolean, 379 | }; 380 | } 381 | ``` 382 | 383 | 5. `Field.Date => Field`: 384 | 385 | - Description: Define a date attribute. 386 | - Returns: A Field instance representing the date attribute. 387 | 388 | ```javascript 389 | import { Field, Model } from "json-modelizer"; 390 | 391 | class User extends Model { 392 | static _table = "users"; 393 | static schema = { 394 | birthday: Field.Date, 395 | }; 396 | } 397 | ``` 398 | 399 | Each of the above methods returns a `Field` instance, which can be used to define additional properties for the attribute (see [Fields > Methods](#methods)). 400 | 401 | #### Methods 402 | 403 | The field object is an instance of the `Field` class, which provides various methods to define the attribute. 404 | 405 | 1. `Field#Nullable => Field`: 406 | 407 | - Description: Define the attribute as nullable. By default all attributes are required unless the `Nullable` method is called. 408 | - Returns: The Field instance. 409 | 410 | ```javascript 411 | import { Field, Model } from "json-modelizer"; 412 | 413 | class User extends Model { 414 | static _table = "users"; 415 | static schema = { 416 | name: Field.String.Nullable, 417 | }; 418 | } 419 | ``` 420 | 421 | 2. `Field#Default(value) => Field`: 422 | 423 | - Description: Define the default value for the attribute. 424 | - Parameters: 425 | - `value` (any): The default value for the attribute. This can be a value or a function that returns a value. Make sure the value or the return value of the function matches the type of the attribute. 426 | - Returns: The Field instance. 427 | 428 | ```javascript 429 | import { Field, Model } from "json-modelizer"; 430 | 431 | class User extends Model { 432 | static _table = "users"; 433 | static schema = { 434 | age: Field.Number.Default(18), 435 | }; 436 | } 437 | ``` 438 | 439 | ### Relationships 440 | 441 | The `Model` class provides various methods to define relationships between models. The following types of relationships are supported: 442 | 443 | - One-to-One 444 | - One-to-Many 445 | - Many-to-Many 446 | 447 | #### Definition 448 | 449 | Relationships are defined on the model class using one of the `hasOne`, `hasMany`, `belongsTo`, `belongsToMany` methods. For example, to define a `hasOne` relationship between a `User` model and a `Contact` model: 450 | 451 | ```javascript 452 | import { Field, Model } from "json-modelizer"; 453 | 454 | class User extends Model { 455 | static _table = "users"; 456 | static schema = { 457 | name: Field.String, 458 | age: Field.Number.Nullable, 459 | }; 460 | } 461 | 462 | class Contact extends Model { 463 | static _table = "contacts"; 464 | static schema = { 465 | phone: Field.String, 466 | email: Field.String, 467 | }; 468 | } 469 | 470 | User.hasOne(Contact); 471 | Contact.belongsTo(User); 472 | ``` 473 | 474 | The `hasOne`, `hasMany`, `belongsTo`, and `belongsToMany` methods takes only one parameter, which is the related model class. Each of these methods returns a `Relationship` instance, which can be used to define additional properties for the relationship (see [Relationships > Methods](#methods-2)). The `hasOne` and `hasMany` methods are used to define the relationship from the model that owns the foreign key, while the `belongsTo` and `belongsToMany` methods are used to define the relationship from the model that contains the foreign key. 475 | 476 | #### Methods 477 | 478 | The relation object is an instance of the `Relation` class, which provides various methods to define the relationship. 479 | 480 | 1. `Relation#as(name) => Relation`: 481 | 482 | - Description: Define the name of the relationship. If not defined, the name of the relationship will be the name of the related model's table. For belongs-to relationships, the name of the relationship will be the name of the related model's table with the suffix `Id`, this can be overridden by the `foreignKey` method. 483 | - Parameters: 484 | - `name` (string): The name of the relationship. 485 | - Returns: The Relation instance. 486 | 487 | ```javascript 488 | console.log(Contact._table); // contacts 489 | 490 | User.hasOne(Contact); 491 | // The name of the relationship will be "contacts" 492 | 493 | User.hasOne(Contact).as("contact"); 494 | // The name of the relationship will be "contact" 495 | ``` 496 | 497 | 2. `Relation#foreignKey(key) => Relation`: 498 | 499 | - Description: Define the foreign key of the relationship. If not defined, the foreign key of the relationship will be the name of the related model's table with the suffix `Id`. If the `as` method is used after the `foreignKey` method and the type of the relationship is `belongsTo`, the name of the relationship will be overridden by the name provided to the `as` method. Please make sure to call the `foreignKey` method after the `as` method. 500 | - Parameters: 501 | - `key` (string): The foreign key of the relationship. 502 | - Returns: The Relation instance. 503 | 504 | ```javascript 505 | console.log(Contact._table); // contacts 506 | 507 | User.hasOne(Contact); 508 | // The foreign key of the relationship will be "contactsId" 509 | 510 | User.hasOne(Contact).foreignKey("contactId"); 511 | // The foreign key of the relationship will be "contactId" 512 | 513 | console.log(User._table); // users 514 | Contact.belongsTo(User); 515 | // The foreign key of the relationship will be "usersId" 516 | 517 | Contact.belongsTo(User).as("user"); 518 | // The foreign key of the relationship will be "userId" 519 | 520 | Contact.belongsTo(User).foreignKey("userId"); 521 | // The foreign key of the relationship will be "userId" 522 | 523 | Contact.belongsTo(User).foreignKey("userId").as("owner"); 524 | // The foreign key of the relationship will be "ownerId" 525 | 526 | Contact.belongsTo(User).as("owner").foreignKey("userId"); 527 | // The foreign key of the relationship will be "userId" 528 | ``` 529 | 530 | #### One to One 531 | 532 | A One-to-One relationship is defined on the model class as follows: 533 | 534 | ```javascript 535 | import { Model, Field } from "json-modelizer"; 536 | 537 | class User extends Model { 538 | static _table = "users"; 539 | static schema = { 540 | name: Field.String, 541 | age: Field.Number.Nullable, 542 | }; 543 | } 544 | 545 | class Contact extends Model { 546 | static _table = "contacts"; 547 | static schema = { 548 | phone: Field.String, 549 | email: Field.String, 550 | }; 551 | } 552 | 553 | User.hasOne(Contact); 554 | Contact.belongsTo(User); 555 | ``` 556 | 557 | #### One to Many 558 | 559 | A One-to-Many relationship is defined on the model class as follows: 560 | 561 | ```javascript 562 | import { Model, Field } from "json-modelizer"; 563 | 564 | class User extends Model { 565 | static _table = "users"; 566 | static schema = { 567 | name: Field.String, 568 | age: Field.Number.Nullable, 569 | }; 570 | } 571 | 572 | class Post extends Model { 573 | static _table = "posts"; 574 | static schema = { 575 | title: Field.String, 576 | content: Field.String, 577 | }; 578 | } 579 | 580 | User.hasMany(Post); 581 | Post.belongsTo(User); 582 | ``` 583 | 584 | #### Many to Many 585 | 586 | A Many-to-Many relationship is defined on the model class as follows: 587 | 588 | ```javascript 589 | import { Model, Field } from "json-modelizer"; 590 | 591 | class User extends Model { 592 | static _table = "users"; 593 | static schema = { 594 | name: Field.String, 595 | age: Field.Number.Nullable, 596 | }; 597 | } 598 | 599 | class Role extends Model { 600 | static _table = "roles"; 601 | static schema = { 602 | name: Field.String, 603 | }; 604 | } 605 | 606 | User.hasMany(Role); 607 | Role.belongsToMany(User); 608 | ``` 609 | -------------------------------------------------------------------------------- /Field.js: -------------------------------------------------------------------------------- 1 | export default class Field { 2 | #type; 3 | #isNullable; 4 | #default; 5 | 6 | constructor(type) { 7 | this.#type = type; 8 | this.#isNullable = false; 9 | } 10 | 11 | get Nullable() { 12 | this.#isNullable = true; 13 | 14 | return this; 15 | } 16 | 17 | Default(value) { 18 | if (typeof value === "function") { 19 | this.validateAndSanitize(value()); 20 | this.#default = value; 21 | } else { 22 | this.#default = this.validateAndSanitize(value); 23 | } 24 | 25 | return this; 26 | } 27 | 28 | validateAndSanitize(value) { 29 | const shouldCheckDefaultValue = !this.#isNullable && value === undefined; 30 | if (shouldCheckDefaultValue) { 31 | const doesntHaveDefaultValue = this.#default === undefined; 32 | if (doesntHaveDefaultValue) throw new Error("This field is required"); 33 | 34 | value = 35 | typeof this.#default === "function" ? this.#default() : this.#default; 36 | } 37 | 38 | const isUndefinedOrNull = value === undefined || value === null; 39 | if (isUndefinedOrNull) { 40 | return value; 41 | } 42 | 43 | switch (this.#type) { 44 | case Field.#NUMBER: 45 | if (typeof value !== "number" && isNaN(+value)) 46 | throw new Error(`This field must be a number (got ${typeof value})`); 47 | return +value; 48 | 49 | case Field.#STRING: 50 | if (`${value}`.length > 255) throw new Error("This field is too long"); 51 | return `${value}`; 52 | 53 | case Field.#TEXT: 54 | return `${value}`; 55 | 56 | case Field.#BOOLEAN: 57 | return !!value; 58 | 59 | case Field.#DATE: 60 | if (isNaN(new Date(value).getTime())) 61 | throw new Error("This field must be a valid date"); 62 | return new Date(value); 63 | 64 | default: 65 | throw new Error(`Unknown field type: ${this.#type}`); 66 | } 67 | } 68 | 69 | static get Number() { 70 | return new Field(Field.#NUMBER); 71 | } 72 | 73 | static get String() { 74 | return new Field(Field.#STRING); 75 | } 76 | 77 | static get Text() { 78 | return new Field(Field.#TEXT); 79 | } 80 | 81 | static get Boolean() { 82 | return new Field(Field.#BOOLEAN); 83 | } 84 | 85 | static get Date() { 86 | return new Field(Field.#DATE); 87 | } 88 | 89 | static #NUMBER = "NUMBER"; 90 | static #STRING = "STRING"; 91 | static #TEXT = "TEXT"; 92 | static #BOOLEAN = "BOOLEAN"; 93 | static #DATE = "DATE"; 94 | } 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tri Putra Fauzan H Radji 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Model.js: -------------------------------------------------------------------------------- 1 | import Relation from "./Relation.js"; 2 | import { data } from "./utils.js"; 3 | 4 | export default class Model { 5 | static _table; 6 | static _relations = []; 7 | static schema = {}; 8 | #table; 9 | 10 | /** 11 | * @param {Object} obj 12 | * @param {number} obj.id 13 | * @returns {Model} 14 | */ 15 | constructor(obj) { 16 | this.#table = this.constructor._table; 17 | Object.defineProperty(this, "id", { 18 | value: obj.id || 0, 19 | enumerable: true, 20 | writable: false, 21 | configurable: false, 22 | }); 23 | 24 | for (const [key, field] of Object.entries(this.constructor.sanitize(obj))) { 25 | this[key] = field; 26 | } 27 | 28 | for (const relation of this.constructor._relations) { 29 | if (relation.key in obj) { 30 | this[relation.key] = obj[relation.key]; 31 | } 32 | 33 | Object.defineProperty(this, relation.name, { 34 | get: () => relation.getRelatedModel(this), 35 | enumerable: true, 36 | }); 37 | } 38 | 39 | Object.defineProperty(this, "createdAt", { 40 | value: new Date(obj.createdAt) || new Date(), 41 | enumerable: true, 42 | writable: false, 43 | configurable: false, 44 | }); 45 | 46 | Object.defineProperty(this, "updatedAt", { 47 | value: new Date(obj.updatedAt) || new Date(), 48 | enumerable: true, 49 | writable: false, 50 | configurable: false, 51 | }); 52 | } 53 | 54 | delete() { 55 | return this.constructor.delete(this.id); 56 | } 57 | 58 | update(obj) { 59 | this.constructor.update(this.id, obj); 60 | return this.#refresh(); 61 | } 62 | 63 | #refresh() { 64 | const model = this.constructor.find(this.id); 65 | const relationNames = this.constructor._relations.map( 66 | (relation) => relation.name 67 | ); 68 | if (model) 69 | for (const [key, value] of Object.entries(model)) { 70 | if (["id", "createdAt", "updatedAt", ...relationNames].includes(key)) 71 | continue; 72 | this[key] = value; 73 | } 74 | 75 | return this; 76 | } 77 | 78 | get table() { 79 | return this.#table; 80 | } 81 | 82 | get JSON() { 83 | return JSON.stringify(this); 84 | } 85 | 86 | static get table() { 87 | return this._table; 88 | } 89 | 90 | static _save(models) { 91 | const relationNames = this._relations.map((relation) => relation.name); 92 | data(this.table, models, (key, value) => { 93 | if (relationNames.includes(key)) return undefined; 94 | 95 | return value; 96 | }); 97 | } 98 | 99 | static count() { 100 | return this.all().length; 101 | } 102 | 103 | /** 104 | * Retrieve all Models from the table 105 | * @returns {Model[]} 106 | */ 107 | static all() { 108 | const table = this.table; 109 | return data(table).map((model) => new this(model)); 110 | } 111 | 112 | /** 113 | * Paginate Models using given page and limit params 114 | * @param {Number} page 115 | * @param {Number} limit 116 | * @returns {Model[]} 117 | */ 118 | static paginate(page, limit, filterFunction = () => true) { 119 | if (limit < 1) throw new Error("The limit variable should be more than 0"); 120 | const start = (page - 1) * limit; 121 | const end = page * limit; 122 | return this.filter(filterFunction).slice(start, end); 123 | } 124 | 125 | /** 126 | * Create new Model and save it to the table 127 | * @param {Object} obj 128 | * @returns {Model} 129 | */ 130 | static create(obj) { 131 | const models = this.all(); 132 | const lastId = Math.max(0, ...models.map((model) => model.id)); 133 | obj.id = lastId + 1; 134 | obj.createdAt = new Date(); 135 | obj.updatedAt = new Date(); 136 | 137 | const model = new this(obj); 138 | models.push(model); 139 | this._save(models); 140 | 141 | return model; 142 | } 143 | 144 | /** 145 | * Find a Model by key and value 146 | * @param {string} key 147 | * @param {*} value 148 | * @returns {Model | null} 149 | * @example 150 | * Contact.findBy("chatId", 1234567890); // => Contact 151 | * Contact.findBy("chatId", 999); // => null 152 | */ 153 | static findBy(key, value) { 154 | const models = this.all(); 155 | const model = models.find((model) => model[key] === value); 156 | if (model) return model; 157 | else return null; 158 | } 159 | 160 | /** 161 | * Find a Model by id 162 | * @param {number} id 163 | * @returns {Model | null} 164 | * @example 165 | * Contact.find(1); // => Contact 166 | * Contact.find(999); // => null 167 | */ 168 | static find(id) { 169 | return this.findBy("id", id); 170 | } 171 | 172 | /** 173 | * Find all Models by key and value 174 | * @param {string} key 175 | * @param {*} value 176 | * @returns {Model[]} 177 | * @example 178 | * Contact.where("chat_id", 1234567890); // [Contact, Contact, Contact] 179 | */ 180 | static where(key, value) { 181 | return this.filter((model) => model[key] === value); 182 | } 183 | 184 | /** 185 | * Delete a Model by id 186 | * @param {number} id 187 | * @returns {Model | null} 188 | */ 189 | static delete(id) { 190 | const models = this.all(); 191 | const index = models.findIndex((model) => model.id === id); 192 | if (index === -1) return null; 193 | 194 | const model = models[index]; 195 | models.splice(index, 1); 196 | this._save(models); 197 | 198 | return model; 199 | } 200 | 201 | /** 202 | * Update a Model by id 203 | * @param {number} id 204 | * @param {Object} obj 205 | * @returns {Model | null} 206 | */ 207 | static update(id, obj) { 208 | const models = this.all(); 209 | const index = models.findIndex((model) => model.id === id); 210 | if (index === -1) return null; 211 | 212 | const model = models[index]; 213 | models[index] = new this({ ...model, ...obj, updatedAt: new Date() }); 214 | this._save(models); 215 | 216 | return model; 217 | } 218 | 219 | /** 220 | * Retrieve the last Model from the table 221 | * @returns {Model | null} 222 | */ 223 | static last() { 224 | const models = this.all(); 225 | if (models.length === 0) return null; 226 | 227 | return models[models.length - 1]; 228 | } 229 | 230 | /** 231 | * Retrieve the first Model from the table 232 | * @returns {Model | null} 233 | */ 234 | static first() { 235 | const models = this.all(); 236 | if (models.length === 0) return null; 237 | 238 | return models[0]; 239 | } 240 | 241 | /** 242 | * Retrieve the number of Models in the table that match the callback 243 | * @param {Function} callback 244 | * @returns {Model[]} 245 | */ 246 | static filter(callback) { 247 | const models = this.all(); 248 | return models.filter(callback); 249 | } 250 | 251 | /** 252 | * Clear the table 253 | * @returns {Model[]} The deleted Models 254 | */ 255 | static clear() { 256 | const models = this.all(); 257 | this._save([]); 258 | return models; 259 | } 260 | 261 | static sanitize(obj) { 262 | const newObj = {}; 263 | for (const [key, field] of Object.entries(this.schema)) { 264 | try { 265 | const validated = field.validateAndSanitize(obj[key]); 266 | if (validated === undefined) continue; 267 | newObj[key] = validated; 268 | } catch (e) { 269 | throw new Error(`${key}: ${e.message}`); 270 | } 271 | } 272 | 273 | return newObj; 274 | } 275 | 276 | /** 277 | * @param {Model} model 278 | * @returns {void} 279 | */ 280 | static hasOne(model) { 281 | this._relations = this._relations.slice(); 282 | const relation = Relation.hasOne(this, model); 283 | this._relations.push(relation); 284 | 285 | return relation; 286 | } 287 | 288 | /** 289 | * @param {Model} model 290 | * @returns {void} 291 | */ 292 | static hasMany(model) { 293 | this._relations = this._relations.slice(); 294 | const relation = Relation.hasMany(this, model); 295 | this._relations.push(relation); 296 | 297 | return relation; 298 | } 299 | 300 | /** 301 | * @param {Model} model 302 | * @returns {void} 303 | */ 304 | static belongsTo(model) { 305 | this._relations = this._relations.slice(); 306 | const relation = Relation.belongsTo(this, model); 307 | this._relations.push(relation); 308 | 309 | return relation; 310 | } 311 | 312 | static belongsToMany(model) { 313 | this._relations = this._relations.slice(); 314 | const relation = Relation.belongsToMany(this, model); 315 | this._relations.push(relation); 316 | 317 | return relation; 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON-Modelizer 2 | 3 | JSON-Modelizer is a lightweight and intuitive Object-Relational Mapping (ORM) package designed to work with JSON files as a data storage solution. It simplifies the process of creating, managing, and querying structured data models from JSON files, providing a seamless integration between your Node.js application and JSON-based data storage. 4 | 5 | ## Table of Contents 6 | 7 | - [JSON-Modelizer](#json-modelizer) 8 | - [Table of Contents](#table-of-contents) 9 | - [Key Features](#key-features) 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Examples](#examples) 13 | - [Contributions](#contributions) 14 | - [License](#license) 15 | 16 | ## Key Features 17 | 18 | - **JSON Data Mapping**: Define data models using JavaScript classes, and effortlessly map them to JSON files for data storage. 19 | 20 | - **CRUD Operations**: Perform Create, Read, Update, and Delete operations on JSON data models using simple and expressive methods. 21 | 22 | - **Relationship Handling**: Easily handle relationships between JSON data models, including one-to-one, one-to-many, and many-to-many relationships. 23 | 24 | ## Installation 25 | 26 | To install "json-modelizer," simply run the following command: 27 | 28 | ``` 29 | npm install json-modelizer 30 | ``` 31 | 32 | ## Usage 33 | 34 | For detailed usage instructions and API documentation, please refer to the [documentation](DOCS.md). 35 | 36 | ## Examples 37 | 38 | Here's a quick example of how to use "json-modelizer" to create and query a JSON data model: 39 | 40 | ```javascript 41 | const { Model, Field } = require("json-modelizer"); 42 | 43 | class User extends Model { 44 | static _table = "users"; 45 | static schema = { 46 | username: Field.String, 47 | email: Field.String, 48 | age: Field.Number, 49 | }; 50 | } 51 | 52 | // Create a new user 53 | const newUser = User.create({ 54 | username: "john_doe", 55 | email: "john@example.com", 56 | age: 30, 57 | }); 58 | 59 | // Get all users 60 | const users = User.all(); 61 | ``` 62 | 63 | ## Contributions 64 | 65 | We welcome contributions from the community! If you encounter any issues, have suggestions for improvements, or want to contribute new features, feel free to submit a pull request. 66 | 67 | ## License 68 | 69 | This project is licensed under the [MIT License](LICENSE). 70 | -------------------------------------------------------------------------------- /Relation.js: -------------------------------------------------------------------------------- 1 | export default class Relation { 2 | #name; 3 | 4 | constructor(name, src, dest, type, key) { 5 | this.key = key; 6 | this.name = name; 7 | this.src = src; 8 | this.dest = dest; 9 | this.type = type; 10 | } 11 | 12 | as(name) { 13 | this.name = name; 14 | return this; 15 | } 16 | 17 | foreignKey(key) { 18 | this.key = key; 19 | return this; 20 | } 21 | 22 | getRelatedModel(model) { 23 | const relatedModel = this.dest; 24 | 25 | switch (this.type) { 26 | case Relation.#HAS_ONE: 27 | return relatedModel.findBy(this.key, model.id); 28 | 29 | case Relation.#HAS_MANY: 30 | return relatedModel.filter((related) => { 31 | return related[this.key] instanceof Array 32 | ? related[this.key].includes(model.id) 33 | : model.id === related[this.key]; 34 | }); 35 | 36 | case Relation.#BELONGS_TO: 37 | return relatedModel.find(model[this.key]); 38 | 39 | case Relation.#BELONGS_TO_MANY: 40 | return relatedModel.filter((related) => { 41 | return model[this.key].includes(related.id); 42 | }); 43 | 44 | default: 45 | throw new Error("Invalid relation type"); 46 | } 47 | } 48 | 49 | set name(value) { 50 | if (typeof value !== "string") 51 | throw new Error("Relation name must be a string"); 52 | 53 | this.#name = value; 54 | 55 | if (this.type === Relation.#BELONGS_TO) { 56 | this.key = `${value}Id`; 57 | } 58 | } 59 | 60 | get name() { 61 | return this.#name; 62 | } 63 | 64 | static hasOne(src, dest) { 65 | const name = dest.table; 66 | const key = `${src.table}Id`; 67 | 68 | return new Relation(name, src, dest, Relation.#HAS_ONE, key); 69 | } 70 | 71 | static hasMany(src, dest) { 72 | const name = dest.table; 73 | const key = `${src.table}Id`; 74 | 75 | return new Relation(name, src, dest, Relation.#HAS_MANY, key); 76 | } 77 | 78 | static belongsTo(src, dest) { 79 | const name = dest.table; 80 | const key = `${dest.table}Id`; 81 | 82 | return new Relation(name, src, dest, Relation.#BELONGS_TO, key); 83 | } 84 | 85 | static belongsToMany(src, dest) { 86 | const name = dest.table; 87 | const key = `${dest.table}Ids`; 88 | 89 | return new Relation(name, src, dest, Relation.#BELONGS_TO_MANY, key); 90 | } 91 | 92 | static #HAS_ONE = "hasOne"; 93 | static #HAS_MANY = "hasMany"; 94 | static #BELONGS_TO = "belongsTo"; 95 | static #BELONGS_TO_MANY = "belongsToMany"; 96 | } 97 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - [ ] **Data Validation**: Validate your JSON data models with customizable validation rules to ensure data integrity and consistency. 4 | - **Data Querying**: Effortlessly query JSON data models using a powerful and flexible query API, supporting filtering, sorting, and pagination. 5 | - **Data Migration Support**: Seamlessly manage changes to your JSON data schema with built-in data migration support. 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { configDotenv } from "dotenv"; 2 | configDotenv({ 3 | path: [ 4 | ".env", 5 | ".env.local", 6 | ".env.development", 7 | ".env.production", 8 | ".env.test", 9 | ], 10 | }); 11 | 12 | export { default as Model } from "./Model.js"; 13 | export { default as Relation } from "./Relation.js"; 14 | export { default as Field } from "./Field.js"; 15 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-modelizer", 3 | "version": "1.0.14", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "json-modelizer", 9 | "version": "1.0.14", 10 | "license": "MIT", 11 | "dependencies": { 12 | "dotenv": "^16.4.5" 13 | } 14 | }, 15 | "node_modules/dotenv": { 16 | "version": "16.4.5", 17 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 18 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 19 | "license": "BSD-2-Clause", 20 | "engines": { 21 | "node": ">=12" 22 | }, 23 | "funding": { 24 | "url": "https://dotenvx.com" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-modelizer", 3 | "version": "1.0.14", 4 | "description": "Lightweight and intuitive Object-Relational Mapping (ORM) package designed to work with JSON files as a data storage solution.", 5 | "main": "index.js", 6 | "type": "module", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "postversion": "npm publish", 11 | "postpublish": "git push" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/fauzan-radji/json-modelizer.git" 16 | }, 17 | "keywords": [ 18 | "JSON", 19 | "ORM" 20 | ], 21 | "author": "Fauzan Radji", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/fauzan-radji/json-modelizer/issues" 25 | }, 26 | "homepage": "https://github.com/fauzan-radji/json-modelizer#readme", 27 | "dependencies": { 28 | "dotenv": "^16.4.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /path.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fauzan-radji/json-modelizer/c5030a004aae91c1e2954e503cea7a3b81103003/path.js -------------------------------------------------------------------------------- /types/Field.d.ts: -------------------------------------------------------------------------------- 1 | export default class Field { 2 | #type: string; 3 | #isNullable: boolean; 4 | #default: any; 5 | 6 | constructor(type: string); 7 | 8 | get Nullable(): this; 9 | 10 | Default(value: string | (() => any)): this; 11 | 12 | validateAndSanitize(value: any): any; 13 | 14 | static get Number(): Field; 15 | static get String(): Field; 16 | static get Text(): Field; 17 | static get Boolean(): Field; 18 | static get Date(): Field; 19 | 20 | static #NUMBER: string; 21 | static #STRING: string; 22 | static #TEXT: string; 23 | static #BOOLEAN: string; 24 | static #DATE: string; 25 | } 26 | -------------------------------------------------------------------------------- /types/Model.d.ts: -------------------------------------------------------------------------------- 1 | import type Relation from "./Relation"; 2 | 3 | export default class Model { 4 | static _table: string; 5 | static _relations: Relation[]; 6 | static schema: { [key: string]: any }; 7 | 8 | #table: string; 9 | 10 | delete(): Model | null; 11 | 12 | update>(this: M, obj: O): M; 13 | 14 | #refresh(): Model; 15 | 16 | get table(): string; 17 | 18 | get JSON(): string; 19 | 20 | static get table(): string; 21 | 22 | static _save(this: new () => M, models: M[]): void; 23 | 24 | static count(): number; 25 | 26 | static all(this: new () => M): M[]; 27 | 28 | static paginate(this: new () => M, page: number, limit: number, filterFunction: (model: M) => boolean): M[]; 29 | 30 | static create>(this: new () => M, obj: O): M; 31 | 32 | static findBy(this: new () => M, key: string, value: any): M | null; 33 | 34 | static find(this: new () => M, id: number): M | null; 35 | 36 | static where(this: new () => M, key: string, value: any): M[]; 37 | 38 | static delete(this: new () => M, id: number): M | null; 39 | 40 | static update>(this: new () => M, id: number, obj: O): M | null; 41 | 42 | static last(this: new () => M): M | null; 43 | 44 | static first(this: new () => M): M | null; 45 | 46 | static filter(this: new () => M, callback: (model: M) => boolean): M[]; 47 | 48 | static clear(this: new () => M): M[]; 49 | 50 | static sanitize(obj: { [key: string]: any }): { [key: string]: any }; 51 | 52 | static hasOne(model: typeof Model): Relation; 53 | 54 | static hasMany(model: typeof Model): Relation; 55 | 56 | static belongsTo(model: typeof Model): Relation; 57 | 58 | static belongsToMany(model: typeof Model): Relation; 59 | } 60 | -------------------------------------------------------------------------------- /types/Relation.d.ts: -------------------------------------------------------------------------------- 1 | import type Model from "./Model"; 2 | 3 | export default class Relation { 4 | #name: string; 5 | 6 | constructor( 7 | name: string, 8 | src: typeof Model, 9 | dest: typeof Model, 10 | type: string, 11 | key: string 12 | ); 13 | 14 | key: string; 15 | name: string; 16 | src: typeof Model; 17 | dest: typeof Model; 18 | type: string; 19 | 20 | as(name: string): this; 21 | 22 | foreignKey(key: string): this; 23 | 24 | getRelatedModel(model: typeof Model): typeof Model | (typeof Model)[]; 25 | 26 | static hasOne(src: typeof Model, dest: typeof Model): Relation; 27 | 28 | static hasMany(src: typeof Model, dest: typeof Model): Relation; 29 | 30 | static belongsTo(src: typeof Model, dest: typeof Model): Relation; 31 | 32 | static belongsToMany(src: typeof Model, dest: typeof Model): Relation; 33 | 34 | static #HAS_ONE: string; 35 | static #HAS_MANY: string; 36 | static #BELONGS_TO: string; 37 | static #BELONGS_TO_MANY: string; 38 | } 39 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as Model } from "./Model"; 2 | export { default as Relation } from "./Relation"; 3 | export { default as Field } from "./Field"; 4 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | existsSync, 3 | lstatSync, 4 | mkdirSync, 5 | readFileSync, 6 | writeFileSync, 7 | } from "fs"; 8 | 9 | import DEFAULT_CONFIG from "./DEFAULT_CONFIG.js"; 10 | 11 | function fileExists(path) { 12 | return existsSync(path) && lstatSync(path).isFile(); 13 | } 14 | 15 | function dirExists(path) { 16 | return existsSync(path) && lstatSync(path).isDirectory(); 17 | } 18 | 19 | function readConfig() { 20 | const datapath = process.env.JSON_MODELIZER_DATAPATH 21 | ? process.env.JSON_MODELIZER_DATAPATH 22 | : DEFAULT_CONFIG.datapath; 23 | const prettyfy = process.env.JSON_MODELIZER_PRETTYFY 24 | ? process.env.JSON_MODELIZER_PRETTYFY === "true" 25 | : DEFAULT_CONFIG.prettyfy; 26 | const indent = process.env.JSON_MODELIZER_INDENT 27 | ? +process.env.JSON_MODELIZER_INDENT 28 | : DEFAULT_CONFIG.indent; 29 | 30 | return { datapath, prettyfy, indent }; 31 | } 32 | 33 | function sanitizeDatapath(datapath) { 34 | return datapath.endsWith("/") ? datapath : `${datapath}/`; 35 | } 36 | 37 | export function data(name, data, replacer = null, space) { 38 | const config = readConfig(); 39 | const { prettyfy, indent } = config; 40 | const datapath = sanitizeDatapath(config.datapath); 41 | const filePath = `${datapath}${name}.json`; 42 | if (!dirExists(datapath)) { 43 | mkdirSync(datapath); 44 | } 45 | if (!fileExists(filePath)) { 46 | writeFileSync(filePath, "[]"); 47 | return []; 48 | } 49 | 50 | if (data) { 51 | const json = prettyfy 52 | ? JSON.stringify(data, replacer, space || indent) 53 | : JSON.stringify(data, replacer); 54 | writeFileSync(filePath, json); 55 | return data; 56 | } else { 57 | const content = readFileSync(filePath, "utf-8"); 58 | 59 | if (!content) { 60 | writeFileSync(filePath, "[]"); 61 | return []; 62 | } 63 | 64 | const data = JSON.parse(content); 65 | return data; 66 | } 67 | } 68 | 69 | export default { data }; 70 | --------------------------------------------------------------------------------