{"id":18369851,"url":"https://github.com/cawfree/sofia","last_synced_at":"2025-04-06T18:32:34.694Z","repository":{"id":34645424,"uuid":"181350916","full_name":"cawfree/sofia","owner":"cawfree","description":"Firestore Rules. With variables.","archived":false,"fork":false,"pushed_at":"2023-01-03T19:59:30.000Z","size":1282,"stargazers_count":3,"open_issues_count":14,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-17T20:59:02.720Z","etag":null,"topics":["firebase","firestore","rules"],"latest_commit_sha":null,"homepage":"","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/cawfree.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-04-14T18:21:28.000Z","updated_at":"2021-06-12T13:20:04.000Z","dependencies_parsed_at":"2023-01-15T08:18:51.300Z","dependency_job_id":null,"html_url":"https://github.com/cawfree/sofia","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cawfree%2Fsofia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cawfree%2Fsofia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cawfree%2Fsofia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cawfree%2Fsofia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cawfree","download_url":"https://codeload.github.com/cawfree/sofia/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247531341,"owners_count":20953937,"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":["firebase","firestore","rules"],"created_at":"2024-11-05T23:32:34.061Z","updated_at":"2025-04-06T18:32:33.836Z","avatar_url":"https://github.com/cawfree.png","language":"JavaScript","funding_links":["https://www.buymeacoffee.com/cawfree"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./raw/sofia.png\" alt=\"sofia\" width=\"250\" height=\"300\"\u003e\n\u003c/p\u003e\n\n# sofia\nFirestore Rules. With variables.\n\n## 🤔  What is sofia?\nsofia is a representation of Firestore Rules described using JSON, which provides several benefits over the `.rules` syntax:\n\n  - Provides variable declarations to reduce verbosity\n  - Promotes more rigid and predictable rules structure\n  - Easily integrated with dynamic representations\n  - Relative path resolution\n  - Intuitive conditions\n\n## 🚀 Installing\nUsing `npm`:\n```\nnpm install --save @cawfree/sofia\n```\n\nUsing `yarn`:\n```\nyarn add @cawfree/sofia\n```\n\n## ✔️ Getting Started\n\n```javascript\nimport sofia, { $ifel } from '@cawfree/sofia';\n\n// declare rules json using sofia syntax\nconst rules = {\n  $userId: 'request.auth.uid',\n  'databases/{database}/documents': {\n    'user/{document=**}': {\n      $userIsAuthed: '$userId != null',\n      $exists: {\n        $userIsBlocked: './../../blocked/$($userId)',\n      },\n      $read: '$userIsAuthed',\n      $write: '$userIsAuthed \u0026\u0026 !$userIsBlocked',\n    },\n  },\n};\n\n// print the firebase-compatible rules\nconsole.log(sofia(rules));\n\n```\n\n## ✍️ Syntax Examples\n\n### Simple Variables\nIn the example below, we provide an example of dynamically constructing a `sofia`-compatible JSON object.\n\n```javascript\n// Checks whether the referenced document is not deleted.\nconst ensureNotDeleted = doc =\u003e `!${doc}.deleted`;\n// Ensures that a document's user information can never change.\nconst ensureUserNotChanged = (next, last) =\u003e `${next}.userId == $userId \u0026\u0026 ${next}.userId == ${last}.userId`;\nconst rules = sofia(\n  {\n    // Define $variables that are scoped to the adjacent collections and their subcollections.\n    // Note that variables are subject to by subcollections.\n    $nextDoc: 'request.resource.data',\n    $lastDoc: 'resource.data',\n    $userId: 'request.auth.uid',\n    $offset: 'request.query.offset',\n    ['databases/{database}/documents']: {\n      // Define the reference of the existing collection. This object effectively\n      // describes the database root as 'databases/{database}/documents'.\n      ['atomic/{docId}']: {\n        // Here we define the list rule, where we state callers are permitted\n        // to make list queries if they have provided a falsey offset. \n        // Looking at the global variables, offset refers to \"request.query.offset\".\n        $list: '$offset == null || $offset == 0',\n        // Here we can execute additional conditions based upon the results of the \n        // function invocations.\n        $update: [\n          ensureNotDeleted('$nextDoc'),\n          ensureUserNotChanged('$nextDoc', '$lastDoc'),\n        ]\n          .join(' \u0026\u0026 '),\n      },\n    },\n  },\n);\n```\nAfter a call to `sofia`, the returned `.rules` are as follows:\n```\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /atomic/{docId} {\n      allow list: if request.query.offset == null || request.query.offset == 0;\n      allow update: if !request.resource.data.deleted \u0026\u0026 request.resource.data.userId == request.auth.uid \u0026\u0026 request.resource.data.userId == resource.data.userId;\n    }\n  }\n}\n```\n\n### Transaction Variables\n\nIt is also possible to use **transaction variables**; these permit us to interact with the results of transcions  such as `exists` or `getAfter` themselves, just as if they were like any other variable. These help clearly establish the relationships that exist between collections.\n\n```javascript\n{\n  ['databases/{database}/documents']: {\n    $nextDoc: 'request.resource.data',\n    $userId: 'request.auth.uid',\n    ['outer/{document=**}']: {\n      // Declare a number of $getAfter variables within the scope\n      // of the 'outer' collection and its subcollections.\n      $getAfter: {\n        $outerVariable: './$($userId)',\n      },\n      $read: '$outerVariable != null',\n      ['inner/{innerRefId}']: {\n        // It is possible to even parse data out of the result\n        // of a transaction from an adjacent cell!\n        $innerVariable: '$outerVariable.userId',\n        $create: '$innerVariable == $userId',\n      },\n    },\n  },\n}\n```\n\nAfter a call to `sofia`, the returned `.rules` are as follows:\n\n```\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /outer/{document=**} {\n      allow read: if getAfter(/databases/$(database)/documents/outer/$(request.auth.uid)) != null;\n      match /inner/{innerRefId} {\n        allow create: if getAfter(/databases/$(database)/documents/outer/$(request.auth.uid)).userId == request.auth.uid;\n      }\n    }\n  }\n}\n```\n\n### Conditions\n\nIt is even possible to define **conditions**. These help clearly define which rules need to be processed based upon a previous condition. Since `.rules` are predefined, it's probably useful to note that there's nothing _special_ going on here, conditions merely resolve to a lazy evaluation of _both_ the positive and negative outcome, which effectively creates a branch in your static logic.\n\nThis block emphasises that `sofia` can result in more readable rule definitions, when handling more complex transactions.\n\n```javascript\n{\n  $nextDoc: 'request.resource.data',\n  $userId: 'request.auth.uid',\n  ['databases/{database}/documents']: {\n    ['user/{someUserId}']: {\n      $exists: {\n        $friendRecord: './../../friendsList/$(someUserId)/friend/$($userId)',\n      },\n      $read: '!resource.data.deleted \u0026\u0026 ' + $ifel(\n        'someUserId == $userId',\n        // All users are allowed to read their own documents.\n        () =\u003e 'true',\n        // If another user is trying to get the user information,\n        // make sure they are part of their friends first.\n        () =\u003e '$friendRecord',\n      ),\n    },\n    ['friendsList/{someFriendsListId}']: {\n      ['friend/{friendId}']: {\n\n      },\n    },\n  },\n}\n```\n\nAfter a call to `sofia`, the returned `.rules` are as follows. As you can see, the order of the evaluated conditions are preserved, without the headaches. \n\n```\n service cloud.firestore {\n   match /databases/{database}/documents {\n     match /user/{someUserId} {\n       allow read: if (((!resource.data.deleted) \u0026\u0026 ((someUserId == request.auth.uid) \u0026\u0026 true)) || ((!(someUserId == request.auth.uid)) \u0026\u0026 exists(/databases/$(database)/documents/friendsList/$(someUserId)/friend/$(request.auth.uid))));\n     }\n     match /friendsList/{someFriendsListId} {\n       match /friend/{friendId} {\n       }\n     }\n   }\n }\n```\n\nFor further information, check out [`index.test.js`](./index.test.js) to find a complete breakdown of the sofia syntax.\n\n## ✌️ Credits\nMade possible by [jsep](https://www.npmjs.com/package/jsep).\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.buymeacoffee.com/cawfree\"\u003e\n    \u003cimg src=\"https://cdn.buymeacoffee.com/buttons/default-orange.png\" alt=\"Buy @cawfree a coffee\" width=\"232\" height=\"50\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcawfree%2Fsofia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcawfree%2Fsofia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcawfree%2Fsofia/lists"}