Coder Social home page Coder Social logo

arrowheadapps / strapi-connector-firestore Goto Github PK

View Code? Open in Web Editor NEW
166.0 15.0 15.0 4.1 MB

Strapi database connector for Firestore database on Google Cloud Platform.

License: MIT License

TypeScript 84.93% JavaScript 15.07%
strapi firebase firestore gcp orm firestore-database google-cloud-platform

strapi-connector-firestore's Introduction

⚠️⚠️ Archived ⚠️⚠️

As discussed below, Strapi V4 dropped support for pluggable database connectors. This project is no longer maintained and so has been archived.

⚠️⚠️ Warning: pre-release ⚠️⚠️

This is package an early work in progress an is not suitable for production in it's current state. Feel free to use it an feedback any issues here: https://github.com/arrowheadapps/strapi-connector-firestore/issues

The shape of the generated database output may break compatibility often while in "alpha" state.

Known issues/not implemented:

  • Deep filtering queries

I welcome contributors to help get this package to a production ready state and maintain it.

See the discussion in issue #1.

strapi-connector-firestore

NPM Version Monthly download on NPM Tests codecov Snyk Vulnerabilities GitHub bug issues GitHub last commit

Note about Strapi V4 support: As per this blog post, the V4 release of Strapi does not have a pluggable database connector layer like V3 did, and therefore cannot support third-party database connectors. The good news is that an extensible database layer is being developed in partnership between Strapi and MongoDB, and that this will hopefully represent a great step forward in extensibility for third-party connectors like this one. However, that pluggable database layer is not expected any sooner than the last half of 2022.

In short, this connector cannot be used with Strapi V4 until the pluggable database layer is released.

Strapi database connector for Cloud Firestore database on Google Cloud Platform.

Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud Platform.

It has several advantages such as:

  • SDKs for Android, iOS, Web, and many others.
  • Realtime updates.
  • Integration with the suite of mobile and web development that come with Firebase, such as Authentication, Push Notifications, Cloud Functions, etc.
  • Generous free usage tier so there is no up-front cost to get started.

Requirements

  • NodeJS >= 12
  • Strapi version compatible with ^3.0.0 (Strapi V4 is not supported)

Installation

Install the NPM package:

$ npm install --save strapi-connector-firestore

Configure Strapi to use the Firestore database connector in ./config/database.js:

// ./config/database.js
module.exports = ({ env }) => ({
  defaultConnection: 'default',
  connections: {
    default: {
      connector: 'firestore',
      settings: {
        projectId: '{YOUR_PROJECT_ID}',
      },
      options: {
        // Connect to a local running Firestore emulator
        // when running in development mode
        useEmulator: env('NODE_ENV') == 'development',
      }
    }
  },
});

Examples

See some example projects:

Usage Instructions

Connector options

These are the available options to be specified in the Strapi database configuration file: ./config/database.js.

Name Type Default Description
settings Object undefined Passed directly to the Firestore constructor. Specify any options described here: https://googleapis.dev/nodejs/firestore/latest/Firestore.html#Firestore. You can omit this completely on platforms that support Application Default Credentials such as Cloud Run, and App Engine. If you want to test locally using a local emulator, you need to at least specify the projectId.
options.useEmulator boolean | undefined false Connect to a local Firestore emulator instead of the production database. You must start a local emulator yourself using firebase emulators:start --only firestore before you start Strapi. See https://firebase.google.com/docs/emulator-suite/install_and_configure.
options.logTransactionStats boolean | undefined process.env.NODE_ENV === 'development' Indicates whether or not to log the number of read and write operations for every transaction that is executed. Defaults to true for development environments and false otherwise.
options.logQueries boolean | undefined false Indicates whether or not to log the details of every query that is executed. Defaults to false.
options.singleId string | undefined "default" The document ID to used for singleType models and flattened models.
options.flattenModels boolean | string | RegExp | FlattenFn | (string | RegExp | FlattenFn)[]

Where FlattenFn is (model: StrapiModel) => string | boolean | DocumentReference | null | undefined.
false A boolean to enable or disable flattening on all models, or a regex or (or array of those) that are matched against the uid property of each model, or a function (or array of those) used to test each model to determine if it should be flattened (see collection flattening). If a function is provided it must return a string which acts as the singleId or true (in which case the singleId option is used) or "falsey". If the returned string contains "/" characters then it is parsed as a document path (overrides the models collectionName option). It can also return a DocumentReference which is used as the document to store the flattened collection (create using model.firestore.doc(...)).

This is useful for flattening models built-in models or plugin models where you don't have access to the model configuration JSON.
options.allowNonNativeQueries boolean | string | RegExp | TestFn | (string | RegExp | TestFn)

Where TestFn is (model: StrapiModel<T>) => boolean.
false Indicates whether to allow the connector to manually perform search and other query types than are not natively supported by Firestore (see Search and non-native queries). These can have poor performance and higher usage costs, and can cause some queries to fail. If disabled, then search and OR filters will not function. If a string or RegExp is provided, then this will be tested against each model's uid (but this may still be overridden by the model's own setting).
options.ensureComponentIds boolean | undefined true If true, then ID's are automatically generated and assigned on component instances if they don't already exist. ID's are assigned immediately before being sent to Firestore, so they aren't be assigned yet during lifecycle hooks. This setting applies to component models only, and has no effect on normal models.
options.maxQuerySize number | undefined 200 If defined, enforces a maximum limit on the size of all queries. You can use this to limit out-of-control quota usage. Does not apply to flattened collections which use only a single read operation anyway. Set to 0 to remove the limit.

