{"id":17105635,"url":"https://github.com/yarnaimo/fireschema","last_synced_at":"2025-04-05T23:05:43.433Z","repository":{"id":40275617,"uuid":"281302352","full_name":"yarnaimo/fireschema","owner":"yarnaimo","description":"Strongly typed Firestore framework for TypeScript","archived":false,"fork":false,"pushed_at":"2022-10-23T08:47:43.000Z","size":4681,"stargazers_count":276,"open_issues_count":16,"forks_count":7,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-29T22:07:17.003Z","etag":null,"topics":["cloud-functions","firebase","firestore","firestore-rules","react","react-hooks","ttypescript","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/yarnaimo.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":"2020-07-21T05:17:49.000Z","updated_at":"2025-02-28T18:45:39.000Z","dependencies_parsed_at":"2023-01-19T20:47:08.976Z","dependency_job_id":null,"html_url":"https://github.com/yarnaimo/fireschema","commit_stats":null,"previous_names":[],"tags_count":90,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yarnaimo%2Ffireschema","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yarnaimo%2Ffireschema/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yarnaimo%2Ffireschema/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yarnaimo%2Ffireschema/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yarnaimo","download_url":"https://codeload.github.com/yarnaimo/fireschema/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247411226,"owners_count":20934653,"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":["cloud-functions","firebase","firestore","firestore-rules","react","react-hooks","ttypescript","typescript"],"created_at":"2024-10-14T15:42:35.018Z","updated_at":"2025-04-05T23:05:43.412Z","avatar_url":"https://github.com/yarnaimo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cbr /\u003e\n\n![Fireschema](./logo.png)\n\n\u003cp align=\"center\"\u003eStrongly typed Firestore framework for TypeScript\u003c/p\u003e\n\n\u003cbr /\u003e\n\n## Features\n\n- **Strong type safety for Firestore** - Automatically provide type information to _nested documents_ without unsafe type assertions, from the simple schema. Also support data decoding.\n- **Security rules generation** - Generate firestore.rules file including data type validation and access control from the schema.\n- **React Hooks** - Get realtime updates with React Hooks.\n- **Type safety for Cloud Functions**\n  - Automatically provide type information to snapshot data on Firestore Trigger Function based on the path string.\n  - Guard HTTPS callable function's request/response data type _both on compile time and runtime_.\n\n\u003cbr /\u003e\n\n## Requirement\n\n- **TypeScript** (\u003e= 4.4)\n\n\u003cbr /\u003e\n\n## Install\n\n```sh\nyarn add fireschema firebase firebase-admin firebase-functions zod\nyarn add -D typescript ts-node\n```\n\n\u003cbr /\u003e\n\n## Setup\n\n\u003e 🎉 Since Fireschema v5, you no longer need to compile codes via custom transformer.\n\n\u003cbr /\u003e\n\n## Usage - Firestore\n\n### Schema Transformation\n\n| Zod Schema                           | Security Rules Output                                                  |\n| ------------------------------------ | ---------------------------------------------------------------------- |\n| `z.any()`                            | `true`                                                                 |\n| `z.unknown()`                        | `true`                                                                 |\n| `z.undefined()`                      | `!(\"key\" in data)`                                                     |\n| `z.null()`                           | `data.key == null`                                                     |\n| `z.boolean()`                        | `data.key is bool`                                                     |\n| `z.literal('a')`                     | `data.key == \"a\"`                                                      |\n| `z.string()`                         | `data.key is string`                                                   |\n| `z.string().min(5)`                  | `(data.key is string \u0026\u0026 data.key.size \u003e= 5)`                           |\n| `z.string().min(5).max(20)`          | `(data.key is string \u0026\u0026 data.key.size \u003e= 5 \u0026\u0026 data.key.size \u003c= 20)`    |\n| `z.string().regex(/@example\\.com$/)` | `(data.key is string \u0026\u0026 data.key.matches(\"@example\\\\.com$\"))`          |\n| `z.number()`                         | `data.key is number`                                                   |\n| `z.number().int()`                   | `data.key is int`                                                      |\n| `z.number().min(5)`                  | `(data.key is int \u0026\u0026 data.key \u003e= 5)`                                   |\n| `z.number().max(20)`                 | `(data.key is int \u0026\u0026 data.key \u003c= 20)`                                  |\n| `timestampType()`                    | `data.key is timestamp`                                                |\n| `z.record(z.string())`               | `data.key is map`                                                      |\n| `z.tuple([z.string(), z.number()])`  | `(data.key is list \u0026\u0026 data.key[0] is string \u0026\u0026 data.key[1] is number)` |\n| `z.string().array()`                 | `data.key is list`                                                     |\n| `z.string().array().min(5)`          | `(data.key is list \u0026\u0026 data.key.size() \u003e= 5)`                           |\n| `z.string().array().max(20)`         | `(data.key is list \u0026\u0026 data.key.size() \u003c= 20)`                          |\n| `z.string().optional()`              | `(data.key is string \\|\\| !(\"key\" in data))`                           |\n| `z.union([z.string(), z.null()])`    | `(data.key is string \\|\\| data.key == null)`                           |\n\n\u003cbr\u003e\n\n### 1. Define schema\n\nThe schema definition must be default exported.\n\n\u003c!-- AUTO-GENERATED-CONTENT:START (CODE:src=./src/example/1-1-schema.ts) --\u003e\n\u003c!-- The below code snippet is automatically added from ./src/example/1-1-schema.ts --\u003e\n\n```ts\nimport { Merge } from 'type-fest'\nimport { z } from 'zod'\n\nimport { DataModel, FirestoreModel, rules, timestampType } from 'fireschema'\n\nexport const UserType = z.object({\n  name: z.string(),\n  displayName: z.union([z.string(), z.null()]),\n  age: z.number().int(),\n  timestamp: timestampType(),\n  options: z.object({ a: z.boolean() }).optional(),\n})\n\ntype User = z.infer\u003ctypeof UserType\u003e\n/* =\u003e {\n  name: string\n  displayName: string | null\n  age: number\n  timestamp: FTypes.Timestamp\n  options?: { a: boolean } | undefined\n} */\n\ntype UserDecoded = Merge\u003cUser, { timestamp: Date }\u003e\n\nconst UserModel = new DataModel({\n  schema: UserType,\n  decoder: (data: User): UserDecoded =\u003e ({\n    ...data,\n    timestamp: data.timestamp.toDate(),\n  }),\n})\n\nconst PostType = z.object({\n  authorUid: z.string(),\n  text: z.string(),\n  tags: z.object({ id: z.number().int(), name: z.string() }).array(),\n})\n\nconst PostModel = new DataModel({\n  schema: PostType,\n  selectors: (q) =\u003e ({\n    byTag: (tag: string) =\u003e [\n      q.where('tags', 'array-contains', tag),\n      q.limit(20),\n    ],\n  }),\n})\n\nexport const firestoreModel = new FirestoreModel({\n  'function isAdmin()': `\n    return exists(${rules.basePath}/admins/$(request.auth.uid));\n  `,\n\n  'function requestUserIs(uid)': `\n    return request.auth.uid == uid;\n  `,\n\n  collectionGroups: {\n    '/posts/{postId}': {\n      allow: {\n        read: true,\n      },\n    },\n  },\n\n  '/users/{uid}': {\n    model: UserModel,\n    allow: {\n      read: true, // open access\n      write: rules.or('requestUserIs(uid)', 'isAdmin()'),\n    },\n\n    '/posts/{postId}': {\n      'function authorUidMatches()': `\n        return request.resource.data.authorUid == uid;\n      `,\n\n      model: PostModel,\n      allow: {\n        read: true,\n        write: rules.and('requestUserIs(uid)', 'authorUidMatches()'),\n      },\n    },\n  },\n})\n\nexport default firestoreModel\n```\n\n\u003c!-- AUTO-GENERATED-CONTENT:END --\u003e\n\nWrite rules are **combined** with the rules automatically generated from zod schema.\n\n\u003cbr\u003e\n\n### 2. Generate firestore.rules\n\n```sh\nyarn fireschema rules \u003cpath-to-schema\u003e.ts\n```\n\n\u003e Environment variable `TS_NODE_PROJECT` is supported.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample of generated firestore.rules\u003c/summary\u003e\n\n\u003c!-- AUTO-GENERATED-CONTENT:START (CODE:src=./firestore.rules) --\u003e\n\u003c!-- The below code snippet is automatically added from ./firestore.rules --\u003e\n\n```rules\nrules_version = '2';\n\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    function __validator_meta__(data) {\n      return (\n        (request.method == \"create\" \u0026\u0026 data._createdAt == request.time \u0026\u0026 data._updatedAt == request.time)\n          || (request.method == \"update\" \u0026\u0026 data._createdAt == resource.data._createdAt \u0026\u0026 data._updatedAt == request.time)\n      );\n    }\n\n    function __validator_keys__(data, keys) {\n      return data.keys().removeAll(['_createdAt', '_updatedAt']).hasOnly(keys);\n    }\n\n    function isAdmin() {\n      return exists(/databases/$(database)/documents/admins/$(request.auth.uid));\n    }\n\n    function requestUserIs(uid) {\n      return request.auth.uid == uid;\n    }\n\n    match /{path=**}/posts/{postId} {\n      allow read: if true;\n    }\n\n    match /users/{uid} {\n      function __validator_0__(data) {\n        return (__validator_meta__(data) \u0026\u0026 (\n          __validator_keys__(data, ['name', 'displayName', 'age', 'timestamp', 'options'])\n            \u0026\u0026 data.name is string\n            \u0026\u0026 (data.displayName is string || data.displayName == null)\n            \u0026\u0026 data.age is int\n            \u0026\u0026 data.timestamp is timestamp\n            \u0026\u0026 (data.options.a is bool || !(\"options\" in data))\n        ));\n      }\n\n      allow read: if true;\n      allow write: if ((requestUserIs(uid) || isAdmin()) \u0026\u0026 __validator_0__(request.resource.data));\n\n      match /posts/{postId} {\n        function authorUidMatches() {\n          return request.resource.data.authorUid == uid;\n        }\n\n        function __validator_1__(data) {\n          return (__validator_meta__(data) \u0026\u0026 (\n            __validator_keys__(data, ['authorUid', 'text', 'tags'])\n              \u0026\u0026 data.authorUid is string\n              \u0026\u0026 data.text is string\n              \u0026\u0026 data.tags is list\n          ));\n        }\n\n        allow read: if true;\n        allow write: if ((requestUserIs(uid) \u0026\u0026 authorUidMatches()) \u0026\u0026 __validator_1__(request.resource.data));\n      }\n    }\n  }\n}\n```\n\n\u003c!-- AUTO-GENERATED-CONTENT:END --\u003e\n\n\u003c/details\u003e\n\n\u003cbr\u003e\n\n### 3. Read/write collections and documents\n\nThe Firestore interface of Fireschema supports both **Web SDK and Admin SDK**.\n\n\u003c!-- AUTO-GENERATED-CONTENT:START (CODE:src=./src/example/1-3-typed-firestore.ts) --\u003e\n\u003c!-- The below code snippet is automatically added from ./src/example/1-3-typed-firestore.ts --\u003e\n\n```ts\nimport { initializeApp } from 'firebase/app' // or firebase-admin\nimport { initializeFirestore } from 'firebase/firestore'\n\nimport { TypedFirestoreWeb } from 'fireschema'\nimport { firestoreModel } from './1-1-schema.js'\n\nconst app = initializeApp({\n  // ...\n})\nconst firestoreApp = initializeFirestore(app, {\n  ignoreUndefinedProperties: true,\n})\n\n/**\n * Initialize TypedFirestore\n */\nexport const $web: TypedFirestoreWeb\u003ctypeof firestoreModel\u003e =\n  new TypedFirestoreWeb(firestoreModel, firestoreApp)\n\n/**\n * Reference collections/documents and get snapshot\n */\nconst usersRef = $web.collection('users') // TypedCollectionRef instance\nconst userRef = usersRef.doc('userId') // TypedDocumentRef instance\n\nconst postsRef = userRef.collection('posts')\nconst postRef = postsRef.doc('123')\nconst techPostsQuery = postsRef.select.byTag('tech') // selector defined in schema\n\nawait userRef.get() // TypedDocumentSnap\u003cUser\u003e\nawait userRef.getData() // User | undefined\nawait userRef.getDataOrThrow() // User\n\nawait postRef.get() // TypedDocumentSnap\u003cPostA | PostB\u003e\nawait postsRef.get() // TypedQuerySnap\u003cPostA | PostB\u003e\nawait postsRef.getData() // (PostA | PostB)[]\nawait techPostsQuery.get() // TypedQuerySnap\u003cPostA | PostB\u003e\n\n/**\n * Get child collection of retrived document snapshot\n */\nconst snap = await usersRef.get()\nconst firstUserRef = snap.docs[0]!.ref\n\nawait firstUserRef.collection('posts').get()\n\n/**\n * Reference parent collection/document\n */\nconst _postsRef = postRef.parentCollection()\nconst _userRef = postsRef.parentDocument()\n\n/**\n * Reference collections groups and get snapshot\n */\nconst postsGroup = $web.collectionGroup('posts')\nconst techPostsGroup = postsGroup.select.byTag('tech')\n\nawait postsGroup.get() // TypedQuerySnap\u003cPostA | PostB\u003e\nawait techPostsGroup.get() // TypedQuerySnap\u003cPostA | PostB\u003e\n\n/**\n * Write data\n */\nawait userRef.create(({ serverTimestamp }) =\u003e ({\n  name: 'test',\n  displayName: 'Test',\n  age: 20,\n  timestamp: serverTimestamp(),\n  options: { a: true },\n}))\nawait userRef.setMerge({\n  age: 21,\n})\nawait userRef.update({\n  age: 21,\n})\nawait userRef.delete()\n\n/**\n * Transaction\n */\nawait $web.runTransaction(async (tt) =\u003e {\n  const snap = await tt.get(userRef)\n  tt.update(userRef, {\n    age: snap.data()!.age + 1,\n  })\n})\n```\n\n\u003c!-- AUTO-GENERATED-CONTENT:END --\u003e\n\n#### Write methods of Fireschema's document reference\n\n- **`create()`** - Create a document. (`_createdAt` / `_updatedAt` fields are added)\n  - **Web** - Call JS SDK's `set()` internally.\n    It fails if the document already exists because overwriting \\_createdAt is denied by the automatically generated security rules.\n  - **Admin** - Call Admin SDK's `create()` internally. It fails if the document already exists.\n- **`setMerge()`** - Call `set(data, { merge: true })` internally. (`_updatedAt` field is updated)\n- **`update()`** - Call `update()` internally. (`_updatedAt` field is updated)\n\n\u003e `set()` is not implemented on fireschema because it cannot determine whether `_createdAt` should be included in update fields without specifying it is a new creation or an overwrite.\n\n\u003cbr\u003e\n\n### 4. React Hooks\n\n\u003c!-- AUTO-GENERATED-CONTENT:START (CODE:src=./src/example/1-4-react-hooks.tsx) --\u003e\n\u003c!-- The below code snippet is automatically added from ./src/example/1-4-react-hooks.tsx --\u003e\n\n```tsx\nimport React, { Suspense } from 'react'\n\nimport { useTypedCollection, useTypedDoc } from 'fireschema/hooks'\nimport { $web } from './1-3-typed-firestore.js'\n\n/**\n * Get realtime updates of collection/query\n */\nexport const PostsComponent = () =\u003e {\n  const userRef = $web.collection('users').doc('user1')\n  const postsRef = userRef.collection('posts')\n\n  const posts = useTypedCollection(postsRef)\n  const techPosts = useTypedCollection(postsRef.select.byTag('tech'))\n\n  return (\n    \u003cSuspense fallback={'Loading...'}\u003e\n      \u003cul\u003e\n        {posts.data.map((post, i) =\u003e (\n          \u003cli key={i}\u003e{post.text}\u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/Suspense\u003e\n  )\n}\n\n/**\n * Get realtime updates of document\n */\nexport const UserComponent = ({ id }: { id: string }) =\u003e {\n  const user = useTypedDoc($web.collection('users').doc(id))\n\n  return (\n    \u003cSuspense fallback={'Loading...'}\u003e\n      \u003cspan\u003e{user.data?.displayName}\u003c/span\u003e\n    \u003c/Suspense\u003e\n  )\n}\n```\n\n\u003c!-- AUTO-GENERATED-CONTENT:END --\u003e\n\n\u003cbr\u003e\n\n---\n\n\u003cbr\u003e\n\n## Usage - Cloud Functions\n\n### 1. Create functions\n\n\u003c!-- AUTO-GENERATED-CONTENT:START (CODE:src=./src/example/2-1-typed-functions.ts) --\u003e\n\u003c!-- The below code snippet is automatically added from ./src/example/2-1-typed-functions.ts --\u003e\n\n```ts\nimport * as functions from 'firebase-functions'\nimport { z } from 'zod'\n\nimport { TypedFunctions } from 'fireschema/admin'\nimport { UserType, firestoreModel } from './1-1-schema.js'\n\n/**\n * Initialize TypedFunctions\n */\nconst timezone = 'Asia/Tokyo'\nconst typedFunctions = new TypedFunctions(firestoreModel, timezone)\nconst builder = functions.region('asia-northeast1')\n\n/**\n * functions/index.ts file\n */\nexport const UserJsonType = UserType.extend({ timestamp: z.string() })\nexport const callable = {\n  createUser: typedFunctions.callable({\n    schema: {\n      input: UserJsonType, // schema of request data (automatically validate on request)\n      output: z.object({ result: z.boolean() }), // schema of response data\n    },\n    builder,\n    handler: async (data, context) =\u003e {\n      console.log(data) // UserJson\n\n      return { result: true }\n    },\n  }),\n}\n\nexport const firestoreTrigger = {\n  onUserCreate: typedFunctions.firestoreTrigger.onCreate({\n    builder,\n    path: 'users/{uid}',\n    handler: async (decodedData, snap, context) =\u003e {\n      console.log(decodedData) // UserDecoded (provided based on path string)\n      console.log(snap) // QueryDocumentSnapshot\u003cUser\u003e\n    },\n  }),\n}\n\nexport const http = {\n  getKeys: typedFunctions.http({\n    builder,\n    handler: (req, resp) =\u003e {\n      if (req.method !== 'POST') {\n        resp.status(400).send()\n        return\n      }\n      resp.json(Object.keys(req.body))\n    },\n  }),\n}\n\nexport const topic = {\n  publishMessage: typedFunctions.topic('publish_message', {\n    schema: z.object({ text: z.string() }),\n    builder,\n    handler: async (data) =\u003e {\n      data // { text: string }\n    },\n  }),\n}\n\nexport const schedule = {\n  cron: typedFunctions.schedule({\n    builder,\n    schedule: '0 0 * * *',\n    handler: async (context) =\u003e {\n      console.log(context.timestamp)\n    },\n  }),\n}\n```\n\n\u003c!-- AUTO-GENERATED-CONTENT:END --\u003e\n\n\u003cbr\u003e\n\n### 2. Call HTTPS callable function\n\nAutomatically provide types to request/response data based on passed functions module type.\n\n\u003c!-- AUTO-GENERATED-CONTENT:START (CODE:src=./src/example/2-2-callable-function.tsx) --\u003e\n\u003c!-- The below code snippet is automatically added from ./src/example/2-2-callable-function.tsx --\u003e\n\n```tsx\nimport { initializeApp } from 'firebase/app'\nimport { getFunctions } from 'firebase/functions'\nimport React from 'react'\n\nimport { TypedCaller } from 'fireschema'\n\ntype FunctionsModule = typeof import('./2-1-typed-functions.js')\n\nconst app = initializeApp({\n  // ...\n})\nconst functionsApp = getFunctions(app, 'asia-northeast1')\n\nexport const typedCaller = new TypedCaller\u003cFunctionsModule\u003e(functionsApp)\n\nconst Component = () =\u003e {\n  const createUser = async () =\u003e {\n    const result = await typedCaller.call('createUser', {\n      name: 'test',\n      displayName: 'Test',\n      age: 20,\n      timestamp: new Date().toISOString(),\n      options: { a: true },\n    })\n\n    if (result.error) {\n      console.error(result.error)\n      return\n    }\n    console.log(result.data)\n  }\n\n  return \u003cbutton onClick={createUser} /\u003e\n}\n```\n\n\u003c!-- AUTO-GENERATED-CONTENT:END --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyarnaimo%2Ffireschema","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyarnaimo%2Ffireschema","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyarnaimo%2Ffireschema/lists"}