{"id":15677759,"url":"https://github.com/mamena2020/fireme","last_synced_at":"2025-05-07T01:25:14.600Z","repository":{"id":181149426,"uuid":"662786917","full_name":"Mamena2020/fireme","owner":"Mamena2020","description":"Fastest way to Build REST APIs with no Budget.  Boilerplate for Nodejs x Firebase.","archived":false,"fork":false,"pushed_at":"2023-10-16T10:53:59.000Z","size":517,"stargazers_count":20,"open_issues_count":0,"forks_count":7,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-31T05:02:20.181Z","etag":null,"topics":["backend","backend-template","boilderplate","boilerplate-nodejs","expressjs","firebase-api","firebase-backend","firebase-boilerplate","firebase-database","firebase-firestore","firebase-storage","firebase-template","nodejs","rest-api","role-and-permission"],"latest_commit_sha":null,"homepage":"https://fireme.vercel.app","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Mamena2020.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-05T22:22:17.000Z","updated_at":"2024-07-15T05:56:14.000Z","dependencies_parsed_at":"2023-10-16T20:30:00.436Z","dependency_job_id":null,"html_url":"https://github.com/Mamena2020/fireme","commit_stats":null,"previous_names":["mamena2020/fireme"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mamena2020%2Ffireme","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mamena2020%2Ffireme/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mamena2020%2Ffireme/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Mamena2020%2Ffireme/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Mamena2020","download_url":"https://codeload.github.com/Mamena2020/fireme/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252794716,"owners_count":21805238,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backend","backend-template","boilderplate","boilerplate-nodejs","expressjs","firebase-api","firebase-backend","firebase-boilerplate","firebase-database","firebase-firestore","firebase-storage","firebase-template","nodejs","rest-api","role-and-permission"],"created_at":"2024-10-03T16:11:18.671Z","updated_at":"2025-05-07T01:25:14.580Z","avatar_url":"https://github.com/Mamena2020.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fireme\n\n\u003ccenter\u003e\nBoilerplate for nodejs. base on express js with Firebase.\n\u003c/center\u003e\n\u003ccenter style='margin-top:20px; margin-bottom:20px;'\u003e\n\u003cimg src='fireme.png' style='max-height:500px;'\u003e\n\u003c/center\u003e\n\n- ### Features\n\n  - Model\n\n    Create model via cli.\n    Connect to firestore collections.\n\n  - Media library\n\n    Any model can own the media, and will able to save media, get media, and destroy media.\n    Media stored to `Firebase Storage`.\n\n  - File request handling\n\n    Not worry about handling uploaded file, just upload from client side, and file will accessible at req, `req.body.avatar`.\n\n  - Request validation\n\n    Determine if request passes the rule.\n    You can create `custom rule` via cli.\n\n  - Role and Permissions\n\n    Any model can have a role and permissions, set role, checking access.\n\n  - Resources\n\n    Create custom resource from resources.\n\n  - Auth - JWT/Basic\n\n    Create token, re generate token, and set middleware authorization for certain routes.\n\n  - Locale\n\n    Enabled or disabled locale or just enabled on certain routes.\n\n  - Mail\n\n    create mail via cli, and send mail with html, file, or just text.\n\n  - Firebase Cloud Messaging\n\n    Sending push notification from server to client device.\n\n  - Seeder\n\n    Running seeder via cli.\n\n- ### Live demo\n\n  | Action        | Method | Auth   | Body             | EndPoint                                  |\n  | ------------- | ------ | ------ | ---------------- | ----------------------------------------- |\n  | Login         | POST   |        | email            | https://fireme.vercel.app/api/login       |\n  |               |        |        | password         |                                           |\n  |               |        |        |                  |                                           |\n  | Register      | POST   |        | email            | https://fireme.vercel.app/api/register    |\n  |               |        |        | name             |                                           |\n  |               |        |        | password         |                                           |\n  |               |        |        | confirm_password |                                           |\n  |               |        |        |                  |                                           |\n  | Token         | GET    |        |                  | https://fireme.vercel.app/api/token       |\n  |               |        |        |                  |                                           |\n  |               |        |        |                  |                                           |\n  | Logout        | DELETE |        |                  | https://fireme.vercel.app/api/logout      |\n  |               |        |        |                  |                                           |\n  | Get User      | GET    | Bearer |                  | https://fireme.vercel.app/api/user        |\n  |               |        | token  |                  |                                           |\n  |               |        |        |                  |                                           |\n  | Upload avatar | POST   | Bearer | avatar (file)    | https://fireme.vercel.app/api/user/avatar |\n  |               |        | token  |                  |                                           |\n  |               |        |        |                  |                                           |\n  | Remove avatar | DELETE | Bearer |                  | https://fireme.vercel.app/api/user/avatar |\n  |               |        | token  |                  |                                           |\n  |               |        |        |                  |                                           |\n  | Get Users     | GET    |        |                  | https://fireme.vercel.app/api/users       |\n  |               |        |        |                  |                                           |\n\n# Getting Started\n\n- ### Clone\n\nClone this repo `https` or `SSH` and move to directory project and run `npm install`\n\n```\n   git clone git@github.com:Mamena2020/fireme.git\n\n```\n\n- ### Setup .env\n\nAfter clone, you can create `.env` file from `.env.example`.\n\n```\n   cp .env.example .env\n\n```\n\n- ### Create firebase project\n\nCreate new Firebase project on \u003ca href='https://console.firebase.google.com'\u003eFirebase Console\u003c/a\u003e.\nAfter create firebase project, go to `project settings -\u003e service accounts`, then generate new private key,\nafter download Service Account .json, convert to `base64 string`, then set to\n`FIREBASE_SERVICE_ACCOUNT_BASE64` in the `.env` file, and then go to firebase `storage` and copy firebase bucket name and\nset to `FIREBASE_STORAGE_BUCKET` in the `.env` file, and also create firestore database.\n\n```\n   FIREBASE_STORAGE_BUCKET=gs://your-project.appspot.com\n   FIREBASE_SERVICE_ACCOUNT_BASE64= # base64 of firebaseServiceAccount.json\n\n```\n\nAfter setup firebase config then you ready to write the code.\n\n# Model\n\n- Create new model via cli.\n\n```\n   npx fireme make:model Product\n```\n\nThe model will be created in the `models/Product.js` directory.\n\n```\n\n   import Model, { DataTypes } from '../core/model/Model.js';\n\n   class Product extends Model {}\n\n   Product.init({\n    fields: {\n        name: {\n            type: DataTypes.string,\n        },\n        price: {\n            type: DataTypes.number,\n        },\n        description: {\n            type: DataTypes.string,\n            nullable: true,\n        },\n    },\n    collection: 'products',\n    hasRole: false,\n   });\n\n   export default Product\n\n```\n\n- ### Store data\n\nStored data using `stored` static method. This method will return object that you save or null if failed.\n\n```\n\n   const data = {\n      name: 'Macbook pro M1',\n      price: 20000,\n      description: 'Macbook pro chipset M1 16inch screen...',\n   };\n\n   const product = await Product.stored(data);\n\n```\n\nBulk stored, stored many data at once. This `bulkStored` static method will return boolean true if success false if failed.\n\n```\n\n   const data = [\n        {\n           name: 'Macbook pro M1',\n           price: 20000,\n           description: 'Macbook pro chipset M1 16inch screen...',\n        },\n        {\n           name: 'Macbook pro M2',\n           price: 40000,\n           description: 'Macbook pro chipset M2 14inch screen...',\n        }\n   ];\n\n   const products = await Product.bulkStored(data);\n\n```\n\n- ### Update data\n\nUpdate data using `update` method. This method will return true if success or false if failed.\n\n```\n\n    const product = await Product.findOne({\n                        where: [{\n                            field: 'id',\n                            operator: Operator.equal,\n                            value: id,\n                        }],\n                    });\n\n   const newData = {\n      price: 22000,\n   };\n\n   const updated = await product.update(newData);\n\n```\n\nUpdate many, using `update` static method. This method will return true if success or false if failed.\n\n```\n    const oldPrice = 20000;\n    const newPrice = 22000;\n\n    const updated = await Product.update({\n            data: {\n                price: newPrice,\n            },\n            where: [\n                { field: 'price', operator: Operator.lte, value: oldPrice },\n            ],\n        });\n\n```\n\n- ### Delete data\n\nDelete data using `destroy` method. This method will return true if success or false if failed.\n\n```\n    const product = await Product.findOne({\n                        where: [{\n                            field: 'id',\n                            operator: Operator.equal,\n                            value: id,\n                        }],\n                    });\n\n    const deleted = await product.destroy();\n\n```\n\nDelete many data using `destroy` static method. This method required `where` condition, and will return true if success or false if failed.\n\n```\n    const deleted = await Product.destroy({\n        where: [\n                 { field: 'price', operator: Operator.lt, value: 20000 },\n            ],\n    });\n\n```\n\n- ### Get data\n\nGet single data using `findOne` static method. This method required `where` condition will return object if success of null if failed.\n\n```\n    const product = await Product.findOne({\n                        where: [{\n                            field: 'id',\n                            operator: Operator.equal,\n                            value: id,\n                        }],\n                    });\n\n```\n\nGet many data using `findAll` static method. This method required `where` condition, and have optional parameter `limit`, and `orderBy`. \u003cbr\u003e\n`orderBy` required 2 property `field` and `sort`, `sort` can be `asc` or `desc`.\n\n```\n    const products = await Product.findAll({\n                        where: [{\n                            field: 'price',\n                            operator: Operator.gte,\n                            value: 20000,\n                        }],\n                        limit: 10,\n                        orderBy: {\n                            field: \"updated_at\",\n                            sort: \"desc\"\n                        }\n                    });\n\n```\n\n- ### Count\n\nGet count of collection from the database by calling  `count()`.  This method has `where` condition, and will return number if success of null if failed\n\n```\n    const totalProduct = await Product.count(\n                          where: [{\n                                field: 'price',\n                                operator: Operator.gte,\n                                value: 20000,\n                            }],\n                         );\n```\n\n- ### Refresh\n\nReload an instance from the database by calling `refresh()`.\n\n```\n    await product.refresh();\n```\n\n- ### Firestore\n\nUsing firestore instance directly.\n\n```\n    import FirebaseCore from '../core/firebase/FirebaseCore.js';\n\n    const snapshot = await FirebaseCore.admin.firestore().collection('myCollection').get();\n        snapshot.docs.forEach((doc)=\u003e{\n         .... document\n    });\n\n```\n\n- ### Instance info\n\nAny instance of model has 'info' method with information.\n\n```\n    const info = product.info();\n\n    // collection    -\u003e collection name\n    // fields        -\u003e fields of model\n    // hasRole       -\u003e has role status\n    // doc           -\u003e raw data of document\n    // id            -\u003e document id\n    // ref           -\u003e reference document\n    // data          -\u003e data\n    // role          -\u003e role data\n    // medias        -\u003e array of medias\n\n```\n\n- ### Field Data Types\n\n```\n    string: 'string',\n    number: 'number',\n    boolean: 'boolean',\n    map: 'map',\n    array: 'array',\n    null: 'null',\n    timestamp: 'timestamp',\n    geopoint: 'geopoint',\n    reference: 'reference',\n```\n\n- ### Operators\n\n```\n    equal: '==',\n    notEqual: '!=',\n    lt: '\u003c',\n    lte: '\u003c=',\n    gt: '\u003e',\n    gte: '\u003e=',\n    arrayContains: 'array-contains',\n    in: 'in',\n    like: 'like',\n    arrayContainsAny: 'array-contains-any',\n    startsWith: 'startsWith',\n    endsWith: 'endsWith',\n    contains: 'contains',\n\n```\n\n- ### Noted\n\nAll model by default has `created_at` and `updated_at` property as timestamp.\nto convert to datetime using `toDate()`.\n\n```\n    const product = await Product.findOne({\n                        where: [{\n                            field: 'id',\n                            operator: Operator.equal,\n                            value: id,\n                        }],\n                    });\n\n    const productData = {\n                          id: product.id,\n                          name: product.name,\n                          price: product.price,\n                          created_at: product.created_at.toDate(),\n                          updated_at: product.updated_at.toDate(),\n                        }\n\n```\n\n# Media\n\nAny model already has ability to save and get media.\n\n- ### Save a file\n\nSave file using `saveMedia` method. The method required file object and media name, and will return media object if success or null if failed.\nIf the instance already has a file with the same name, then the file will be replaced with a new file.\n\n```\n    const id = req.params.id;\n    const { image } = req.body;\n\n    const product = await Product.findOne({\n                        where: [{\n                            field: 'id',\n                            operator: Operator.equal,\n                            value: id,\n                        }],\n                    });\n\n    const media = await product.saveMedia(image, 'thumbnail');\n\n```\n\n- ### Get media\n\nGet all media by calling `instance.getMedia()`.\n\n```\n\n    const product = await Product.findOne({\n                        where: [{\n                            field: 'id',\n                            operator: Operator.equal,\n                            value: id,\n                        }],\n                    });\n\n   product.getMedia() // return list of object\n\n```\n\nGet media by name, params is media name, will return media object if success and null if failed.\n\n```\n    product.getMedia('thumbnail') // return single object\n    product.getMedia('thumbnail').url // return single object url\n\n```\n\n- ### Destroy media\n\nDestroy media by calling `instance.destroyMedia(mediaName)`. This method will return true if success and false if failed.\n\n```\n    const product = await Product.findOne({\n                        where: [{\n                            field: 'id',\n                            operator: Operator.equal,\n                            value: id,\n                        }],\n                    });\n\n   const deleted = await product.destroyMedia('thumbnail')\n\n```\n\n- ### Storage\n\nUsing `saveMedia` directly and return url and path.\n\n```\n    const { avatar } = req.body;\n\n    const myAvatar = await FirebaseCore.saveMedia(avatar);\n\n    const { url, path } = myAvatar;\n```\n\nUsing `deleteMedia` or `deleteMedias` to delete media.\n\n```\n    await FirebaseCore.deleteMedia(mediaPath); // delete single media.\n\n    await FirebaseCore.deleteMedias(mediaPaths); // delete many media at once.\n```\n\n\n- ### Noted\n\nAll media files will be automatically deleted whenever `instance` of model is deleted.\n\n# Request \u0026 Upload Files\n\nHandling Content-Type header for\n\n      - application/json\n      - application/form-data\n      - application/x-www-form-urlencoded\n\nHandling all upload files on `POST` and `PUT` method, and nested fields.\n\n- ### File properties\n\nUploaded file will have this properties.\n\n```\n\n     name           -\u003e file name,\n     encoding       -\u003e file encoding,\n     type           -\u003e file mimeType,\n     size           -\u003e file size,\n     sizeUnit       -\u003e file size in bytes\n     extension      -\u003e file extension\n     tempDir        -\u003e file temporary directory\n\n```\n\n# Rule Validation\n\nCreate Request validation via cli.\n\n```\n   npx fireme make:request ProductRequest\n```\n\nThe Request will be created in the `requests` directory.\n\n```\n\n   import RequestValidation from \"../core/validation/RequestValidation.js\"\n\n   class ProductRequest extends RequestValidation {\n\n       constructor(req) {\n           super(req).load(this)\n       }\n\n        /**\n        * Get the validation rules that apply to the request.\n        *\n        * @return object\n        */\n       rules() {\n           return {\n\n           }\n       }\n    }\n\n    export default ProductRequest\n\n```\n\n- ### Basic usage.\n\n```\n\n   const valid = new ProductRequest(req)\n\n   await valid.check()\n\n   if (valid.isError)\n      return valid.responseError(res) // or  return res.status(422).json(valid.errors)\n\n```\n\n- ### Example html form.\n\n```\n\n   \u003cform action=\"endpoint\" method=\"post\" enctype=\"multipart/form-data\"\u003e\n      \u003cdiv class=\"row justify-content-center d-flex\"\u003e\n\n         \u003cdiv class=\"col-md-10\"\u003e\n             \u003clabel\u003eItem\u003c/label\u003e\n             \u003cinput type=\"text\" name=\"name\" placeholder=\"name\" /\u003e\n             \u003cinput type=\"text\" name=\"discount\" placeholder=\"discount\" /\u003e\n             \u003cinput type=\"date\" name=\"expired_date\" placeholder=\"expired date\" /\u003e\n             \u003cinput type=\"file\" name=\"product_image\" placeholder=\"file\" /\u003e\n         \u003c/div\u003e\n         \u003cdiv class=\"col-md-10\"\u003e\n             \u003clabel\u003eItem 1\u003c/label\u003e\n             \u003cinput type=\"text\" name=\"item[0][name]\" placeholder=\"name\" /\u003e\n             \u003cinput type=\"text\" name=\"item[0][description]\" placeholder=\"description\" /\u003e\n             \u003cinput type=\"text\" name=\"price[0]\" placeholder=\"price \" /\u003e\n         \u003c/div\u003e\n         \u003cdiv class=\"col-md-10 mt-5\"\u003e\n             \u003clabel\u003eItem 2\u003c/label\u003e\n             \u003cinput type=\"text\" name=\"item[1][name]\" placeholder=\"name\" /\u003e\n             \u003cinput type=\"text\" name=\"item[1][description]\" placeholder=\"description\" /\u003e\n             \u003cinput type=\"text\" name=\"price[1]\" placeholder=\"price\" /\u003e\n         \u003c/div\u003e\n          \u003cdiv class=\"col-md-10 mt-5\"\u003e\n             \u003cinput type=\"text\" name=\"comments[]\" /\u003e\n             \u003cinput type=\"text\" name=\"comments[]\" /\u003e\n             \u003cinput type=\"text\" name=\"comments[]\" /\u003e\n         \u003c/div\u003e\n          \u003cdiv class=\"col-md-10 mt-5\"\u003e\n             \u003cinput type=\"text\" name=\"seo[title]\" value=\"\" placeholder=\"seo title\" /\u003e\n             \u003cinput type=\"text\" name=\"seo[description][long]\" value=\"\" placeholder=\"seo long desc\" /\u003e\n             \u003cinput type=\"text\" name=\"seo[description][short]\" value=\"\" placeholder=\"seo short desc\" /\u003e\n         \u003c/div\u003e\n\n\n         \u003cdiv class=\"col-md-10 my-2 \"\u003e\n             \u003cbutton class=\"float-end btn btn-primary\" type=\"submit\"\u003eSubmit\u003c/button\u003e\n         \u003c/div\u003e\n\n      \u003c/div\u003e\n   \u003c/form\u003e\n\n```\n\n- ### Example rules.\n\n```\n\n   rules() {\n     return {\n         \"name\": {\n             \"rules\": [\"required\"]\n         },\n         \"discount\": {\n             \"rules\": [\"required\", \"float\", \"min:3\", \"max:4\"]\n         },\n         \"expired_date\": {\n             \"rules\": [\"required\", \"date\", \"date_after:now\"]\n         },\n         \"product_image\": {\n             \"rules\": [\"required\", \"image\", \"max_file:1,MB\"]\n         },\n         \"item.*.name\": {\n             \"rules\": [\"required\"]\n         },\n         \"item.*.description\": {\n             \"rules\": [\"required\"]\n         },\n         \"price.*\": {\n             \"rules\": [\"required\", \"float\"]\n         },\n         \"comments.*\": {\n             \"rules\": [\"required\"]\n         },\n         \"seo.title\": {\n             \"rules\": [\"required\"]\n         },\n         \"seo.description.long\": {\n             \"rules\": [\"required\"]\n         },\n         \"seo.description.short\": {\n             \"rules\": [\"required\"]\n         }\n     }\n   }\n\n```\n\n- ### Example error messages\n\n```\n\n   {\n     \"errors\": {\n          \"name\": [\n            \"The Name is required\"\n          ],\n          \"discount\": [\n            \"The Discount is required\",\n            \"The Discount must be valid format of float\",\n            \"The Discount should be more or equal than 3\",\n            \"The Discount should be less or equal than 4\"\n          ],\n          \"expired_date\": [\n            \"The Expired date is required\",\n            \"The Expired date must be valid format of date\",\n            \"The Expired date date must be after the now's date\"\n          ],\n          \"product_image\": [\n            \"The Product image is required\"\n          ],\n          \"item.0.name\": [\n            \"The Item.0.name is required\"\n          ],\n          \"item.1.name\": [\n            \"The Item.1.name is required\"\n          ],\n          \"item.0.description\": [\n            \"The Item.0.description is required\"\n          ],\n          \"item.1.description\": [\n            \"The Item.1.description is required\"\n          ],\n          \"price.0\": [\n            \"The Price.0 is required\",\n            \"The Price.0 must be valid format of float\"\n          ],\n          \"price.1\": [\n            \"The Price.1 is required\",\n            \"The Price.1 must be valid format of float\"\n          ],\n          \"comments.0\": [\n            \"The Comments.0 is required\"\n          ],\n          \"comments.1\": [\n            \"The Comments.1 is required\"\n          ],\n          \"comments.2\": [\n            \"The Comments.2 is required\"\n          ]\n          \"seo.title\": [\n            \"The Seo.title is required\"\n          ],\n          \"seo.description.long\": [\n            \"The Seo.description.long is required\"\n          ],\n          \"seo.description.short\": [\n            \"The Seo.description.short is required\"\n          ]\n      }\n   }\n\n```\n\n- ### Basic rules\n\n  | Rule                 | Description                                 | Example                                                    |\n  | -------------------- | ------------------------------------------- | ---------------------------------------------------------- |\n  | required             | check empty value                           | \"required\"                                                 |\n  | email                | check email formats                         | \"email\"                                                    |\n  | match                | check match value with other value          | \"match:password\"                                           |\n  | exists               | check value exists in the database          | \"exists:users,email\" or \"exists:users,email,\"+this.body.id |\n  | unique               | check value is unique in database           | \"unique:users,email\" or \"unique:users,email,\"+this.body.id |\n  | string               | check value is an string                    | \"string\"                                                   |\n  | float                | check value is an float                     | \"float\"                                                    |\n  | integer              | check value is an ineteger                  | \"integer\"                                                  |\n  | max                  | count maximum value of numeric,             | \"max:12\"                                                   |\n  |                      | if string/array its count the length        |                                                            |\n  | min                  | count minimum value of numeric,             | \"min:5\"                                                    |\n  |                      | if string/array its count the length        |                                                            |\n  | date                 | check value is date format                  | \"date\"                                                     |\n  | array                | check value is an array                     | \"array\"                                                    |\n  | mimetypes            | check file mimetypes                        | \"mimetypes:image/webp,image/x-icon,video/mp4\"              |\n  | mimes                | check file extension                        | \"mimes:jpg,png,jpeg\"                                       |\n  | max_file             | check maximum file size,                    | \"max_file:1,GB\" or \"max_file:1,MB\" or \"max_file:1,Byte\"    |\n  |                      | param can be `GB`, `MB`, `KB` or `Byte`     |                                                            |\n  | image                | check file is an image format               | \"image\"                                                    |\n  | date_after           | check value after particular date           | \"date_after:now\" or \"date_after:birthdate\"                 |\n  |                      | param can be `now`, or other field name     |                                                            |\n  | date_after_or_equal  | check value after or equal particular date  | \"date_after_or_equal:now\"                                  |\n  |                      | param can be `now`, or other field name     |                                                            |\n  | date_before          | check value before particular date          | \"date_before:now\" or \"date_before:birthdate\"               |\n  |                      | param can be `now`, or other field name     |                                                            |\n  | date_before_or_equal | check value before or equal particular date | \"date_before_or_equal:now\"                                 |\n  |                      | param can be `now`, or other field name     |                                                            |\n  | boolean              | check value is an boolean                   | \"boolean\"                                                  |\n  | in_array             | check value exist in array                  | \"in_array:1,3,4,1,4,5\"                                     |\n  | not_in_array         | check value is not include in array         | \"not_in_array:1,3,4,1,4,5\"                                 |\n  | ip                   | check value is as ip address                | \"ip\"                                                       |\n  | url                  | check value is as url                       | \"url\"                                                      |\n  | json                 | check value is as json format               | \"json\"                                                     |\n  | digits               | check value digits,                         | \"digits:4\"                                                 |\n  | max_digits           | check maximum digits of value               | \"max_digits:20\"                                            |\n  | min_digits           | check minumum digits of value               | \"min_digits:20\"                                            |\n  | digits_between       | check digits bewteen of value               | \"digits_between:5,10\"                                      |\n\n- ### Custom\n\nCustom validation `messages` and `attribute`\n\n```\n\n    rules() {\n        return {\n                \"name\": {\n                    \"rules\": [\"required\"],\n                    \"attribute\": \"Product name\"\n                },\n                \"discount\": {\n                    \"rules\": [\"required\", \"float\", \"min:3\", \"max:4\"],\n                    \"messages\": {\n                        \"required\": \"The _attribute_ need discount\",\n                        \"float\": \"The data must be numeric\"\n                    },\n                    \"attribute\": \"DISCOUNT\"\n                }\n            }\n    }\n\n```\n\n- ### Direct add error messages\n\n```\n    const valid = new ProductRequest(req)\n    await valid.check()\n\n    if (valid.isError)\n    {\n        valid.addError(\"name\",\"Name have to .....\")\n        valid.addError(\"name\",\"Name must be .....\")\n\n\n```\n\n# Custom Rule\n\nCreate Custom Rule via cli.\n\n```\n   npx fireme make:rule GmailRule\n```\n\nThe Rule will be created in the `rules` directory.\n\n```\n    class GmailRule  {\n\n        constructor() {\n        }\n\n        /**\n        * Determine if the validation rule passes.\n        * @param {*} attribute\n        * @param {*} value\n        * @returns bolean\n        */\n        passes(attribute, value) {\n\n            return value.includes(\"@gmail.com\")\n        }\n\n        /**\n        * Get the validation error message.\n        *\n        * @return string\n        */\n        message() {\n            return 'The _attribute_ must be using @gmail.com'\n        }\n   }\n\n   export default GmailRule\n\n```\n\n- ### Custom rule usage\n\n```\n    rules() {\n        return {\n                \"email\": {\n                    \"rules\": [ new GmailRule, \"required\",\"email\" ]\n                }\n            }\n    }\n```\n\n- ### Noted\n\nDefault error messages outputs are dependent on the locale. If you haven't set up the locale as a middleware, it will be set to English `en` by default.\n\n# Role and Permissions\n\nA user model can have a role by set `hasRole` to true.\n\n```\n\n    User.init({\n        fields: {\n            name: {\n                type: DataTypes.string,\n            },\n            email: {\n                type: DataTypes.string,\n            },\n            password: {\n                type: DataTypes.string,\n            },\n        },\n        collection: 'users',\n        hasRole: true, // set true\n    });\n\n```\n\n- ### Set role\n\nIf the user instance already has a role, then the user role will be replaced with a new role. `instance.setRole(params)` params is role `name`. `setRole` will return status in boolean.\n\n```\n\n\n   const user = await User.stored({\n      name: 'andre',\n      emmail: 'andre@gmail.com',\n      password: 'mypassword',\n   });\n\n   const status = await user.setRole('admin');\n\n```\n\n- ### Get role\n\nGet role object by calling `instance.getRole()`, or direcly access role name `instance.getRole().name`.\n\n```\n\n   const role = user.getRole() // role object\n   const roleName = user.getRole().name // role name\n\n\n```\n\n- ### Get permissions\n\nGet permission by calling `instance.getRole().permissions` will get array of permissions name.\n\n```\n   const permissions = user.getRole().permissions; //  array of permissions name [ \"user-create\",\"user-stored\"]\n\n```\n\n- ### Remove role\n  Remove role from an instance.\n\n```\n   const removed  = await user.removeRole();\n\n```\n\n- ### Check user access\n\nLimitation user access using `gateAccess(userInstance,permissionNames)`, `permissionNames` must be an array of permission names.\n\n```\n    import gateAccess from '../core/service/RolePermission/RolePermissionService.js';\n\n    if (!gateAccess(user, [\"user-create\",\"user-stored\",\"user-access\"]))\n        return res.sendStatus(403); // return forbidden status code\n\n```\n\n- ### Add permissions\n\n```\n   import Permission from '../core/service/RolePermission/Permission.js';\n\n   const permissions = [\n       \"user-create\",\n       \"user-stored\",\n       \"user-edit\",\n       \"user-update\",\n       \"user-delete\",\n       \"user-search\"\n   ]\n\n    for (let permission of permissions) {\n        await Permission.stored({ name: permission })\n    }\n\n```\n\n- ### Add Role\n\n```\n\n    import Role from '../core/service/RolePermission/Role.js';\n\n    const roles = [ \"admin\",\"customer\" ]\n\n    for (let role of roles) {\n        await Role.stored({ name: role })\n    }\n\n```\n\n- ### Add Permissions to Role\n\nAssign permissions to a role by update. Permissions must be an array of permission names.\n\n```\n    const permissions = [\n       \"user-create\",\n       \"user-stored\"\n    ]\n\n    const roleAdmin = await Role.findOne({\n                        where: [{\n                            field: 'name',\n                            operator: Operator.equal,\n                            value: 'admin',\n                        }],\n                    });\n\n    if (roleAdmin) {\n        await roleAdmin.update({permissions: permissions})\n    }\n\n```\n\n- ### Noted\n\nThere is no ref between permission and role, permission only stored all permission. The role has own field type an array to stored permission.\n\n# Resource\n\nCreate new resource via cli.\n\n```\n   npx fireme make:resource UserResource\n```\n\nThe Resource will be created in `resources/UserResource.js` directory.\n\n```\n\n   import Resource from \"../core/resource/Resource.js\"\n   class UserResource extends Resource {\n       constructor() {\n           super().load(this)\n       }\n\n       /**\n        * Transform the resource into custom object.\n        *\n        * @return\n        */\n       toArray(data) {\n            return {}\n       }\n    }\n\n    export default UserResource\n\n```\n\n- ### Basic usage\n\nTo create resources from a single object use `make` or `collection` for an array of objects.\n\n```\n\n     let userResource = new UserResource().make(user); // for single object\n\n     let userResources = new UserResource().collection(users); // for array of object\n\n```\n\n- ### Example user resource\n\n```\n\n   class UserResource extends Resource {\n       constructor() {\n           super().load(this)\n       }\n\n       toArray(data) {\n           return {\n                     \"id\": data.id,\n                     \"name\": data.name,\n                     \"email\": data.email,\n                     \"image\": data.getMedia(\"avatar\")?.url || '',\n                     \"role\": data.getRole()?.name || '',\n                 }\n       }\n    }\n\n```\n\n- ### Example usage\n\n```\n    const id = req.params.id;\n\n    const user = await User.findOne({\n                        where: [{\n                            field: 'id',\n                            operator: Operator.equal,\n                            value: id,\n                        }],\n                    });\n\n   const userResource = new UserResource().make(user);\n\n   return res.json(userResource);\n\n```\n\n- ### Example result\n\n```\n\n   {\n       \"id\": 'b96jRXfy8nSfZqk545SN',\n       \"name\": 'Andre',\n       \"email\": 'andre@gmail.com',\n       \"image\": 'https://storage.googleapis.com/projectx.appspot.com/91ecb634-46dd-4fce-995c-b38d4eaa2bd1.jpeg',\n       \"role\": 'admin',\n   }\n\n```\n\n# Auth\n\n- ### Create token\n\nCreate token by calling `JwtAuth.createToken()`, that will return `refreshToken` and `accessToken`.\n\n```\n   const payload = {\n         id: user.id,\n         name: user.name,\n         email: user.email\n     }\n\n   const token = JwtAuth.createToken(payload)\n\n   console.log(token.refreshToken)\n   console.log(token.accessToken)\n\n\n```\n\n- ### Regenerate access token\n\nRegenerate access token by calling `JwtAuth.regenerateAccessToken(refreshToken)`, that will return new access token.\n\n```\n\n   const accessToken = JwtAuth.regenerateAccessToken(refreshToken)\n\n```\n\n- ### Get Auth user\n\nGet authenticated user by `calling JwtAuth.getUser(email)`, that will get user by email.\n\n```\n\n   const user = await JwtAuth.getUser(email)\n\n```\n\nOr you just setup the .env `AUTH_GET_CURRENT_USER_ON_REQUEST=true` and you can access current authenticated user by access\n`req.user`. You can found the code where current user instance set to req.user in `core/middleware/JwtAuthPass.js` file.\n\n```\n    req.user = await JwtAuth.getUser(email);\n```\n\nBefore using `JwtAuth.GetUser()`, ensure that you have set up your `User` model inside the `AuthConfig` in the `core/config/Auth.js` file.\n\n- Default user model for auth in `core/config/Auth.js`.\n\n```\n   class AuthConfig {\n\n       /**\n       * Default user model for auth\n       * @returns\n       */\n       static user = User\n```\n\nIt is crucial that your User model has a `email` field, as `JwtAuth.GetUser()` will retrieve the user instance based on the `email` by default. However, if you prefer to retrieve the current authenticated user in a different manner, you can modify the `JwtAuth.GetUser()` function to suit your needs in `core/auth/JwtAuth.js` file.\n\n- ### Use Middleware - Auth jwt\n\nFor secure access to controller by adding `JwtAuthPass` to your router.\n\n```\n   import JwtAuthPass from '../core/middleware/JwtAuthPass.js';\n\n   routerAuth.use(JwtAuthPass)\n   routerAuth.get(\"/upload\", UserController.upload)\n\n   app.use(\"/api\",routerAuth)\n\n```\n\n- ### Use Middleware - Basic auth\n\nFor secure access to controller by adding `BasicAuthPass` to your router.\n\n```\n   import BasicAuthPass from '../core/middleware/BasicAuthPass.js';\n\n   routerAuth.use(BasicAuthPass)\n   routerAuth.get(\"/upload\", UserController.upload)\n\n   app.use(\"/api\",routerAuth)\n\n```\n\nBefore using this, make sure already set username and password for basic auth in `.env` file.\n\n```\n    AUTH_BASIC_AUTH_USERNAME=myBasicUsername\n    AUTH_BASIC_AUTH_PASSWORD=myBasicPassword\n```\n\n# Locale\n\n- ### Config\n\nSetup locale in `core/config/Locale.js`. by default locale setup to english `en`\n\n```\n\n    defaultLocale: \"en\",\n    useLocale: useLocale,\n    locales: [\"en\", \"id\"]\n\n```\n\nYou can add more locale Code to `locales`. By default `locales` are only available for English `en`, and for Indonesia `id`.\n\n- ### Default validation error Messages\n\nAfter adding additional `locales`, it is important to update the validation error messages in the `core/locale/LangValidation.js` file, as the messages generated will depend on the selected locale.\n\n```\n\n   const langValidation = Object.freeze({\n       required: {\n           en: \"The _attribute_ is required\",\n           id: \"_attribute_ wajib di isi\",\n         //ja: \"_attribute_ .........\"  -\u003e adding new validation messages for code ja\n       },\n       email: {\n           en: \"The _attribute_ must in E-mail format\",\n           id: \"_attribute_ harus dalam format E-mail\",\n       },\n       match: {\n           en: \"The _attribute_ must be match with _param1_\",\n           id: \"_attribute_ harus sama dengan _param1_\"\n       },\n       ......\n\n```\n\n- ### Use Locale\n\nIts easy to use locale, just setup `.env` `LOCALE_USE=true`, then this will effect to `all` routes, so that have to has a params for locale, for the API router it should be `/api/:locale` and for the web router it should be `/:locale`.\n\n```\n\n   // example for api route\n   const routerAuth = express.Router()\n\n   routerAuth.use(JwtAuthPass)\n   routerAuth.get(\"/user\", UserController.getUser)\n   routerAuth.post(\"/upload\", UserController.upload)s\n\n   app.use(\"/api/:locale\", routerAuth)\n\n   // http://localhost:5000/api/en/endpoint |  http://localhost:5000/api/id/endpoint\n\n```\n\nIf you don't want to set the locale for all routes, only for a particular route, then simply set up the `.env` as `LOCALE_USE=false`. Then you can use the `LocalePass` middleware directly to your route.\n\n```\n   import LocalePass from '../core/middleware/LocalePass.js';\n\n   // example for web route\n   app.get(\"/:locale\",LocalePass, (req, res) =\u003e {\n\n   // http://localhost:5000/en | http://localhost:5000/id\n\n\n   // example for api route\n   app.get(\"/api/:locale/login\",LocalePass, (req, res) =\u003e {\n\n   // http://localhost:5000/api/en/login | http://localhost:5000/api/id/login\n\n```\n\n- ### Noted\n\nAll routers that using `LocalePass` will have the locale Code on `req`, accessible via `req.locale`.\n\n# Mail\n\nCreate mail via cli.\n\n```\n   npx fireme make:mail AccountVerify\n```\n\nThe mail will be created in the `mails` directory, with `examplefile.txt` and `template.ejs`\n\n```\n\n   import Mail from \"../../core/mail/Mail.js\"\n\n   class AccountVerify extends Mail {\n         constructor(from = String, to = [], subject = String) {\n             super().load({\n                 from: from,\n                 to: to,\n                 subject: subject,\n                 text: \"Just need to verify that this is your email address.\",\n                 attachments: [\n                     {\n                         filename: \"theFile.txt\",\n                         path: \"mails/AccountVerify/examplefile.txt\"\n                     },\n                 ],\n                 html: {\n                     path: \"mails/AccountVerify/template.ejs\",\n                     data: {\n                         title: \"Welcome to the party!\",\n                         message: \"Just need to verify that this is your email address.\"\n                     }\n                 },\n             })\n         }\n   }\n\n   export default AccountVerify\n\n```\n\nThe `template.ejs` using express view engine `ejs` to render html into mail content.\n\n```\n   \u003c!DOCTYPE html\u003e\n     \u003chtml lang=\"en\"\u003e\n     \u003chead\u003e\n         \u003cmeta charset=\"UTF-8\"\u003e\n         \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"\u003e\n         \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n         \u003ctitle\u003e\n             \u003c%= title %\u003e\n         \u003c/title\u003e\n     \u003c/head\u003e\n     \u003cbody\u003e\n         \u003ch3\u003eHello!\u003c/h3\u003e\n         \u003cp\u003e\n             \u003c%= message %\u003e\n         \u003c/p\u003e\n         \u003cp\u003e\n             Regards.\n         \u003c/p\u003e\n         \u003cp\u003e\n             fireme\n         \u003c/p\u003e\n     \u003c/body\u003e\n   \u003c/html\u003e\n\n```\n\nTo use this `template.ejs`, you need to add an `html` object with a `path` and `data` (if needed) into `super().load()` method.\n\n```\n   html: {\n            path: \"mails/AccountVerify/template.ejs\", // path is required\n            data: // data is optional base on your template.ejs\n            {\n                title: \"Welcome to the party!\",\n                message: \"Just need to verify that this is your email address.\"\n            }\n        }\n```\n\n- ### Basic usage\n\nTo send email by calling `instance.send()`\n\n```\n   const sendMail = new AccountVerify(\"from@gmail.com\",[\"receiver@gmail.com\"],\"Verify Account\")\n\n   await sendMail.send()\n\n```\n\n- ### Send file\n\nTo send files, you need to add an `attachments` to `super().load()`. See full \u003ca href=\"https://nodemailer.com/message/attachments/\"\u003edoc\u003c/a\u003e.\n\n```\n   attachments: [\n                     {\n                         filename: \"theFile.txt\",\n                         path: \"mails/AccountVerify/examplefile.txt\"\n                     }\n                ]\n\n```\n\n- ### Mail message options\n\nMessage options that you can add into `super().load()`.\n\n| Name         | Description                                                                                                                          | Type   | Required |\n| ------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------ | -------- |\n| from         | The e-mail address of the sender. All e-mail addresses can be plain 'sender@server.com'                                              | string | `Yes`    |\n| to           | Recipients e-mail addresses that will appear on the To                                                                               | array  | `Yes`    |\n| subject      | Subject of the e-mail                                                                                                                | string | No       |\n| text         | If you are using HTML for the body of the email, then this text will not be used again                                               | string | No       |\n| html         | The HTML version of the message                                                                                                      | object | No       |\n| attachments  | An array of objects is used for the purpose of sending files. See full \u003ca href=\"https://nodemailer.com/message/attachments/\"\u003edoc\u003c/a\u003e | array  | No       |\n| cc           | Recipients e-mail addresses that will appear on the Cc                                                                               | array  | No       |\n| bcc          | Recipients e-mail addresses that will appear on the Bcc                                                                              | array  | No       |\n| sender       | E-mail address that will appear on the Sender                                                                                        | string | No       |\n| replyTo      | An array of e-mail addresses that will appear on the Reply-To                                                                        | array  | No       |\n| alternatives | An array of alternative text contents. See full \u003ca href=\"https://nodemailer.com/message/alternatives/\"\u003edoc\u003c/a\u003e                       | array  | No       |\n| encoding     | optional transfer encoding for the textual parts.                                                                                    | string | No       |\n\n- ### Noted\n\nBefore using mail, make sure you already setup `.env` file.\n\n```\n   MAIL_HOST= #example: smtp.gmail.com | smtp-relay.sendinblue.com\n   MAIL_PORT=587\n   MAIL_USERNAME=\n   MAIL_PASSWORD=\n\n```\n\n# Firebase Cloud Messaging\n\n- ### Send message\n\n```\n   import FirebaseCore from '../core/firebase/FirebaseCore.js';\n\n   const message = {\n        title: \"Notification\", // notification title\n        body: \"Hello there\",   // notification body\n        data: {\n                               // payload\n        },\n        registrationTokens: [\"token1\",\"token2\"] // target token\n   }\n\n   await FirebaseCore.sendMessage(message)\n\n```\n\n- ### Noted\n\nBefore using FCM, make sure you already `enable` Firebase Cloud Messaging API on \u003ca href=\"https://console.cloud.google.com/\"\u003eGoogle Cloud Console\u003c/a\u003e, by selecting your project and navigating to `APIs \u0026 Services`. Once you have enabled the API, you can set up your .env file.\n\n```\n   FIREBASE_CLOUD_MESSAGING_SERVER_KEY= #fcm server key\n\n```\n\n# Seeder\n\n- ### Running seeder\n\nRunning seeder via cli\n\n```\n    npx fireme seed:run\n```\n\nYou can put your seeder code inside `seeder` function in the `core/seeder/Seeder.js` file\n\n```\n\n   const seeder = async () =\u003e {\n\n         // put seeder code here..\n\n   }\n\n```\n\n# Cors\n\nThe configuration for Cross-Origin Resource Sharing (CORS) can be found in the `core/config/Cors.js` file.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmamena2020%2Ffireme","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmamena2020%2Ffireme","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmamena2020%2Ffireme/lists"}