WARNING: It is highly recommend to set a maximum limit, and to set it as low as applicable for your application, to limit unexpected quota usage.
options.metadataField string | ((attrKey: string) => string) | undefined "$meta" The field used to build the field that will store the metadata map which holds the indexes for repeatable and dynamic-zone components. If it is a string, then it will be combined with the component field as a postfix. If it is a function, then it will be called with the field of the attribute containing component, and the function must return the string to be used as the field. See Indexing fields in repeatable and dynamic-zone components.
options.creatorUserModel string | { model: string, plugin?: string } { model: "user", plugin: "admin" } If defined, then overrides the model that is associated with creator fields such as "created_by" and "updated_by". It defaults to the Strapi administrator user model, but you can use this to associate the creator fields with a model of your choice.

If you use this setting, then you must also implement a custom authentication policy for Strapi. Relations to this model are simply created by taking the id field of the ctx.state.user object which is created by strapi-admin's authentication policy.
options.beforeMountModel ((model: StrapiModel) => void | Promise<void>) | undefined undefined A hook called before each model is loaded. This can be used to modify any model (particularly useful for builtin or plugin models), before it is loaded into the Firestore connector.
options.afterMountModel ((model: FirestoreConnectorModel) => void | Promise<void>) | undefined undefined A hook called after each model is loaded. Modifying a model after it has been loaded is not recommended because it can cause unexpected behaviour.

Model options

In addition to the normal model options, you can provide the following to customise Firestore behaviour. This configuration is in the model's JSON file: ./api/{model-name}/models/{model-name}.settings.json.

Name Type Default Description
collectionName string | undefined globalId Controls the Firestore collection name used for the model. Defaults to the model's globalId if not provided.
options.singleId string | undefined undefined If defined, overrides the connector's global singleId setting (see above) for this model. Note: this is overridden by the result of the connector's flattenModels option if a function is provided there.
options.flatten boolean | undefined undefined If defined, overrides the connector's global flattenModels setting (see above) for this model.
options.allowNonNativeQueries boolean | undefined undefined If defined, overrides the connector's global allowNonNativeQueries setting (see above) for this model. If this model is flattened, this setting is ignored and non-native queries including search are supported.
options.searchAttribute string | undefined undefined If defined, nominates a single attribute to be queried natively, instead of performing a manual search. This can be used to enable primitive search when fully-featured search is disabled because of the allowNonNativeQueries setting, or to improve the performance or cost of search queries when full search is not required. See Search and non-native queries.
options.ensureComponentIds boolean | undefined undefined If defined, overrides the connector's global ensureComponentIds setting (see above) for this model. This setting applies to component models only, and has no affect on normal models.
options.maxQuerySize number | undefined undefined If defined, overrides the connector's global maxQuerySize setting (see above) for this model. Set to 0 to disable the limit.
options.logQueries boolean | undefined undefined If defined, overrides the connector's global logQueries setting (see above) for this model.
options.converter { toFirestore?: (data: Object) => Object, fromFirestore?: (data: Object) => Object } undefined An object with functions used to convert objects going in and out of Firestore. The toFirestore function will be called to convert an object immediately before writing to Firestore. The fromFirestore function will be called to convert an object immediately after it is read from Firestore. You can config this parameter in a Javascript file called ./api/{model-name}/models/{model-name}.config.js, which must export an object with the converter property.
options.metadataField string | ((attrKey: string) => string) | undefined undefined If defined, overrides the connector's global metadataField setting (see above) for this model.
options.creatorUserModel string | { model: string, plugin?: string } undefined If defined, overrides the connector's global creatorUserModel setting (see above) for this model.
options.onChange ((previousData: T | undefined, newData: T | undefined, transaction: Transaction, ref: Reference<T>) => void | Promise<void>) | undefined undefined A hook that is called inside the transaction whenever an changes. This is called before any change is committed to the database. If an exception is thrown, then the transaction will be aborted. Any additional write created by the hook will be committed atomically with this transaction.

This hook can return a function or async function which will be run only once, after the transaction has succeeded.
options.virtualDataSource DataSource | undefined undefined Makes this model a virtual model, with the given object acting as the data source. Virtual models are not stored in Firestore, but the given object acts as the proxy to fetch and store data in it's entirety. This can be used to create in-memory or on-disk models.
attributes.*.index true | string | { [key: string]: true | IndexerFn }

Where IndexerFn is (value: any, component: object) => any.
undefined Only relevant for attributes in component models. When the component is embedded as a repeatable or dynamic-zone component, this indicates that the containing parent document should maintain an index for this attribute (see Indexing fields in repeatable and dynamic-zone components).

If true, the attribute will be indexed with the attribute name used as the key. If string, the attribute will be indexed with this string used as the key. If an object, then the attribute will be indexed with each key in the object. The value in the object can be true directing an index aliased with that key, or a function which can map and filter values in the index. The function takes the value to be indexed, and the containing component object, and returns the value to be stored in the index, or undefined for the value to be omitted.

Minimal example

This is the minimum possible configuration, which will only work for GCP platforms that support Application Default Credentials.

// ./config/database.js
module.exports = ({ env }) => ({
  defaultConnection: 'default',
  connections: {
    default: {
      connector: 'firestore',
    }
  },
});

Full example

This configuration will work for production deployments, and also local development using an emulator (when process.env.NODE_ENV == 'development').

