{"_id":"@bridges-wood/graphql-firestore-subscriptions","name":"@bridges-wood/graphql-firestore-subscriptions","dist-tags":{"latest":"1.1.2"},"versions":{"1.1.2":{"name":"@bridges-wood/graphql-firestore-subscriptions","version":"1.1.2","description":"A simple & powerful package to broadcast events from Cloud Firestore over an AsyncIterator to your GraphQL Subscription Resolver.","main":"dist/index.js","scripts":{"test":"npm-run-all test:unit","test:unit":"jest --coverage","test:unit:watch":"jest --watch","test:lint":"npx eslint --max-warnings=0 --ext .ts . --fix","build":"npm-run-all build:clean build:transpile","build:transpile":"tsc","build:clean":"rimraf dist/*","format":"prettier --write \"{,!(node_modules)/**/}*.{js,jsx}\""},"repository":{"type":"git","url":"git+https://github.com/bridges-wood/graphql-firestore-subscriptions.git"},"keywords":["graphql","firestore","firebase","subscription","apollo"],"author":{"name":"Marc Binder","email":"marcandrebinder@gmail.com"},"license":"MIT","bugs":{"url":"https://github.com/bridges-wood/graphql-firestore-subscriptions/issues"},"homepage":"https://github.com/bridges-wood/graphql-firestore-subscriptions#readme","peerDependencies":{"graphql-subscriptions":"^2.0.0"},"devDependencies":{"@faker-js/faker":"^8.4.1","@google-cloud/firestore":"^7.7.0","@types/invariant":"^2.2.37","@types/jest":"^29.5.12","@types/node":"^20.12.12","@typescript-eslint/eslint-plugin":"^7.9.0","@typescript-eslint/parser":"^7.9.0","eslint":"^8.56.0","eslint-plugin-import":"^2.29.1","graphql-subscriptions":"^2.0.0","jest":"^29.7.0","npm-run-all":"^4.1.5","prettier":"^3.2.5","prettier-eslint":"^16.3.0","ts-jest":"^29.1.2","tslib":"^2.6.2","typescript":"^5.4.5"},"jest":{"coveragePathIgnorePatterns":["node_modules","dist"],"collectCoverageFrom":["src/**/*.ts","!src/**/*.test.ts"],"transform":{"^.+\\.tsx?$":"ts-jest"},"testRegex":"(/__tests__/.*|(\\.|/)(test|spec))\\.(tsx?)$","moduleFileExtensions":["ts","tsx","js","jsx","json","node"]},"dependencies":{"eslint-plugin-jest":"^28.5.0","invariant":"^2.2.4","iterall":"^1.3.0"},"types":"./dist/index.d.ts","gitHead":"ff4821e30e867ff3326bd45e5b26362a730abce4","_id":"@bridges-wood/graphql-firestore-subscriptions@1.1.2","_nodeVersion":"16.20.2","_npmVersion":"8.19.4","dist":{"integrity":"sha512-Aoey230vyqg0e0axva11TQ0oMnsOzVkN+C/JF9OnRF9shkAKrFAYv8viM+SB9XUyNRMBsRMkNj54CPI3rXu3xw==","shasum":"f94107bb84ea1cf12ebac2ab940e1e5fe0ac378a","tarball":"https://registry.npmjs.org/@bridges-wood/graphql-firestore-subscriptions/-/graphql-firestore-subscriptions-1.1.2.tgz","fileCount":29,"unpackedSize":60646,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIA6X6OFwNfSrK8aVXxunlF9JqDLCZ8yR0BV4lU8GsWAuAiEAzyeURV5llDdoZU9p5kEEI2R4kFJ69BDXkQR56a6MmSw="}]},"_npmUser":{"name":"bridges-wood","email":"bridges.wood@gmail.com"},"directories":{},"maintainers":[{"name":"bridges-wood","email":"bridges.wood@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/graphql-firestore-subscriptions_1.1.2_1716162214776_0.704260249436923"},"_hasShrinkwrap":false}},"time":{"created":"2024-05-19T23:43:34.675Z","1.1.2":"2024-05-19T23:43:35.031Z","modified":"2024-05-19T23:43:35.340Z"},"maintainers":[{"name":"bridges-wood","email":"bridges.wood@gmail.com"}],"description":"A simple & powerful package to broadcast events from Cloud Firestore over an AsyncIterator to your GraphQL Subscription Resolver.","homepage":"https://github.com/bridges-wood/graphql-firestore-subscriptions#readme","keywords":["graphql","firestore","firebase","subscription","apollo"],"repository":{"type":"git","url":"git+https://github.com/bridges-wood/graphql-firestore-subscriptions.git"},"author":{"name":"Marc Binder","email":"marcandrebinder@gmail.com"},"bugs":{"url":"https://github.com/bridges-wood/graphql-firestore-subscriptions/issues"},"license":"MIT","readme":"# graphql-firestore-subscriptions\n\n> 🚨 Important note!\n> Changing the primary NodeJS Package Manager account results in the new package name `@m19c/graphql-firestore-subscriptions`.\n\n_graphql-firestore-subscriptions_ implements the `PubSubEngine` interface from the [graphql-subscriptions](https://github.com/apollographql/graphql-subscriptions) package.\n\nUnlike other databases, Google's Firestore comes across with real time updates. Therefore, it is not required to publish events to a queue or a pub-sub.\nHowever, there is still something to do to get the data to the clients. In graphql-firestore-subscriptions those tasks are called handlers. They are subscribing a specific topic and broadcast whatever you want over an AsyncIterator which is compatible with graphql-subscriptions.\n\n## Usage\n\nFirst of all, you have to install the _graphql-firestore-subscriptions_ package using **yarn** or **npm** by calling either `yarn add @m19c/graphql-firestore-subscriptions` or `npm i --save @m19c/graphql-firestore-subscriptions`.\n\n### Create a new _graphql-firestore-subscription_ instance\n\n```typescript\nimport PubSub from \"@m19c/graphql-firestore-subscriptions\"\n\nconst ps = new PubSub()\n```\n\n### Adding handlers\n\nA handler gets two arguments:\n\n- The `broadcast` function itself to send new data\n- An object with options\n\nNote, that the handler **MUST** return a _unsubscribe_ function.\n\n```typescript\nps.registerHandler(() => {\n  // subscribe to a topic\n  return () => {\n    // unsubscribe\n  }\n})\n```\n\nThe unsubscribe function can either return void or a boolean value. If a boolean value is returned by the unsubscribe function, the `PubSubEngine` will throw an error if the return value is falsey.\n\n### Advanced handlers\n\nUnlike other _graphql-subscriptions_, `graphql-firestore-subscriptions` requires a handler for each topic you are about to subscribe.\nTo make the handler-creation as easy as possibile graphql-firestore-subscriptions comes across with a bunch of utility functions.\n\nThe following example shows a simple fall-through handler which takes document changes of a collection to broadcast this changes immediately.\n\n```typescript\nimport PubSub, {\n  createFallThroughHandler,\n} from \"@m19c/graphql-firestore-subscriptions\"\nimport db from \"../path/to/firestore/conenction\"\n\n// ...\n\nenum Topic {\n  NEW_COMMENT = \"NEW_COMMENT\",\n}\n\nps.registerHandler(\n  ...createFallThroughHandler(db, {\n    topic: Topic.NEW_COMMENT,\n    collection: \"comment\",\n    filter: [\"added\"],\n  })\n)\n```\n\nYou can also create multiple fall-through handlers at once:\n\n```typescript\nimport PubSub, {\n  createFallThroughHandlerFromMap,\n} from \"@m19c/graphql-firestore-subscriptions\"\nimport db from \"../path/to/firestore/connection\"\n\n// ...\n\nenum Topic {\n  NEW_COMMENT = \"NEW_COMMENT\",\n  UPDATE_COMMENT = \"UPDATE_COMMENT\",\n}\n\ncreateFallThroughHandlerFromMap(db, {\n  [Topic.NEW_COMMENT]: {\n    collection: \"comment\",\n    filter: [\"added\"],\n  },\n  [Topic.UPDATE_COMMENT]: {\n    collection: \"comment\",\n    filter: [\"modified\"],\n  },\n}).forEach((topic, handler) => ps.registerHandler(topic, handler))\n```\n\nSee API for additional information about how `createFallThroughHandlerFromMap` / `createFallThroughHandler` work.\n\n### Full example\n\n```typescript\nimport PubSub from \"@m19c/graphql-firestore-subscriptions\"\nimport db from \"../path/to/firestore/connection\"\n\nenum Topic {\n  NEW_COMMENT = \"NEW_COMMENT\",\n}\n\nconst ps = new PubSub()\n\nps.registerHandler(Topic.NEW_COMMENT, (broadcast) =>\n  // Note, that `onSnapshot` returns a unsubscribe function which\n  // returns void.\n  db.collection(\"comments\").onSnapshot((snapshot) => {\n    snapshot\n      .docChanges()\n      .filter((change) => change.type === \"added\")\n      .map((item) => broadcast(item.doc.data()))\n  })\n)\n\nconst iterator = ps.asyncIterator(Topic.NEW_COMMENT)\nconst addedComment = await iterator.next()\n\n// ...\n```\n\n### With apollo-server-graphql\n\nDefine a _GraphQL_ schema with a `Subscription` type.\n\n```graphql\nschema {\n  query: Query\n  mutation: Mutation\n  subscription: Subscription\n}\n\ntype Subscription {\n  newComment: Comment\n}\n\ntype Comment {\n  message: String\n}\n```\n\nNow, implement the resolver:\n\n```typescript\nexport const resolvers = {\n  Subscription: {\n    newComment: {\n      subscribe: () => ps.asyncIterator(Topic.NEW_COMMENT),\n    },\n  },\n}\n```\n\nCalling `asyncIterator(topics: string | string[])` or `createAsyncIterator<T>(topics: string | string[], args: T)` will subscribe to the given topics and will return an AsyncIterator bound to the `PubSubEngine` of **graphql-firestore-subscriptions**.\nEverytime, a handler calls the obtained `broadcast`-function, the `PubSubEngine` of **graphql-firestore-subscriptions** will publish the event.\n\nYou can implement the resolver with passing arguments to the subscription\n\n```typescript\nexport const resolvers = {\n  Subscription: {\n    newComment: {\n      subscribe: (parent, args, context) =>\n        ps.createAsyncIterator(Topic.NEW_COMMENT, args),\n    },\n  },\n}\n```\n\nYou will be able to access those arguments within the registered handler.\n\n```typescript\ntype MyData = { userId: string }\n\nconst handler: Handler<MyData> = (broadcast, options) => {\n  // options is of type { args: MyData }\n  const { args } = options\n\n  // Note, that `onSnapshot` returns a unsubscribe function which\n  // returns void.\n  return db\n    .collection(\"comments\")\n    .where(\"userId\", \"==\", args.userId)\n    .onSnapshot((snapshot) => {\n      snapshot\n        .docChanges()\n        .filter((change) => change.type === \"added\")\n        .map((item) => broadcast(item.doc.data()))\n    })\n}\n\nps.registerHandler(Topic.NEW_COMMENT, handler)\n```\n\n## API\n\n### createFallThroughHandler\n\n```typescript\nfunction createFallThroughHandler(\n  fs: Firestore,\n  overwriteOptions: FallThroughHandlerOptions\n): [string, Handler]\n```\n\n#### Options\n\n| Name           | Type                                  | Description                                                   |\n| -------------- | ------------------------------------- | ------------------------------------------------------------- | ----------------------------------------- |\n| `topic`\\*      | `string`                              | -                                                             |\n| `collection`\\* | `string`                              | The firebase collection                                       |\n| `transform`    | `TransformStrategy                    | (change: DocumentChange) => any`                              | Called to transform the broadcast-payload |\n| `filter`       | `(change: DocumentChange) => boolean` | Called to filter document changes before they are broadcasted |\n\n> \\* required\n\n### createFallThroughHandlerFromMap\n\n```typescript\nfunction createFallThroughHandlerFromMap(\n  fs: Firestore,\n  options: FallThroughHandlerFromMapOptions\n): [string, Handler][]\n```\n\n#### Options\n\n| Name  | Type                      | Description                                                  |\n| ----- | ------------------------- | ------------------------------------------------------------ |\n| topic | `[topic: string]: Object` | See createFallThroughHandler#Options for a complete overview |\n\n## Contribute\n\nSomething is broken? The documentation is incorrect? You're missing a feature? ...and you wanna help? That's great.\n\nThe following steps are describing the way from an idea / bug / ... to a pull-request.\n\n1. Fork this repository\n1. Apply the changes\n1. Write tests (you can execute the current tests by calling `npm run test:unit` OR `npm run test:unit:watch`)\n1. If necessary, update the documentation\n1. Open a pull-request\n1. :tada:\n\n## Best Practices\n\n### Ignore the initial snapshot\n\nAs described in #9, `onSnapshot` provides an initial snapshot of the current data. Handlers that accept changes of a collection must handle this behaviour on their own. The best way to ignore the initial snapshot is to keep a variable that captures the first event and ignores it accordingly:\n\n```typescript\nps.registerHandler(events.MESSAGE_ADDED, (broadcast) => {\n  let isInitialSnapshot = true\n  firestore.collectionGroup(\"Messages\").onSnapshot((snapshot) => {\n    if (isInitialSnapshot) {\n      isInitialSnapshot = false\n      return\n    }\n\n    // ...\n  })\n})\n```\n","readmeFilename":"README.md"}