{"id":15797662,"url":"https://github.com/ehacke/simple-cached-firestore","last_synced_at":"2025-04-30T13:40:57.147Z","repository":{"id":40289425,"uuid":"241987109","full_name":"ehacke/simple-cached-firestore","owner":"ehacke","description":"Firestore wrapper with simplified API and optional caching built in","archived":false,"fork":false,"pushed_at":"2023-03-04T06:10:13.000Z","size":2586,"stargazers_count":31,"open_issues_count":14,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-19T09:07:57.814Z","etag":null,"topics":["cache","firebase","firestore","redis"],"latest_commit_sha":null,"homepage":"https://asserted.io/posts/simplified-firestore-with-redis","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/ehacke.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-20T20:51:10.000Z","updated_at":"2025-02-16T23:31:02.000Z","dependencies_parsed_at":"2024-11-22T08:15:49.912Z","dependency_job_id":null,"html_url":"https://github.com/ehacke/simple-cached-firestore","commit_stats":{"total_commits":94,"total_committers":7,"mean_commits":"13.428571428571429","dds":0.5851063829787234,"last_synced_commit":"2cf3549a05504d9cffa9abff49c7f517a44d5872"},"previous_names":[],"tags_count":35,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ehacke%2Fsimple-cached-firestore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ehacke%2Fsimple-cached-firestore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ehacke%2Fsimple-cached-firestore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ehacke%2Fsimple-cached-firestore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ehacke","download_url":"https://codeload.github.com/ehacke/simple-cached-firestore/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251713223,"owners_count":21631505,"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":["cache","firebase","firestore","redis"],"created_at":"2024-10-05T00:10:28.546Z","updated_at":"2025-04-30T13:40:57.128Z","avatar_url":"https://github.com/ehacke.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# simple-cached-firestore\n\n![npm](https://img.shields.io/npm/v/simple-cached-firestore)\n![install size](https://badgen.net/packagephobia/install/simple-cached-firestore)\n![Codecov](https://img.shields.io/codecov/c/gh/ehacke/simple-cached-firestore)\n![CircleCI](https://img.shields.io/circleci/build/github/ehacke/simple-cached-firestore)\n![GitHub](https://img.shields.io/github/license/ehacke/simple-cached-firestore)\n\nNodeJS Firestore wrapper with simplified API, model validation, and optional caching built in.\n\n## Features\n\n- transparent, no-effort redis caching to improve speed and limit costs\n- model validation (optional, suggest using [validated-base](https://github.com/ehacke/validated-base))\n- simplified API to reduce boilerplate, but retain access to original API for special cases\n\n\n## Install\n\n```bash\nnpm i -S simple-cached-firestore\n```\n\n## Usage\n\nBefore instantiating the Firestore wrapper, we first need a model it'll use for CRUD operations.\n\nHere is a blog post on [validated models in Node](https://asserted.io/posts/type-safe-models-in-node), and why they are useful.\n\n### Create a Model\n\nAt minimum, the model has to fulfill the following interface: \n\n```typescript\ninterface DalModel {\n  id: string;\n  validate(): void | Promise\u003cvoid\u003e;\n  createdAt: Date;\n  updatedAt: Date;\n}\n```\n\nThat said, it's easiest to just extend [validated-base](https://www.npmjs.com/package/validated-base) and use that.\n\n```typescript\nimport { ValidatedBase } from 'validated-base';\nimport { IsDate, IsString, MaxLength } from 'class-validator';\nimport { toDate } from 'simple-cached-firestore';\n\ninterface ValidatedClassInterface {\n  id: string;\n\n  something: string;\n\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nclass ValidatedClass extends ValidatedBase implements ValidatedClassInterface {\n  constructor(params: ValidatedClassInterface, validate = true) {\n    super();\n\n    this.id = params.id;\n\n    this.something = params.something;\n\n    // This toDate() is necessary to convert either ISO strings or Firebase Timestamps to Date objects\n    this.createdAt = toDate(params.createdAt);\n    this.updatedAt = toDate(params.updatedAt);\n\n    if (validate) {\n      this.validate();\n    }\n  }\n\n  @IsString()\n  id: string;\n\n  @MaxLength(10)\n  @IsString()\n  something: string;\n\n  @IsDate()\n  createdAt: Date;\n\n  @IsDate()\n  updatedAt: Date;\n}\n```\n\n### Create simple-cached-firestore\n\nA single instance is responsible for reading and writing to a specific Firestore collection. \n\nReads are cached for the configured TTL, writes update the cache.\n\n```typescript\nimport admin from 'firebase-admin';\nimport { Redis } from '@ehacke/redis';\nimport { Firestore } from 'simple-cached-firestore';\n\n// Initialize Firebase client\nconst serviceAccount = require('./path/to/serviceAccountKey.json');\nadmin.initializeApp({ credential: admin.credential.cert(serviceAccount) });\n\n// Create instance of wrapper\nconst cachedFirestore = new Firestore\u003cValidatedClass\u003e({ db: admin.firestore(), redis: new Redis() });\n\nconst firebaseConfig = {\n  collection: 'some-collection',\n\n  // The object read from the db will have Firebase Timestamps in place of Dates, that the ValidatedClass must convert \n  convertFromDb: (params) =\u003e new ValidatedClass(params),\n\n  // The object being written to the db will be automatically scanned for Dates, which are converted to Timestamps\n  // NOTE: This scanning does have a performance hit, but it's assumed writes are infrequent compared to reads \n  convertForDb: (params) =\u003e params,\n};\n\nconst cacheConfig = {\n  cacheTtlSec: 5,\n  // Objects read from the cache will obviously have their Dates as ISO strings, ValidatedClass must convert to Date\n  parseFromCache: (instance) =\u003e new ValidatedClass(JSON.parse(instance)),\n  stringifyForCache: (instance: ValidatedClass) =\u003e JSON.stringify(instance),\n};\n\n// Configure simple-cached-firestore before use\ncachedFirestore.configure(firebaseConfig, cacheConfig);\n\n// Firestore wrapper is ready to go.\n```\n\n## CRUD API\n\n### create(instance: T): Promise\\\u003cT\u003e\n\nWrite a new model to the db. If an entry exists with the same ID, the write fails.\n\n```typescript\nconst validatedClass = new ValidatedClass({ id: 'foo-id', something: 'some-data', createdAt: new Date(), updatedAt: new Date() });\nawait cachedFirestore.create(validatedClass);\n```\n\n### get(id: string): Promise\u003cT | null\u003e\n\nRead a model from the db by ID. Returns a constructed instance of the model, or null.\n\n```typescript\nconst validatedClass = await cachedFirestore.get('foo-id');\n```\n\n### getOrThrow(id: string): Promise\\\u003cT\u003e\n\nRead a model from the db by ID. Returns a constructed instance of the model, or throws an Error if not found.\nUseful for cases where you know the ID should exist, and dow't want to add null checks to make Typescript happy.\n\n```typescript\nconst validatedClass = await cachedFirestore.getOrThrow('foo-id');\n```\n\n### patch(id: string, patch: DeepPartial\u003cT\u003e): Promise\u003cT\u003e\n\nPass in any subset of the properties of the model already in the db to update just those properties.\n\n`createdAt` and `updatedAt` are ignored, and `updatedAt` is set by the wrapper.\n\n```typescript\nconst validatedClass = await cachedFirestore.patch('foo-id', { something: 'patch-this' });\n```\n\n### update(id: string, update: T): Promise\\\u003cT\u003e\n\nOverwrite entire instance of model with a new instance.\n\n`createdAt` and `updatedAt` are ignored, and `updatedAt` is set by the wrapper.\n\n```typescript\nconst updatedClass = new ValidatedClass({ id: 'foo-id', something: 'updated', createdAt: new Date(), updatedAt: new Date() });\nconst validatedClass = await cachedFirestore.update('foo-id', updatedClass);\n```\n\n### exists(id: string): Promise\\\u003cboolean\u003e\n\nReturn true if ID exists in collection\n\n```typescript\nconst exists = await cachedFirestore.exists('foo-id');\n```\n\n### remove(id: string): Promise\\\u003cvoid\u003e\n\nRemove model for this ID if it exists, silent return if it doesn't\n\n```typescript\nawait cachedFirestore.remove('foo-id');\n```\n\n## Query API\n\nTo simplify the interface and to abstract it so that it can function for any db (not just Firestore), we created a simpler query language.\n\n```typescript\ninterface QueryInterface {\n  filters?: ListFilterInterface[];\n  sort?: ListSortInterface;\n  offset?: number;\n  limit?: number;\n  before?: DalModelValue;\n  after?: DalModelValue;\n}\n\ntype DalModelValue = string | Date | number | null | boolean;\n\ninterface ListFilterInterface {\n  property: string;\n  operator: FILTER_OPERATORS;\n  value: DalModelValue;\n}\n\nenum FILTER_OPERATORS {\n  GT = '\u003e',\n  GTE = '\u003e=',\n  LT = '\u003c',\n  LTE = '\u003c=',\n  EQ = '==',\n  CONTAINS = 'array-contains',\n}\n\ninterface ListSortInterface {\n  property: string;\n  direction: SORT_DIRECTION;\n}\n\nenum SORT_DIRECTION {\n  ASC = 'asc',\n  DESC = 'desc',\n}\n```\n\nIn use, it looks like this:\n\n```typescript\n// Find all objects with property 'something' equal to 'some-value'\nconst simpleMatchQuery = {\n  filters: [\n    {\n      property: 'something',\n      operator: FILTER_OPERATORS.EQ,\n      value: 'some-value',\n    } \n  ],\n}\n\n// Can add multiple conditions\nconst compoundMatchQuery = {\n  filters: [\n    {\n      property: 'something',\n      operator: FILTER_OPERATORS.EQ,\n      value: 'some-value',\n    },\n    {\n      property: 'another',\n      operator: FILTER_OPERATORS.EQ,\n      value: 'something-else',\n    } \n  ],\n}\n\n// Use sorting, offset and limits\nconst sortedQuery = {\n  filters: [\n    {\n      property: 'something',\n      operator: FILTER_OPERATORS.EQ,\n      value: 'some-value',\n    } \n  ],\n  sort: {\n    property: 'createdAt',\n    direction: SORT_DIRECTION.DESC,\n  },\n  limit: 100, // Return 100 values max\n  offset: 20, // Start at the 20th value in descending order\n}\n\n// Use pagination\nconst paginatedQuery = {\n  filters: [\n    {\n      property: 'something',\n      operator: FILTER_OPERATORS.EQ,\n      value: 'some-value',\n    } \n  ],\n  sort: {\n    property: 'createdAt',\n    direction: SORT_DIRECTION.DESC,\n  },\n  limit: 100, // Return 100 values max\n  // Before or After should match sort property\n  after: 'created-at-1', // Show page of up to 100, with entries that occur after the createdAt 'created-at-1'\n}\n```\n\nThen just pass the query to simple-cached-firestore\n\n```typescript\nconst simpleMatchQuery = {\n  filters: [\n    {\n      property: 'something',\n      operator: FILTER_OPERATORS.EQ,\n      value: 'some-value',\n    } \n  ],\n}\n\nconst results = await cachedFirestore.query(simpleMatchQuery);\n```\n\nNOTE: queries are cached, but not very well. Any writes to this collection that occur after a cached query will invalidate the entire query cache.\n\n## Special Cases\n\nFor situations where you need to access the underlying Firestore instance, you can do that.\n\n```\ncachedFirestore.services.firestore === admin.firestore.Firestore\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fehacke%2Fsimple-cached-firestore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fehacke%2Fsimple-cached-firestore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fehacke%2Fsimple-cached-firestore/lists"}