For production deployments on non-GCP platforms (not supporting Application Default Credentials), make sure to download a service account key file, and set an environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to the file. See Obtaining and providing service account credentials manually.

// ./config/database.js
module.exports = ({ env }) => ({
  defaultConnection: 'default',
  connections: {
    default: {
      connector: 'firestore',
      settings: {
        projectId: '{YOUR_PROJECT_ID}',
      },
      options: {
        singleId: 'default',

        // Decrease max query size limit (default is 200)
        maxQuerySize: 100,

        // Connect to a local running Firestore emulator
        // when running in development mode
        useEmulator: env('NODE_ENV') == 'development',

        // Flatten all internal Strapi models into a collection called "strapi"
        // WARNING: this is an example only, flattening the internal Strapi models is
        // not actually an effective usage of flattening, because they are only 
        // queried one-at-a-time anyway so this would only result in increased bandwidth usage
        flattenModels: ({ uid, collectionName }) => 
          ['strapi::', 'plugins::users-permissions'].some(str => uid.includes(str)) ? `strapi/${collectionName}` : false,

        // Enable search and non-native queries on all except
        // internal Strapi models (beginning with "strapi::")
        allowNonNativeQueries: /^(?!strapi::).*/,
      }
    }
  },
});

Model configuration examples

You can override some configuration options in each models JSON file ./api/{model-name}/models/{model-name}.settings.json.

In this example, the collection will be flattened and the connector's singleId option will be used as the document name, with the collection name being collectionName or globalId (in this example, "myCollection/default"):

{
  "kind": "collectionType",
  "collectionName": "myCollection",
  "options": {
    "flatten": true
  }
}

The document name can also be specified explicitly (in this example "myCollection/myDoc"):

{
  "kind": "collectionType",
  "collectionName": "myCollection",
  "options": {
    "flatten": "myDoc"
  }
}

You can also override the connector's allowNonNativeQueries option:

{
  "kind": "collectionType",
  "collectionName": "myCollection",
  "options": {
    "allowNonNativeQueries": true
  }
}

You can specify data converters for a model in ./api/{model-name}/models/{model-name}.settings.js, like the example below:

module.exports = {
  kind: 'collectionType',
  collectionName: 'myCollection',
  options: {
    converter: {
      toFirestore: (data) => {
        // Convert the data in some way immediately before
        // writing to Firestore
        return {
          ...data,
        };
      },

      fromFirestore: (data) => {
        // Convert the data in some way immediately after
        // reading from Firestore
        return {
          ...data,
        };
      },
    },
  },
  attributes: {
    // ...
  },
};

Collection flattening

You can choose to "flatten" a collection of Firestore documents down to fields within a single Firestore document. Considering that Firestore charges for document read and write operations, you may choose to flatten a collection to reduce usage costs, however it imposes collection size limits and may introduce contention.

Flattening may be especially beneficial for collections that are often counted or queried in their entirety anyway. It will cost a single read to retrieve the entire flattened collection. If a collection is normally only queried one document at a time, then that would only have resulted in a single in the first place.

Flattening also enables search and other query types that are not natively supported in Firestore.

Before choosing to flatten a collection, consider the following:

  • The collection should be bounded (i.e. you can guarantee that there will only be a finite number of entries). For example, a collection of users would be unbounded, but Strapi configurations and permissions/roles would be bounded.
  • The number of entries and size of the entries must fit within a single Firestore document. The size limit for a Firestore document is 1MiB (see limits).
  • The benefits of flattening will be diminished if the collection is most commonly queried one document at a time (in such a case, flattening could increase bandwidth usage with same amount of read operations, although the connector minimises bandwidth using field masks where possible).
  • The collection should not me modified often. If entries in the collection are written to at a high frequency, or by different users/clients, then flattening could cause contention, because all of those writes would be targetting the same document.
  • Firestore document change listeners will be triggered for all changes to any entry in the collection (because the entire collection is stored within the single document).
  • Firestore security rules will apply at the collection-level (because the entire collection is stored within the single document).

Search and non-native queries

