{"id":23159064,"url":"https://github.com/classfunc/firebase-orm","last_synced_at":"2025-04-04T18:41:55.342Z","repository":{"id":239647850,"uuid":"800143330","full_name":"ClassFunc/firebase-orm","owner":"ClassFunc","description":null,"archived":false,"fork":false,"pushed_at":"2024-05-13T19:55:05.000Z","size":331,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-10T03:47:35.335Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ClassFunc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2024-05-13T19:30:17.000Z","updated_at":"2024-05-13T19:55:08.000Z","dependencies_parsed_at":"2024-05-13T20:48:16.683Z","dependency_job_id":"fbab0aab-ef47-4407-883c-489b831c87ac","html_url":"https://github.com/ClassFunc/firebase-orm","commit_stats":null,"previous_names":["classfunc/firebase-orm"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ClassFunc%2Ffirebase-orm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ClassFunc%2Ffirebase-orm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ClassFunc%2Ffirebase-orm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ClassFunc%2Ffirebase-orm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ClassFunc","download_url":"https://codeload.github.com/ClassFunc/firebase-orm/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247234844,"owners_count":20905852,"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":[],"created_at":"2024-12-17T22:28:52.590Z","updated_at":"2025-04-04T18:41:55.321Z","avatar_url":"https://github.com/ClassFunc.png","language":"TypeScript","readme":"# firebase-orm\n\nA typeorm inspired ORM for Firestore.\n\n# Usage\n\n## Define An Entity and CRUD for it.\n\n### Entity (without relation)\n\nリレーションのないシンプルなエンティティの宣言と、そのエンティティ（レコード）のためのCRUDの方法を記載します。\n\n```typescript\nimport { FirebaseEntity, PrimaryColumn, Column } from 'firebase-orm';\n\n@FirebaseEntity('articles')\nexport class Article {\n    @PrimaryColumn()\n    id: string;\n\n    @Column()\n    title: string;\n\n    @Column({name: \"content_text\"})\n    contentText: string;\n}\n```\n\n### CRUD\n\n\n#### Create\n\n```typescript\nimport { getRepository } from 'firebase-orm';\n\nconst repo = getRepository(Article);\n\nconst article = new Article();\narticle.id = '1';\narticle.title = 'hello';\narticle.contentText = 'world!';\n\nawait repo.save(article);\n```\n\n#### Fetch\n\n```typescript\nconst article = await repo.fetchOneById('1');\nif(article) {\n    console.log(article);\n}\n```\n\n#### Update\n\n```typescript\nconst article = await repo.fetchOneById('1');\nif(!article) {\n    return;\n}\n\narticle.contentText = 'space!';\nawait repo.save(article);\n```\n\n#### Partial fields Update\n\n```typescript\nconst article = await repo.fetchOneById('1');\nif(!article) {\n    return;\n}\n\nawait repo.update(article, {\n    contentText: 'space!'\n});\n```\n\n#### Delete\n\n```typescript\nconst article = await repo.fetchOneById('1');\nif(!article) {\n    return;\n}\n\nawait repo.delete(article);\n```\n\n## Conditional Fetch\n\n`wehre`や`limit`などを使った検索をするには、`prepareFetcher`を呼び出します。\n\n```typescript\nconst articles = await repo.prepareFetcher(col =\u003e {\n    return col.where('id', '\u003e=', 5).limit(10);\n}).fetchAll();\n```\n\n`prepareFetcher`のコールバックの第一引数にはfirestoreの`Collection`オブジェクトが渡されるので、\n\nhttps://firebase.google.com/docs/firestore/query-data/queries\n\nを参考に、クエリを構築してください。\n\n\n## Define Entities with relations and CRUD for them.\n\n先程の`Article`が`User`に属し、`Category`と`Stat`を持つような関連を構築してみます。\n\n```typescript\nimport { Column, PrimaryColumn, FirebaseEntity, ManyToOne, OneToOne} from \"firebase-orm\";\n\n@FirebaseEntity('articles')\nexport class Article {\n    @PrimaryColumn()\n    id: string;\n\n    @Column()\n    title: string;\n\n    @ManyToOne(() =\u003e User, {joinColumnName: 'user_id'})\n    user: User;\n\n    @OneToOne(() =\u003e ArticleStat, {relationColumn: 'article_id'})\n    stat: ArticleStat;\n    \n    @ManyToOne(() =\u003e Category, {joinColumnName: 'category_id'})\n    category: Category;\n\n    @Column({name: \"content_text\"})\n    contentText: string;\n}\n\n@FirebaseEntity('article_stats')\nexport class ArticleStat {\n    @PrimaryColumn()\n    id: string;\n\n    @OneToOne(() =\u003e Article, {joinColumnName: 'article_id'})\n    article: Article;\n\n    @Column({name: 'num_of_views'})\n    numOfViews: number;\n}\n\n@FirebaseEntity('categories')\nexport class Category {\n    @PrimaryColumn()\n    id: string;\n    \n    @Column()\n    name: string;\n}\n\n@FirebaseEntity('users')\nexport class User {\n    @PrimaryColumn()\n    id: string;\n    \n    @Column()\n    name: string;\n\n    @OneToMany(() =\u003e Article, {relationColumn: 'user_id'})\n    articles: Article[];\n}\n```\n\n`Article`は`ManyToOne`で`User`に属しています。このとき、オプションで渡される`joinColumnName`は関連のための外部キーの名前になっています。つまり、Firestoreには`articles.user_id`として、`User`への参照が保存されます。`Article`と`ArticleStat`は`OneToOne`の関連がありますが、オプションが双方で異なっています。このとき、`ArticleStat`が`article_stats.article_id`としてfirestoreに`Article`への参照を持ちます。一方で、`Article`は検索時にジョインするための外部キーとして、`relationColumn`に`article_stats.article_id`を指定しています。このように、`OneToOne`では、値を保持する側と保持される側でそれぞれ、`joinColumnName`、`joinColumnName`を設定する必要があります。最後に、`OneToMany`ですが、この例では`User`から`Article`に対して設定されています。今、`articles.user_id`として`Article`が`User`の参照を保持していますから、`User`では`articles`の`relationColumn`にジョインの際の外部キーである`user_id`を指定しています。\n\nまずは、これらのテーブルに予めデータが保存されている想定で、`User`を起点に関連全てを取る例と、`Article`を起点に関連全てを取る例を見てみましょう。\n\n## Fetch with relations\n\n### User\n\n```typescript\nconst user = await getRepository(User).fetchOneById(id, {\n    relations: ['articles.category', 'articles.stat']\n});\n\nuser // User\nuser.articles // Article[]\nuser.articles[0].category // Category\nuser.articles[0].stat // ArticleStat\n```\n\n### Article\n\n```typescript\nconst article = await getRepository(Article).prepareFetcher(col =\u003e {\n    return col.where('id', '==', '1')\n}).fetchOne({\n    relations: ['user', 'category', 'stat']\n});\n\narticle // Article\narticle.user // User\narticle.category // Category\narticle.stat // ArticleStat\n```\n\nでは、次にトランザクションを使って、これら4つのコレクションにまたがる保存を行います。\n\n## Transactions\n\nトランザクションは`runTransaction`を呼び出します。\n\n```typescript\nimport { runTransaction } from \"firebase-orm\";\n\nawait runTransaction(async manager =\u003e {\n    const user = new User();\n    user.id = getRandomIntString();\n    user.name = 'test-user';\n    await manager.getRepository(User).save(user);\n\n    const category = new Category();\n    category.id = getRandomIntString();\n    category.name = 'math';\n    await manager.getRepository(Category).save(category);\n\n    const article = new Article();\n    article.id = getRandomIntString();\n    article.title = 'title';\n    article.contentText = 'bodybody';\n    article.user = user;\n    article.category = category;\n\n    await manager.getRepository(Article).save(article);\n\n    const articleStat = new ArticleStat();\n    articleStat.id = getRandomIntString();\n    articleStat.article = article;\n    articleStat.numOfViews = 100;\n\n    await manager.getRepository(ArticleStat).save(articleStat);\n});\n```\n\n保存の際は、関連の順番に注意して保存する必要があります。たとえばこの例では、`Article`が`User`と`Category`に依存しているので、先に`User`と`Category`を保存します。保存されたエンティティを`Article`オブジェクトにセットし、`save`することで、firestoreに参照が保存されます。最後に、`ArticleStat`オブジェクトに保存済みの`Article`エンティティをセットし`save`を呼び出せば、`Article`の参照が`ArticleStat`に保存されます。\n\nなお、次のように`runTransaction`のブロック内でエラーがthrowされた場合、トランザクションがロールバックされます。\n\n```typescript\nawait runTransaction(async manager =\u003e {\n    const user = new User();\n    user.id = getRandomIntString();\n    user.name = 'test-user';\n    await manager.getRepository(User).save(user);\n\n    const category = new Category();\n    category.id = getRandomIntString();\n    category.name = 'math';\n    await manager.getRepository(Category).save(category);\n\n    ...\n\n    throw new Error('rollback!!');\n});\n```\n\n## ArrayReference\n\nfirestoreでは、配列の形式で参照を持つことが出来ます。例として、`Artile`が複数の`Category`を持つことが出来るようにしてみます。\n\n```typescript\nimport {ArrayReference} from 'firebase-orm';\n\n@FirebaseEntity('articles')\nexport class Article {\n\n    ....\n    \n    @ArrayReference(() =\u003e Category, {joinColumnName: 'categories'})\n    categories: Category;\n\n    ....\n}\n```\n\nこれまでの`Article`は`category_id`を`ManyToOne`として持っていましたが、その代わりに名称を`categories`に変更し、`ArrayReference`として再定義しています。配列形式で`Category`の参照を保存するときは次のようにします。\n\n### Save\n\n```typescript\nconst cat1 = new Category();\ncat1.name = \"category1\";\nawait getRepository(Category).save(cat1);\n\nconst cat2 = new Category();\ncat2.name = \"category2\";\nawait getRepository(Category).save(cat2);\n\nconst article = new Article();\narticle.title = 'foo';\narticle.contentText = 'bar';\narticle.categories = [cat1, cat2];\n\nawait getRepository(Article).save(article);\n```\n\n### Fetch\n\n配列形式の参照からレコードを検査する場合は次のようにします。\n\n```typescript\nimport {PureReference} from 'firebase-orm';\n\nawait getRepository(Article).prepareFetcher(db =\u003e {\n    return db.where('categories', 'array-contains', PureReference(cat1))\n}).fetchAll();\n```\n\n## onSnapShot\n\nリアルタイム同期などに用いられる`onSnapShot`もRepositoryから扱うことができます。これまでの`fetch`オペレーションと同様に、`prepareFetcher`でクエリのコンディションを指定することも可能です。`onSnapShot`はレコードの`購読を`やめるための関数を返します。\n\n```typescript\nconst unsubscribe = getRepository(User).prepareFetcher(db =\u003e {\n    return db.limit(5);\n}).onSnapShot(async result =\u003e {\n    const type = result.type;\n    if(type === \"added\") {\n        console.log(result.item) // User\n    } \n    else if(type === \"modified\") {\n        console.log(result.item) // User\n    } \n    else if(type === \"removed\") {\n        console.log(result.item) // undefined\n    }\n    \n    unsubscribe();\n}, {\n    relations: ['articles.category', 'articles.stat']\n});\n```\n\n## Nested Collection\n\n例えば、`db.collection('foo').doc('1').collection('bar')`のようにネストしたコレクションへのアクセスを行う場合は、エンティティの宣言時に`@FirebaseEntity`の代わりに`@NestedFirebaseEntity`を利用します。次の`ArticleComment`は`Article`コレクションの子コレクションとして宣言されます。\n\n### Define Nested Entity\n\n```typescript\n@NestedFirebaseEntity('article_comments', () =\u003e Article)\nexport class ArticleComment {\n    @PrimaryColumn()\n    id: string;\n\n    @Column()\n    text: string;\n}\n```\n\n### CURD for Nested Entity\n\n子コレクションにアクセスする場合、`getRepository(ArticleComment, {parentIdMapper: Function})`のように、`getRepository`の第2引数に`parentIdMapper`を含むオブジェクトを渡します。\n\n#### 1段ネストコレクション\n\n```typescript\nconst repo = getRepository(ArticleComment, {parentIdMapper: (_) =\u003e {\n    return article.id;\n}});\n\nconst articleComment = new ArticleComment();\narticleComment.id = getRandomIntString();\narticleComment.text = 'hello';  \n\n// Create\nawait repo.save(articleComment);\n\n// Fetch\nconst comments = await repo.fetchOneById(articleComment.id);\n\n// Update\narticleComment.text = 'updated';\nawait repo.save(articleComment);\n\n// Delete\n\nawait repo.delete(articleComment);\n```\n\n#### 多段ネストコレクション\n\nコレクションが多段でネストしている場合は次のようにFirebaseEntityを設定します。\n\n```typescript\n@NestedFirebaseEntity('article_comment_likes', () =\u003e Article, () =\u003e ArticleComment)\nexport class ArticleCommentLike {\n    @PrimaryColumn()\n    id: string;\n\n    @Column()\n    count: number;\n}\n```\n\n```typescript\nconst repo = getRepository(ArticleCommentLike, {parentIdMapper: (Entity) =\u003e {\n    switch(Entity) {\n    case Article:\n        return article.id;\n    case ArticleComment:\n        return articleComment.id;\n    }\n    throw new Error(`Unknonwn Entity ${Entity.name}`);\n}});\n\nconst articleLikes = new ArticleCommentLike();\narticleLikes.text = 'hello';  \n\n// Create\nawait repo.save(articleLikes);\n\n// Fetch\nconst comments = await repo.fetchOneById(articleLikes.id);\n\n// Update\narticleLikes.text = 'updated';\nawait repo.save(articleLikes);\n\n// Delete\n\nawait repo.delete(articleLikes);\n```\n\n## Hooks\n\nfirebase-ormでは\n\n* `beforeSave`: save前に呼ばれる\n* `afterSave`: save後に呼ばれる\n* `afterLoad`: fetch後に呼ばれる\n\nのタイミングでフックメソッドを記述することが出来ます。\n\n### Define hooks\n\n```typescript\n@FirebaseEntity('articles')\nexport class Article {\n    @PrimaryColumn()\n    id: string;\n\n    @BeforeSave()\n    beforeSave() {\n        console.log('before save');\n    }\n\n    @AfterSave()\n    afterSave() {\n        console.log('after save');\n    }\n\n    @AfterLoad()\n    afterLoad() {\n        console.log('after load');\n    }\n\n    ....\n}\n```\n\n## Entity Serializer \u0026 Deserializer\n\n`@FirebaseEntity`は自身のプロパティとして`firestore.DocumentReference`を持つため、`JSON.stringify`が出来ません。そのため、エンティティを永続化したり、サーバーレスポンス時のシリアライズが困難となってしまいますが、この問題の回避策として`FirebaseEntitySerializer`と`FirebaseEntityDeserializer`があります。\n\n### FirebaseEntitySerializer\n\n`FirebaseEntitySerializer`は\n\n* serializeToJSON(object: any, parentId?: string)\n* serializeToJSONString(object: any, parentId?: string)\n\nを持ちます。`serializeToJSON`はエンティティをpureなjsオブジェクトにシリアライズします。一方で、`serializeToJSONString`は`serializeToJSON`の結果を文字列として返します。利用方法は次です。\n\n\n```typescript\nconst article = await runTransaction(async manager =\u003e {\n    const user = new User();\n    user.id = getRandomIntString();\n    user.name = 'test-user';\n    await manager.getRepository(User).save(user);\n\n    const category = new Category();\n    category.id = getRandomIntString();\n    category.name = 'math';\n    await manager.getRepository(Category).save(category);\n\n    const article = new Article();\n    article.id = getRandomIntString();\n    article.title = 'title';\n    article.contentText = 'bodybody';\n    article.user = user;\n    article.categories = [category];\n\n    await manager.getRepository(Article).save(article);\n\n    return article;\n});\n\nconst json = FirebaseEntitySerializer.serializeToJSON(article);\nconst jsonString = FirebaseEntitySerializer.serializeToJSONString(article);\n```\n\nなお、ネストされたコレクションをシリアライズする場合は、第2引数に`parentId`を指定します。\n\n```typescript\nconst comment = getRepository(ArticleComment, {withParentId: article.id}).fetchOneById('1');\nif(!comment) {\n    return;\n}\nconst json = FirebaseEntitySerializer.serializeToJSON(comment, article.id);\n```\n\n### FirebaseEntityDeserializer\n\n`FirebaseEntityDeserializer`は`FirebaseEntitySerializer`でシリアライズされたオブジェクトかJSON文字列をデシリアライズし、エンティティのインスタンスに復元します。\n\n* deserializeFromJSON\u003cT\u003e(Entity: ClassType\u003cT\u003e, str: string, parentId?: string)\n* deserializeFromJSONString\u003cT\u003e(Entity: ClassType\u003cT\u003e, str: string, parentId?: string)\n\nを持ちます。利用方法は次です。\n\n```typescript\nconst json = FirebaseEntitySerializer.serializeToJSON(article);\nconst jsonString = FirebaseEntitySerializer.serializeToJSONString(article);\n\nconst deserializedFromJSON = FirebaseEntityDeserializer.deserializeFromJSON(Article, json);\nconst deserializedFromJSONString = FirebaseEntityDeserializer.deserializeFromJSONString(Article, jsonString);\n```\n\nネストされたエンティティの復元には`FirebaseEntitySerializer`同様に、第3引数に`parentId`を指定します。\n\n## Client-Side\n\nクライアントサイドで`firebase-orm`を利用する際は、`firebase-orm-client`を`npm`や`yarn`でインストールします。`firebase-orm-client`のほとんどのコードベースは`firebase-orm`を共有しています。このとき、オリジナルは`firebase-orm`側にしてください。サーバーとクライアントにおける相違点は`src/type-mapper.ts`と`example`です。特に`src/type-mapper.ts`はクライアントサイド用の `firebase SDK`とサーバーサイド用の`firebase-admin SDK`の違いを吸収するための重要なファイルとなっています。内容は次です。\n\n**server-side**\n```\nimport * as admin from 'firebase-admin';\n\nexport type Firestore = FirebaseFirestore.Firestore;\nexport type DocumentReference = FirebaseFirestore.DocumentReference;\nexport type DocumentData = FirebaseFirestore.DocumentData;\n....\n\nexport const firestore = admin.firestore;\n```\n\n**client-side**\n```\nimport * as firebase from 'firebase';\n\nexport type Firestore = firebase.firestore.Firestore;\nexport type DocumentReference = firebase.firestore.DocumentReference;\nexport type DocumentData = firebase.firestore.DocumentData;\n....\n\nexport const firestore = firebase.firestore;\n```\n\nこれらのようにサーバーとクライアントで`firebase-admin`と`firebase`の型と`firestore`オブジェクトのエイリアスを構成し、`firebase-orm`がこれらの型を識別できるように設定を行います。その後、次のコマンドを入力し、必要なファイルをクライアント側にコピーします。\n\n```sh\ncd ~/YourProjectDir\ngit clone git@github.com:Polyrhythm-Inc/firebase-orm-client.git\ngit clone git@github.com:Polyrhythm-Inc/firebase-orm.git\ncd firebase-orm\nnode copyfile.js \n```\n\nコピー完了後、クライアント側のソースファイルのヘッダーに\n\n```\n// ---------- WARN! DO NOT EDIT BY HAND. THIS FILE IS AUTOMATICALLY GENERATED BY firebase-orm. ---------- \n```\n\nと追記されます。\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclassfunc%2Ffirebase-orm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclassfunc%2Ffirebase-orm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclassfunc%2Ffirebase-orm/lists"}