Firestore does not natively support search. Nor does it support several Strapi filter types such as:

  • OR filters
  • 'contains' (case-insensitive string contains)
  • 'containss' (case-sensitive string contains)
  • 'ncontains' (case-insensitive string doesn't contain)
  • 'ncontainss' (case-sensitive string doesn't contain)

This connector manually implements search and these other filters by reading the Firestore collection in blocks without any filters, and then "manually" filtering the results. This can cause poor performance, and also increased usage costs, because more documents are read from Firestore.

You can enable manual search and manual implementations of unsupported queries by using the allowNonNativeQueries option, but you should consider cost exposure. It is therefore recommended that you only enable this on models that specifically require it.

You can enable a primitive kind of search without enabling allowNonNativeQueries by using the searchAttribute setting. This nominates a single attribute to query against when a search query is performed. If the attribute is a string, a search query will result in a case-sensitive query for strings starting with the given query term (case-sensitive string prefix). If the attribute of any other type, a search query will result in an equality query on this attribute.

If searchAttribute is defined, the primitive search behaviour is used regardless of whether or not fully-featured search is available.

Flattened models support all of these filters including search, because the collection is fetched as a whole and filtered "manually" anyway.

Indexing fields in repeatable and dynamic-zone components

Repeatable components and dynamic-zone components are embedded as an array in the parent document. Firestore cannot query the document based on any field in the components (cannot query inside array).

To support querying, the connector can be configured to maintain a metadata map (index) for any attribute inside these repeatable components. This configured by adding "index": true to any attribute in the component model JSON. This is ignored for non-repeatable components, because they can be queried directly.

The connector automatically does this for all relation attributes. Even if an object is provided for index, the connector will always ensure there is a default indexer for all relation attributes, which is required for reverse-lookup of relations to function.

The metadata map is stored in the document in a field named by appending "$meta" to the name of the field storing the components. The map contains a field for every indexed attribute, and each field is an array of all the unique values of that attribute on all the components, or null if there are no values. If the attribute itself is an array (e.g. many-way relations), then the array is flattened.

Indexers can be defined for the component ID (primary key) also, and if only the index property is defined, then the attribute will be deleted after the indexers have been collected. This way, you can index the IDs without the attribute being visible in the content manager.

Note: when indexing attributes inside components, the data will be duplicated inside the document, increasing document size and bandwidth costs.

For example, consider a model JSON with the shape below:

{
  "attributes": {
    "myRepeatableComponents": {
      "component": "my-component",
      "repeatable": true
    }
  }
}

Where the "my-component" model JSON is like below:

{
  "attributes": {
    "name": {
      "type": "string",
      "index": true
    },
    "name": {
      "type": "string",
      "index": true
    },
    "related": {
      "model": "otherModel"
    }
  }
}

Such a model may have a document with the database output below:

{
  "myRepeatableComponents": [
    {
      "name": "Component 1",
      "related": "/otherModel/doc1", (DocumentReference)
    },
    {
      "name": "Component 2",
      "related": null,
    }
  ],
  "myRepeatableComponents$meta": {
    "name": [
      "Component 1",
      "Component 2"
    ],
    "related": [
      "/collection/doc1", (DocumentReference)
      null
    ]
  },
}

Where the myRepeatableComponents$meta field is automatically maintained and overwritten by the connector.

In this example, we can query documents based on a field inside a component using a query like .where('myRepeatableComponents$meta.name', 'array-contains', 'Component 1'). We can also query a document that contains any value with .where('myRepeatableComponents$meta.name', '!=', null), or a document that contains no values with .where('myRepeatableComponents$meta.name', '==', null).

Advanced indexing can be configured as below (in the component model config):

module.exports = {
  options: {
    // Rename the metadata map with prefix rather than postfix
    metadataField: field => `index$${field}`,
  },
  attributes: {
    id: {
      // Index the primary key
      // This attribute will be removed from existence after the indexer configuration is collected
      index: true,
    }
    name: {
      type: 'string',
      // Index and rename the metadata key instead of default "name"
      // Non-relation attributes are not indexed by default
      index: 'names',
    }
    active: {
      type: 'boolean',
      // Index with default name "active"
      index: true,
    },
    related: {
      collection: 'other-model',
      // Because it is a relation, the connector will always apply a
      // default indexer in addition to those defined in indexedBy
      // but we can override the key
      index: {
        // Rename default indexer
        // If this is omitted then a default indexer with the
        // attribute name "related" will be ensured because
        // this is a relation attribute
        relations: true,
        // Create an index of all relations that are active
        relationsActive: (value, obj) => obj.active ? value : undefined,
        // Create an index of all relations that are inactive
        relationsInactive: (value, obj) => obj.active ? undefined : value,
      },
    },
  },
};

Which would result in a database output like below:

{
  "myRepeatableComponents": [
    {
      "name": "Component 1",
      "active": true,
      "related": "/otherModel/doc1" (DocumentReference)
    },
    {
      "name": "Component 2",
      "active": false,
      "related": null
    }
  ],
  "index$myRepeatableComponents": {
    "names": [
      "Component 1",
      "Component 2"
    ],
    "active": [
      true,
      false
    ],
    "relations": [
      "/collection/doc1", (DocumentReference)
      null
    ],
    "relationsActive": [
      "/collection/doc1" (DocumentReference)
    ],
    "relationsInactive": [
      null
    ]
  }
}

Considerations

Strapi components (including dynamic-zone, repeatable)

The official Strapi connectors behave in a way where components are stored separately in their own collections/tables.

However, this connector behaves differently. It embeds all components directly into the parent document, and there are no collections for components. Here are the motivations behind this behaviour:

  • Firestore charges for the number of operations performed, so we typically wish to minimise the number of read operations accrued. Embedding the components means no additional reads are required.
  • Firestore doesn't natively support populating relational data, so embedding components reduces the latency that would be incurred by several round trips of read operations.
  • Typical usage via the Strapi admin front-end doesn't allow reuse of components, meaning all components are unique per parent document anyway, so there is not reason not to embed.

Be aware of the Firestore document size limit, so a single document can contain only a finite number of embedded components.

The connector automatically maintains an index for every relation inside a repeatable component or a dynamic-zone component. This "index" is a map stored in the parent document alongside the embedded components (See Indexing fields in repeatable and dynamic-zone components), and enables reverse-lookup of relations inside components.

Indexes

Firestore requires an index for every query, and it automatically creates indexes for basic queries (read more).

Depending on the sort of query operations you will perform, this means that you may need to manually create indexes or those queries will fail.

Costs

Unlike other cloud database providers that charge based on the provisioned capacity/performance of the database, Firestore charges based on read/write operations, storage, and network egress.

While Firestore has a free tier, be very careful to consider the potential usage costs of your project in production.

Be aware that the Strapi Admin console can very quickly consume several thousand read and write operations in just a few minutes of usage.

Particularly, when viewing a collection in the Strapi Admin console, the console will count the collection size, which will incur a read operation for every single document in the collection. This would be disastrous for quota usage for large collections. This is why it is highly recommended to apply the maxQuerySize setting, and to set it as low as possible.

For more info on pricing, see the pricing calculator.

Security

The Firestore database can be accessed directly via the many client SDKs available to take advantage of features like realtime updates.

This means that there will be two security policies in play: Firestore security rules (read more), and Strapi's own access control via the Strapi API (read more).

Be sure to secure your data properly by considering several options

  • Disable all access to Firestore using security rules, and use Strapi API only.
  • Restrict all Strapi API endpoints and use Firestore security rules only.
  • Integrate Strapi users, roles and permissions with Firebase Authentication and configure both Firestore security rules and Strapi access control appropriately.

strapi-connector-firestore's People

Contributors

brettwillis avatar dependabot[bot] avatar luisrodriguezld avatar snyk-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

strapi-connector-firestore's Issues

TypeError: Cannot read property 'reduce' of undefined when trying to edit a role in Users & Permissions Plugin

When trying to edit a role in Users & Permissions Plugin the page briefly loads, then I get "an error occurred" modal and this error in the console:

TypeError: Cannot read property 'reduce' of undefined at Object.getRole (/Users/davide/Sviluppo/App/node_modules/strapi-plugin-users-permissions/services/UsersPermissions.js:200:42) at async Object.getRole (/Users/davide/Sviluppo/App/node_modules/strapi-plugin-users-permissions/controllers/UsersPermissions.js:92:18) at async /Users/davide/Sviluppo/App/node_modules/strapi/lib/middlewares/router/utils/routerChecker.js:79:22 at async /Users/davide/Sviluppo/App/node_modules/strapi-utils/lib/policy.js:68:5 at async /Users/davide/Sviluppo/App/node_modules/strapi/lib/middlewares/parser/index.js:48:23 at async /Users/davide/Sviluppo/Appnode_modules/strapi/lib/middlewares/xss/index.js:26:9 [2020-11-23T18:20:17.225Z] debug GET /users-permissions/roles/2xmxFW0T9DJK0Y6dCYmU (661 ms) 500

Strapi 3.3.3
strapi-connector-firestore ^3.0.0-alpha.22

Feature: Firestore subcollections

Allow a model to be configured as a subcollection of a parent model, by adding a parentModel option.

{
  "options": {
    "parentModel": "users"
  },
  "attributes": {
    "owner": {
      "model": "users"
    }
  }
}

If there is a one-way relation to the parent model, then that reltion will become the partent document of the subcollection. If there is no one-way relation to the parent model, then one will be implicitly created with the name "parent".

If the parent relation is changed, then the document will be deleted from the old subcollection and added to the new subcollection.

How to run locally and deploy

Hi @brettwillis
Thanks for this project. I have installed this package and changed ./config/database.js accordingly. What are the next steps to run locally and deploy. Can I deploy my strapi thing to firebase or should I use heroku or something like that?

Cannot delete medias

When deleting a media, the media files are deleted in storage, but the related firestore document is not removed. It seems it occurs even when the image has no dependency attached to it (i.e is not used anywhere in Strapi)

After manually deleting the firestore docs, it works fine.

The Strapi-side error is:
Error: Operations are not supported on component collections. This connector embeds components directly into the parent document.

Thank you for this amazing work ! I was so excited when I saw someone finally built a Strapi Firestore connector 🚀

Feature: Utilise read-only transaction

The Firestore Node.js SDK verision 4.13.0 made read-only transactions availeble. This should be utilised where appropriate to avoid acquiring locks on documents.

Relation handler cannot handle FieldOperation values

The FieldOperation (similar to the native FieldValue) class is currently a private API, so this is low priority. But we may wish to make FieldOperation a public API, at which point this will be a problem.

If the user runs a direct transaction (model.runTransaction(...)) and uses FieldOperation to atomically update a relation attribute, then the RelationHandler will crash - it cannot yet handle FieldOperation instances.

See:

if (!(ref instanceof Reference)) {
throw new Error('Value is not an instance of Reference. Data must be coerced before updating relations.')
}

Execute component lifecycle hooks

Because components are embedded directly in the parent document, no lifecycle hooks are executed for those components.

Also ensure that lifecycle hooks are executed with #17.

Also, confirm that components can even have lifecycle hooks? The default configuration provides only the model JSON.

Fix coercion lifecycle

Currently there is a discrepancy between the result that is returned from query API calls and the data that is written to Firestore.

  • This is because coversions and coercions are performed on the way to Firestore but not performed on the returned data
  • This includes adding IDs to components
  • On update operations, the returned data does not reflect the entire document, only the (potentially partial) input data

Proposal:

  • Migrate type coercion into a coerceToModel() function which operates on incoming data
  • Lightweight transform in document converter, which handles conversion of custom types to Firestore-compatible type
  • Lightweight transform in document converter, which rebuilds certain custom types from Firestore-native
  • No need to coerce/convert to API result, as it is handled by JSON.stringify() with toJSON instance method

To be determined:

  • Should we write to Firestore and then read back the result for API response
    • Pros: Less processing
    • Cons: converters operate on partial data, write->read is non atomic, the data could have changed in Firestore before it is read back
  • Or should we read from Firstore, combine the data manually, then write the modified data to Firestore and also return it
    • Pros: Full data in converters, enables existence check for flat collections, read->write can be an atomic operation
    • Cons: More processing, manual merging

Feature: Search delegates

Currently, search is implemented in one of three ways

  • Not allowed (default)
  • Prefix-like search on a single attribute
  • Full manual search (fetching queries in chunks, or natuarally for flattened collections)

This proposal is to introduce a seachDelegate option which will be used to delagate search to an external service (e.g. Algolia).

This delegate can also be used for count queries, where counting may be costly in Firestore.

Strapi strarting issue after connect with strapi-connector-firestore

Error 👇

TypeError: Cannot set property privateAttributes of #<Object> which has only a getter at Function.assign (<anonymous>) at mountModel (E:\WorkSpace\JS\COMPANY PROJECTS\kinderact\node_modules\strapi-connector-firestore\lib\model.js:166:26) at Object.mountModels (E:\WorkSpace\JS\COMPANY PROJECTS\kinderact\node_modules\strapi-connector-firestore\lib\model.js:56:21) at E:\WorkSpace\JS\COMPANY PROJECTS\kinderact\node_modules\strapi-connector-firestore\lib\index.js:82:21 at async Promise.all (index 0) at async Object.initialize (E:\WorkSpace\JS\COMPANY PROJECTS\kinderact\node_modules\strapi-connector-firestore\lib\index.js:49:9) at async Object.initialize (E:\WorkSpace\JS\COMPANY PROJECTS\kinderact\node_modules\strapi-database\lib\connector-registry.js:30:9) at async DatabaseManager.initialize (E:\WorkSpace\JS\COMPANY PROJECTS\kinderact\node_modules\strapi-database\lib\database-manager.js:43:5) at async Strapi.load (E:\WorkSpace\JS\COMPANY PROJECTS\kinderact\node_modules\strapi\lib\Strapi.js:354:5) at async Strapi.start (E:\WorkSpace\JS\COMPANY PROJECTS\kinderact\node_modules\strapi\lib\Strapi.js:196:9)

Prevent out-of-control quota usage

The Strapi admin font end is designed to count the size of a collection when displaying the list of collection entries (it uses this to determine the number of pages).

A single such query can be disasterous for Firestore cost/quota usage, let alone many such queries during normal usage of the admin front end, because

  • Firestore will acrue a read operation for every document that is counted
  • If the collection being displayed is large (i.e. the a collection of users, which could be hundreds of thousands or even millions), then this will result in hundreds of thousands or millions of reads billed everytime the list is viewed

Consider also that the /count API could be used or abused by authorized users outside the admin fton end.

Also, this doesn't apply only to the count() API, but also find() and search() where the filters would return a large number of results.

Proposal 1 (fallback limiting)

As a fallback in place of any better solution, I propose to add a maxQuerySize option to the settings (and also allow it to be overridden per model) which will enforce a maximum limit on all queries (including count()).

  • This is a fallback to prevent out-of-control quota usage for a single query
  • It wouldn't prevent a large amount of quota usage for for multiple queries
  • It would cause inconsistent/unexpected behaviour when the user actually wants to reliably count the exact collection size (arguably a bad idea anyway - a counter should be maintained instead)

The documentation would recommend always setting the lowest appropriate limit.

Should there be a default limit? If so, what should it be?

Proposal 2 (disable counting)

Apply a PR or patch to the Strapi font-end so it doesn't try to count collections at all. This would drastically reduce usage.

Perhaps we include a proposed patch in the examples or documetation so that people can apply it if they wish. But a PR to Strapi with to make counting behaviour configurable would be far better.

Alternatively we could add an option to disable the count() API entirely in this connector, which would cause unexpected results in the font-end.

This in itself doesn't limit out-of-control usage for find() and search() APIs, so should be used in conjunction with proposal 1.

Proposal 3 (automatically maintain a counter)

For every collection maintain a counter of the number of documents (see https://stackoverflow.com/a/49407570/1513557). This would only work for counting the total number of documents without any filter applied an would immediately become useless when a filter is applied.

Unable to login in local environment

I'm trying to run examples/cloud-run-and-hosting locally, but stuck on login screen. Any help would be appreciated.

What I did:

  • Run npm install on root and examples/cloud-run-and-hosting
  • Changed the projectId in cloud-run-and-hosting/.firebaserc
  • Run firebase init on /cloud-run-and-hosting
  • Change the projectId in /cloud-run-and-hosting/config/database.js
  • Created /cloud-run-and-hosting/.env
    GCP_PROJECT: my-firebase-project
    ADMIN_JWT_SECRET=jwt-token (created by running node -e "console.log(require('crypto').randomBytes(64).toString('base64'))" # (all users))
  • Added the jwt token to cloud-run-and-hosting/server.js
  • When I launched the emulator locally, port 8080 was taken. I changed the port in firebase.json to 8081. Along with this, change the port of server.js to 8082 => Could not start up with error
  • Changed the port under root/index.ts, root/index.js, and root/firestore.js to 8081
  • The emulator and Strapi application started up successfully
  • When I tried to visit the login page (http://localhost:8082/admin/auth/login), I was redirected. I tried to register with my existing credentials then reload
  • Login page appeared. I tried to login => 500 error and could not log in

Am I missing something?
I'm also stuck on deploying the example project to the remote, but I'll try some more and if it doesn't work, I'll ask a separate question later.

Related: #11

Support `BigInt`

Currently BigInt's are stored in Firestore as strings, which means that comparison operators are not robust.

For example, in string comparison: '123' < '1221' is (incorrectly) false.

Unable to deploy project to remote

Hi Brett,

I tried for a day, but the deployment still did not work for me.
If you could give me some advice when you have time, it would be very much appreciated.

The deployment itself seems to be working, but both the front-end and back-end content is not being served, and the default FirebaseHosting screen is displayed. Please see the attached images.
No errors are shown in the browser console for FrontEnd.
As for the Backend, the default screen shows "Error loading the Firebase SDK, check the console".
frontend
backend

Here is what I did.
// Front end

  • ran SET NODE_ENV=production
    (ref: https://stackoverflow.com/a/11928210/13868142)
  • ran npm run build
  • in firebase.json, change
  • "hosting": {"public" : @"public"}
    to
    "hosting": { "public" : "build"}
  • ran firebase deploy --only hosting

// Backend

  • enabled permissions as stated below
    https://cloud.google.com/build/docs/deploying-builds/deploy-cloud-run#before_you_begin
  • in package.json, deleted "strapi-connector-firestore": "../... /" and ran strapi-connector-firestore
  • in Dockerfile, commented out User node (I had to do this in order to avoid "Error: EACCES: permission denied, mkdir '/usr/src/app/node_modules'")
  • from the cloud run console, add environment variable ADMIN_JWT_SECRET=JWT_token
  • ran npm run deploy:backend => Successfully deployed but the browser shows "Error loading the Firebase SDK, check the console."
  • thought I need to install firebase sdk, so ran npm install --save firebase under the cloud-run-and-hosting folder
  • ran npm run deploy:backend again => still have the same error

Thank you in advance.

Single / Multiple Type Images support

Hey, @brettwillis
Thank you a lot for moving forward firestore support for Strapi.
I've found several bugs and wanted to know, how can I help you to solve them.

Firstly, Image from StrapiUploadPlugin always returns array of objects. But the desired behavior — to return array, if Image is type of Multiple and an object if it is of type Single.

Secondly, I think it's problem with parsing nested objects.
Here is a structure:

  • Dynamic zone
    Component

    • Repeatable Component

    • Images Array

Instead of returning array of images objects it return array of references.

Here is a fragment from response.

{
    Footer: [
        {
            __component: "shared.footer-default",
            Column: [
                {
                    Images: [
                        "upload_file/3By5RQEedQIGpJeYhnfn",
                        "upload_file/3By5R234525ssdfYhn31"
                    ],
                    Title: "INFORMATION"
                },
                {
                    Images: [
                        "upload_file/3By5RQEedQIGpJeYhnfn",
                        "upload_file/3By5R234525ssdfYhn31"
                    ],
                    Title: "PAYMENT METHODS"
                },
                {
                    Images: [
                        "upload_file/3By5RQEedQIGpJeYhnfn",
                        "upload_file/3By5R234525ssdfYhn31"
                    ],
                    Title: "GUARANTEE"
                },
                {}
            ],
        }
    ],
}

Example project: EACCES in npm install

Commit 4a1cfa5 introduced use of the non-privileged user in docker install and run, both to allow use of patch-package without unsafe permissions (user needs to be set before install), and as security best-practice (user can be set after install).

While this works for local Docker build, it does not work for remote cloud build using gcloud as reported by @zirho. I could just move the user statement to after the install command but I want to understand why it works for local build but not remote build...

Unknown dialect undefined

I'm not able to npm develop the strapi.

I got an error : Unknown dialect undefined

const path = require("path");

module.exports = ({ env }) => ({
  defaultConnection: "default",
  connections: {
    default: {
      connector: "firestore",
      settings: {
        projectId: "foo-3ef77",
      },
      options: {
        // Connect to a local running Firestore emulator
        // when running in development mode
        useEmulator: env("NODE_ENV") == "development",
      },
    },
  },
});

Connecting to remote server is extremely slow

While developing with Firebase Emulators, everything is going smoothly. Currently my project development is almost done and I'm working on the deployment, The loading is extremely slow. It took 1.5min to load after signed in. I've tried with my local Strapi with remote firebase and the result is also slow.

Strapi: 3.6.5
strapi-connector-firestore: 3.0.0-alpha.46

Allow components to be stored in sub-collections

Currently, components (repeatable) and dynamic zone are stored directly in the document. This does not provide good support for running queries on fields within those components.

  • Add an option flattenComponents (global setting and per-model override) wich defaults to true current behaviour.
  • On the per-model option, allow to specify a test for selecting which components are flattened.

When component flattening is disabled, store the components as individual documents within a sub-collection of the master document.

Feature: Populate designated fields when storing references

Relations are stored as references in Firestore database. In some situations (when accessing the database natively, not via the Strapi API) it may be convenient to have certain values from the target document stored alongside the entry.

In such a way, key data from the relation would be immediately available without need to fetch the target document.

Bootstrap function in admin failed

I run export GOOGLE_APPLICATION_CREDENTIALS='./config/Timetracking inner test-97f38b6dda77.json' && strapi develop

and I got:

warn Your application doesn't have a super admin user. Bootstrap function in admin failed error Error: 3 INVALID_ARGUMENT: inequality filter property and first sort order must be the same: roles and __key__ at Object.callErrorFromStatus (/path/node_modules/@grpc/grpc-js/build/src/call.js:31:26)

"dependencies": { "knex": "<0.20.0", "sqlite3": "^5.0.0", "strapi": "3.2.5", "strapi-admin": "3.2.5", "strapi-connector-bookshelf": "3.2.5", "strapi-connector-firestore": "^3.0.0-alpha.21", "strapi-plugin-content-manager": "3.2.5", "strapi-plugin-content-type-builder": "3.2.5", "strapi-plugin-email": "3.2.5", "strapi-plugin-upload": "3.2.5", "strapi-plugin-users-permissions": "3.2.5", "strapi-utils": "3.2.5"

Single-field search for native-only models

For models that do not allow non-native queries (and search), allow a field to be elected for native search.

A search on such a model will fun a query for == on the given field, a primitive kind of search using native native Firestore queries only.

Collection Types - Filters (500 internal error)

When performing a filter under "Collection Types" i get an internal error. Under headers i can see the right filter being used. The fields is of type Int:

Filter

image

Header

calendar-date?page=1&pageSize=10&_sort=recId:ASC&_where[0][calendarDay]=1

NOTE: querystring query works fine see examle below:

api/calendar-date?calendarDay=${calendarDay}

HTTP Response

image

{"statusCode":500,"error":"Internal Server Error","message":"An internal server error occurred"}

Header

image

UI Error message

image

Lazily create flat collection documents

Currently, a write operation is performed immediately on startup to ensure the existence of the document containing flattened collections. Because a read operation is cheaper than a write operation, it is better to

  • Read first to avoid a write operation on every subsequent startup
  • Perhaps do this lazily when an entity is first created, rather than on startup

Error "Collection File ... is missing from model" when using repeatable component

Error:

When using component that have media field always throw error that File model is not found

[2021-07-12T06:43:09.283Z] error Something went wrong in the model `Application::benefit.benefit` with the attribute `benefitItems$meta.icon`
[2021-07-12T06:43:09.285Z] error Error: The collection `File`, used in the attribute `benefitItems$meta.icon` in the model Application::benefit.benefit, is missing from the models
    at Object.getNature (/Users/skyshi/code/node/iris-cms/node_modules/strapi-utils/lib/models.js:79:15)
    at Object.defineAssociations (/Users/skyshi/code/node/iris-cms/node_modules/strapi-utils/lib/models.js:373:26)
    at mountModel (/Users/skyshi/code/node/iris-cms/node_modules/strapi-connector-firestore/lib/model.js:155:22)
    at Object.mountModels (/Users/skyshi/code/node/iris-cms/node_modules/strapi-connector-firestore/lib/model.js:54:21)
    at /Users/skyshi/code/node/iris-cms/node_modules/strapi-connector-firestore/lib/index.js:88:21
    at async Promise.all (index 0)
    at async Object.initialize (/Users/skyshi/code/node/iris-cms/node_modules/strapi-connector-firestore/lib/index.js:55:9)
    at async Object.initialize (/Users/skyshi/code/node/iris-cms/node_modules/strapi-database/lib/connector-registry.js:30:9)
    at async DatabaseManager.initialize (/Users/skyshi/code/node/iris-cms/node_modules/strapi-database/lib/database-manager.js:43:5)
    at async Strapi.load (/Users/skyshi/code/node/iris-cms/node_modules/strapi/lib/Strapi.js:354:5)

dependencies in package.json

    {
    "lodash": "^4.17.21",
    "strapi": "3.6.5",
    "strapi-admin": "3.6.5",
    "strapi-connector-firestore": "^3.0.0-alpha.35",
    "strapi-plugin-content-manager": "3.6.5",
    "strapi-plugin-content-type-builder": "3.6.5",
    "strapi-plugin-email": "3.6.5",
    "strapi-plugin-upload": "3.6.5",
    "strapi-plugin-users-permissions": "3.6.5",
    "strapi-provider-upload-google-cloud-storage": "^3.6.3",
    "strapi-utils": "3.6.5"
  }

How to reproduce

Create content type that have component inside it, and the component have media field.

Proposal: "documentisation"

Considering that Firestore charges based on read and write operations, there may be some opportunity to reduce usage costs and increase performance.

Proposal:

  • Add a documentise option for each model (or an documentiseAllExcept option)
  • Documentisation combines all documents in a collection/table into an array of objects in a single document

This proposal would be effective for models that are:

  • Bounded (i.e. finite number of documents, like the collections for Strapi configuration and permissions/roles, but not collections like users which may be unbounded) so that they fit within the document size limit
  • Queried often, or queried in their entirety (reduce many document read operations into a single read operation)

Prepare for stable release

I bootstrapped this codebase from the official strapi-connector-mongoose package at version 3.0.0, because I figured that, because Mongoose is also a NoSQL database, that package would be the best starting point.

https://github.com/strapi/strapi/tree/v3.0.0/packages/strapi-connector-mongoose

Simple operations and relations seem to be working, but most features (particularly components and more complicated relations) have either been only lightly tested, or not tested at all.

To do: see project.

Contributions and pull requests welcome, because I have limited capacity to complete and maintain this package.

See also strapi/strapi#530 and strapi/strapi#5529.

media library not displaying uploaded files

Hi Guys,

I'm using the following media library - https://www.npmjs.com/package/strapi-provider-upload-google-cloud-storage

Uploading files works and are also available to the public via the bucket URL. But under media library, no images display, however, the page recognizes that there is content available by displaying the page count underneath.

I am running cloudrun and under Cloud firestore, i can see a populated table "upload_file".

image

Any help is much appreciated.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.