firestore-meilisearch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.editorconfig +20 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +18 -0
  4. package/.github/ISSUE_TEMPLATE/other.md +7 -0
  5. package/.github/dependatbot.yml +23 -0
  6. package/.github/release-draft-template.yml +33 -0
  7. package/.github/scripts/check-release.sh +42 -0
  8. package/.github/workflows/publish.yml +30 -0
  9. package/.github/workflows/release-drafter.yml +16 -0
  10. package/.github/workflows/test.yml +42 -0
  11. package/CHANGELOG.md +3 -0
  12. package/CONTRIBUTING.md +236 -0
  13. package/LICENSE +201 -0
  14. package/POSTINSTALL.md +40 -0
  15. package/PREINSTALL.md +42 -0
  16. package/README.md +128 -0
  17. package/bors.toml +8 -0
  18. package/dataset/firebase-export-metadata.json +8 -0
  19. package/dataset/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata +0 -0
  20. package/dataset/firestore_export/all_namespaces/all_kinds/output-0 +0 -0
  21. package/dataset/firestore_export/firestore_export.overall_export_metadata +0 -0
  22. package/extension.yaml +176 -0
  23. package/firebase.json +20 -0
  24. package/functions/.eslintignore +2 -0
  25. package/functions/.eslintrc.js +54 -0
  26. package/functions/__tests__/__mocks__/console.ts +7 -0
  27. package/functions/__tests__/adapter.test.ts +98 -0
  28. package/functions/__tests__/config.test.ts +130 -0
  29. package/functions/__tests__/data/document.ts +11 -0
  30. package/functions/__tests__/data/environment.ts +9 -0
  31. package/functions/__tests__/functions.test.ts +280 -0
  32. package/functions/__tests__/jest.setup.ts +1 -0
  33. package/functions/__tests__/test.types.d.ts +5 -0
  34. package/functions/__tests__/tsconfig.json +5 -0
  35. package/functions/__tests__/util.test.ts +200 -0
  36. package/functions/jest.config.js +12 -0
  37. package/functions/lib/adapter.js +61 -0
  38. package/functions/lib/config.js +13 -0
  39. package/functions/lib/import/config.js +127 -0
  40. package/functions/lib/import/index.js +93 -0
  41. package/functions/lib/index.js +90 -0
  42. package/functions/lib/logs.js +97 -0
  43. package/functions/lib/meilisearch/create-index.js +17 -0
  44. package/functions/lib/meilisearch-index.js +17 -0
  45. package/functions/lib/types.js +2 -0
  46. package/functions/lib/util.js +47 -0
  47. package/functions/lib/version.js +4 -0
  48. package/functions/package.json +53 -0
  49. package/functions/src/adapter.ts +106 -0
  50. package/functions/src/config.ts +34 -0
  51. package/functions/src/import/config.ts +207 -0
  52. package/functions/src/import/index.ts +115 -0
  53. package/functions/src/index.ts +103 -0
  54. package/functions/src/logs.ts +107 -0
  55. package/functions/src/meilisearch/create-index.ts +20 -0
  56. package/functions/src/types.ts +8 -0
  57. package/functions/src/util.ts +63 -0
  58. package/functions/src/version.ts +1 -0
  59. package/functions/tsconfig.eslint.json +13 -0
  60. package/functions/tsconfig.json +23 -0
  61. package/functions/yarn.lock +5306 -0
  62. package/guides/IMPORT_EXISTING_DOCUMENTS.md +74 -0
  63. package/package.json +21 -0
  64. package/script/version.sh +51 -0
  65. package/test-params-example.env +9 -0
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initMeilisearchIndex = void 0;
4
+ const meilisearch_1 = require("meilisearch");
5
+ /**
6
+ * initMeilisearchIndex
7
+ * @param {MeilisearchConfig} - Meilisearch configuration
8
+ * @return {Index}
9
+ */
10
+ function initMeilisearchIndex({ host, apiKey, indexUid, }) {
11
+ const client = new meilisearch_1.MeiliSearch({
12
+ host,
13
+ apiKey,
14
+ });
15
+ return client.index(indexUid);
16
+ }
17
+ exports.initMeilisearchIndex = initMeilisearchIndex;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMeiliSearchIndex = void 0;
4
+ const meilisearch_1 = require("meilisearch");
5
+ /**
6
+ * createMeiliSearchIndex
7
+ * @param {MeiliSearchConfig}
8
+ * @return {Index}
9
+ */
10
+ function createMeiliSearchIndex({ host, apiKey, indexUid, }) {
11
+ const client = new meilisearch_1.MeiliSearch({
12
+ host,
13
+ apiKey,
14
+ });
15
+ return client.index(indexUid);
16
+ }
17
+ exports.createMeiliSearchIndex = createMeiliSearchIndex;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getFieldsToIndex = exports.getChangedDocumentId = exports.getChangeType = exports.ChangeType = void 0;
4
+ const config_1 = require("./config");
5
+ var ChangeType;
6
+ (function (ChangeType) {
7
+ ChangeType[ChangeType["CREATE"] = 0] = "CREATE";
8
+ ChangeType[ChangeType["DELETE"] = 1] = "DELETE";
9
+ ChangeType[ChangeType["UPDATE"] = 2] = "UPDATE";
10
+ })(ChangeType = exports.ChangeType || (exports.ChangeType = {}));
11
+ /**
12
+ * Get type of the modification perform on a document.
13
+ * @param {Change<T>} change The Functions interface for events that change state.
14
+ * @return {ChangeType} Final type of the event.
15
+ */
16
+ function getChangeType(change) {
17
+ if (!change.after.exists) {
18
+ return ChangeType.DELETE;
19
+ }
20
+ if (!change.before.exists) {
21
+ return ChangeType.CREATE;
22
+ }
23
+ return ChangeType.UPDATE;
24
+ }
25
+ exports.getChangeType = getChangeType;
26
+ /**
27
+ * Get final id of a document after modification.
28
+ * @param {Change<T>} change The Functions interface for events that change state.
29
+ * @return {string} Final state type of the event.
30
+ */
31
+ function getChangedDocumentId(change) {
32
+ if (change.after.exists) {
33
+ return change.after.id;
34
+ }
35
+ return change.before.id;
36
+ }
37
+ exports.getChangedDocumentId = getChangedDocumentId;
38
+ /**
39
+ * Returns the MEILISEARCH_FIELDS_TO_INDEX value from the config file and formats it.
40
+ * @return {string[]} An array of fields.
41
+ */
42
+ function getFieldsToIndex() {
43
+ return config_1.config.meilisearch.fieldsToIndex
44
+ ? config_1.config.meilisearch.fieldsToIndex.split(/[ ,]+/)
45
+ : [];
46
+ }
47
+ exports.getFieldsToIndex = getFieldsToIndex;
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.version = void 0;
4
+ exports.version = '0.1.0';
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "firestore-meilisearch",
3
+ "version": "0.1.0",
4
+ "scripts": {
5
+ "lint": "eslint .",
6
+ "lint:fix": "eslint . --fix",
7
+ "build": "tsc",
8
+ "watch": "tsc --watch",
9
+ "playground": "nodemon ./lib/import --project 521120192778 --source-collection-path movies --index movies --batch-size 300 --non-interactive -H 'http://localhost:7700' -a masterKey",
10
+ "serve": "yarn build && firebase emulators:start --only functions",
11
+ "shell": "yarn build && firebase functions:shell",
12
+ "start": "yarn shell",
13
+ "deploy": "firebase deploy --only functions",
14
+ "logs": "firebase functions:log",
15
+ "test": "jest",
16
+ "test:watch": "jest --watch",
17
+ "test:coverage": "jest --coverage"
18
+ },
19
+ "engines": {
20
+ "node": ">=14.0.0"
21
+ },
22
+ "main": "lib/index.js",
23
+ "dependencies": {
24
+ "firebase-admin": "^10.0.2",
25
+ "firebase-functions": "^3.16.0",
26
+ "meilisearch": "^0.25.0"
27
+ },
28
+ "devDependencies": {
29
+ "@babel/preset-typescript": "^7.15.0",
30
+ "@types/jest": "^27.0.2",
31
+ "@typescript-eslint/eslint-plugin": "^4.32.0",
32
+ "@typescript-eslint/parser": "^4.32.0",
33
+ "commander": "^8.3.0",
34
+ "eslint": "^7.6.0",
35
+ "eslint-config-google": "^0.14.0",
36
+ "eslint-config-prettier": "^8.3.0",
37
+ "eslint-plugin-import": "^2.24.2",
38
+ "eslint-plugin-jest": "^24.4.2",
39
+ "eslint-plugin-prettier": "^4.0.0",
40
+ "firebase-functions-test": "^0.3.2",
41
+ "inquirer": "^8.2.0",
42
+ "jest": "^27.2.2",
43
+ "jest-mock": "^27.1.1",
44
+ "js-yaml": "^4.1.0",
45
+ "mocked-env": "^1.3.5",
46
+ "prettier": "^2.4.1",
47
+ "ts-jest": "^27.0.5",
48
+ "ts-node": "^10.2.1",
49
+ "typescript": "^4.4.3"
50
+ },
51
+ "private": true,
52
+ "bin": "lib/import/index.js"
53
+ }
@@ -0,0 +1,106 @@
1
+ 'use strict'
2
+ /*
3
+ * Copyright 2022 Meilisearch
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import { DocumentSnapshot } from 'firebase-functions/lib/providers/firestore'
19
+ import {
20
+ DocumentReference,
21
+ GeoPoint,
22
+ Timestamp,
23
+ } from 'firebase-admin/lib/firestore'
24
+ import { getFieldsToIndex } from './util'
25
+ import * as logs from './logs'
26
+
27
+ type MeilisearchGeoPoint = {
28
+ lat: number
29
+ lng: number
30
+ }
31
+
32
+ type FirestoreRow =
33
+ | null
34
+ | boolean
35
+ | number
36
+ | string
37
+ | DocumentReference
38
+ | GeoPoint
39
+ | Timestamp
40
+ | Array<any>
41
+ | Map<any, any>
42
+ | MeilisearchGeoPoint
43
+
44
+ /**
45
+ * Adapts documents from the Firestore database to Meilisearch compatible documents.
46
+ * @param {string} documentId Document id.
47
+ * @param {DocumentSnapshot} snapshot Snapshot of the data contained in the document read from your Firestore database.
48
+ * @return {Record<string, any>} A properly formatted document to be added or updated in Meilisearch.
49
+ */
50
+ export function adaptDocument(
51
+ documentId: string,
52
+ snapshot: DocumentSnapshot
53
+ ): Record<string, any> {
54
+ const fields = getFieldsToIndex()
55
+ const data = snapshot.data() || {}
56
+ if ('_firestore_id' in data) {
57
+ delete data.id
58
+ }
59
+ if (fields.length === 0) {
60
+ return { _firestore_id: documentId, ...data }
61
+ }
62
+ const document = Object.keys(data).reduce(
63
+ (acc, key) => {
64
+ if (fields.includes(key)) {
65
+ const [field, value] = adaptValues(key, data[key])
66
+ return { ...acc, [field]: value }
67
+ }
68
+ return acc
69
+ },
70
+ { _firestore_id: documentId }
71
+ )
72
+ return document
73
+ }
74
+
75
+ /**
76
+ * Checks and adapts each values to be compatible with Meilisearch documents.
77
+ * @param {string} field
78
+ * @param {FirestoreRow} value
79
+ * @return {[string,FirestoreRow]} A properly formatted array of field and value.
80
+ */
81
+ export function adaptValues(
82
+ field: string,
83
+ value: FirestoreRow
84
+ ): [string, FirestoreRow | MeilisearchGeoPoint] {
85
+ if (value instanceof GeoPoint) {
86
+ if (field === '_geo') {
87
+ logs.infoGeoPoint(true)
88
+ return [field, adaptGeoPoint(value)]
89
+ } else {
90
+ logs.infoGeoPoint(false)
91
+ }
92
+ }
93
+ return [field, value]
94
+ }
95
+
96
+ /**
97
+ * Adapts GeoPoint Firestore instance to fit with Meilisearch geo point.
98
+ * @param {GeoPoint} geoPoint GeoPoint Firestore object.
99
+ * @return {MeilisearchGeoPoint} A properly formatted geo point for Meilisearch.
100
+ */
101
+ const adaptGeoPoint = (geoPoint: GeoPoint): MeilisearchGeoPoint => {
102
+ return {
103
+ lat: geoPoint.latitude,
104
+ lng: geoPoint.longitude,
105
+ }
106
+ }
@@ -0,0 +1,34 @@
1
+ 'use strict'
2
+ /*
3
+ * Copyright 2022 Meilisearch
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { MeilisearchConfig } from './types'
18
+
19
+ type PluginConfiguration = {
20
+ location: string
21
+ collectionPath: string
22
+ meilisearch: MeilisearchConfig
23
+ }
24
+
25
+ export const config: PluginConfiguration = {
26
+ location: process.env.LOCATION || 'europe-west1',
27
+ collectionPath: process.env.COLLECTION_PATH || '',
28
+ meilisearch: {
29
+ host: process.env.MEILISEARCH_HOST || '',
30
+ apiKey: process.env.MEILISEARCH_API_KEY || '',
31
+ indexUid: process.env.MEILISEARCH_INDEX_NAME || '',
32
+ fieldsToIndex: process.env.MEILISEARCH_FIELDS_TO_INDEX || '',
33
+ },
34
+ }
@@ -0,0 +1,207 @@
1
+ import { Command } from 'commander'
2
+ import * as inquirer from 'inquirer'
3
+ import { MeilisearchConfig } from '../types'
4
+
5
+ const FIRESTORE_VALID_CHARACTERS = /^[^/]+$/
6
+ const FIRESTORE_COLLECTION_NAME_MAX_CHARS = 6144
7
+ const PROJECT_ID_MAX_CHARS = 30
8
+ const MEILISEARCH_VALID_CHARACTERS = /^[a-zA-Z-_0-9,]*$/
9
+ const MEILISEARCH_UID_MAX_CHARS = 6144
10
+
11
+ // Create a new interface to manage the arguments from the non-interactive command line.
12
+ const program = new Command()
13
+
14
+ program
15
+ .name('firestore-meilisearch')
16
+ .option(
17
+ '--non-interactive',
18
+ 'Parse all input from command line flags instead of prompting the caller.',
19
+ false
20
+ )
21
+ .option(
22
+ '-P, --project <project>',
23
+ 'Firebase Project ID for project containing the Cloud Firestore database.'
24
+ )
25
+ .option(
26
+ '-s, --source-collection-path <source-collection-path>',
27
+ 'The path of the Cloud Firestore Collection to import from. (This may, or may not, be the same Collection for which you plan to mirror changes.)'
28
+ )
29
+ .option(
30
+ '-q, --query-collection-group [true|false]',
31
+ "Use 'true' for a collection group query, otherwise a collection query is performed."
32
+ )
33
+ .option(
34
+ '-i, --index <index>',
35
+ "The Uid of the index in Meilisearch to import to. (An index will be created if it doesn't already exist.)"
36
+ )
37
+ .option(
38
+ '-b, --batch-size [batch-size]',
39
+ 'Number of documents to stream into Meilisearch at once.',
40
+ value => parseInt(value, 10),
41
+ 1000
42
+ )
43
+ .option(
44
+ '-H, --host <host>',
45
+ 'The Host of your Meilisearch database. Example: http://localhost:7700.'
46
+ )
47
+ .option(
48
+ '-a, --api-key <api-key>',
49
+ 'The Meilisearch API key with permission to perform actions on indexes. Both the private key and the master key are valid choices but we strongly recommend using the private key for security purposes.'
50
+ )
51
+
52
+ const validateInput = (
53
+ value: string,
54
+ name: string,
55
+ regex: RegExp,
56
+ sizeLimit: number
57
+ ) => {
58
+ if (!value || typeof value !== 'string' || value.trim() === '') {
59
+ return `Please supply a ${name}`
60
+ }
61
+ if (value.trim().length > sizeLimit) {
62
+ return `${name} must be at most ${sizeLimit} characters long`
63
+ }
64
+ if (!value.match(regex)) {
65
+ return `The ${name} does not match the regular expression provided`
66
+ }
67
+ return true
68
+ }
69
+
70
+ const validateBatchSize = (value: string) => {
71
+ if (/^\d+$/.test(value) == false) {
72
+ return `The batchsize ${value} should be a number`
73
+ }
74
+ return parseInt(value, 10) > 0
75
+ }
76
+
77
+ // Questions for the interactive user interface in command line.
78
+ const questions = [
79
+ {
80
+ message: 'What is your Firebase project ID?',
81
+ name: 'project',
82
+ type: 'input',
83
+ validate: value =>
84
+ validateInput(
85
+ value,
86
+ 'project ID',
87
+ FIRESTORE_VALID_CHARACTERS,
88
+ PROJECT_ID_MAX_CHARS
89
+ ),
90
+ },
91
+ {
92
+ message:
93
+ 'What is the path of the Cloud Firestore Collection you would like to import from? ' +
94
+ '(This may, or may not, be the same Collection for which you plan to mirror changes.)',
95
+ name: 'sourceCollectionPath',
96
+ type: 'input',
97
+ validate: value =>
98
+ validateInput(
99
+ value,
100
+ 'collection path',
101
+ FIRESTORE_VALID_CHARACTERS,
102
+ FIRESTORE_COLLECTION_NAME_MAX_CHARS
103
+ ),
104
+ },
105
+ {
106
+ message: 'Would you like to import documents via a Collection Group query?',
107
+ name: 'queryCollectionGroup',
108
+ type: 'confirm',
109
+ default: false,
110
+ },
111
+ {
112
+ message:
113
+ "What is the Uid of the Meilisearch index that you would like to use? (A index will be created if it doesn't already exist)",
114
+ name: 'index',
115
+ type: 'input',
116
+ validate: value =>
117
+ validateInput(
118
+ value,
119
+ 'index',
120
+ MEILISEARCH_VALID_CHARACTERS,
121
+ MEILISEARCH_UID_MAX_CHARS
122
+ ),
123
+ },
124
+ {
125
+ message:
126
+ 'How many documents should the import stream into Meilisearch at once?',
127
+ name: 'batchSize',
128
+ type: 'input',
129
+ default: 1000,
130
+ validate: validateBatchSize,
131
+ },
132
+ {
133
+ message:
134
+ 'What is the host of the Meilisearch database that you would like to use? Example: http://localhost:7700.',
135
+ name: 'host',
136
+ type: 'input',
137
+ },
138
+ {
139
+ message:
140
+ 'Which Meilisearch API key with permission to perform actions on indexes do you like to use? Both the private key and the master key are valid choices but we strongly recommend using the private key for security purposes.',
141
+ name: 'apiKey',
142
+ type: 'input',
143
+ },
144
+ ]
145
+
146
+ export interface CLIConfig {
147
+ projectId: string
148
+ sourceCollectionPath: string
149
+ queryCollectionGroup: boolean
150
+ batchSize: string
151
+ meilisearch: MeilisearchConfig
152
+ }
153
+
154
+ /**
155
+ * Parse the argument from the interactive or non-interactive command line.
156
+ */
157
+ export async function parseConfig(): Promise<CLIConfig> {
158
+ program.parse(process.argv)
159
+
160
+ const options = program.opts()
161
+ if (options.nonInteractive) {
162
+ if (
163
+ options.project === undefined ||
164
+ options.sourceCollectionPath === undefined ||
165
+ options.index === undefined ||
166
+ options.host === undefined ||
167
+ options.apiKey === undefined ||
168
+ !validateBatchSize(options.batchSize)
169
+ ) {
170
+ program.outputHelp()
171
+ process.exit(1)
172
+ }
173
+
174
+ return {
175
+ projectId: options.project,
176
+ sourceCollectionPath: options.sourceCollectionPath,
177
+ queryCollectionGroup: options.queryCollectionGroup === 'true',
178
+ batchSize: options.batchSize,
179
+ meilisearch: {
180
+ indexUid: options.index,
181
+ host: options.host,
182
+ apiKey: options.apiKey,
183
+ },
184
+ }
185
+ }
186
+ const {
187
+ project,
188
+ sourceCollectionPath,
189
+ queryCollectionGroup,
190
+ index,
191
+ batchSize,
192
+ host,
193
+ apiKey,
194
+ } = await inquirer.prompt(questions)
195
+
196
+ return {
197
+ projectId: project,
198
+ sourceCollectionPath: sourceCollectionPath,
199
+ queryCollectionGroup: queryCollectionGroup,
200
+ batchSize: batchSize,
201
+ meilisearch: {
202
+ indexUid: index,
203
+ host: host,
204
+ apiKey: apiKey,
205
+ },
206
+ }
207
+ }
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+
3
+ /*
4
+ * Copyright 2022 Meilisearch
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * https://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import * as admin from 'firebase-admin'
20
+ import { DocumentSnapshot } from 'firebase-functions/lib/providers/firestore'
21
+ import { CLIConfig, parseConfig } from './config'
22
+ import * as logs from '../logs'
23
+ import { adaptDocument } from '../adapter'
24
+ import { initMeilisearchIndex } from '../meilisearch/create-index'
25
+ import { Index } from '../types'
26
+
27
+ const run = async () => {
28
+ // Retrieve all arguments from the commande line.
29
+ const config: CLIConfig = await parseConfig()
30
+
31
+ // Initialize Firebase using the Google Credentials in the GOOGLE_APPLICATION_CREDENTIALS environment variable.
32
+ admin.initializeApp({
33
+ credential: admin.credential.applicationDefault(),
34
+ databaseURL: `https://${config.projectId}.firebaseio.com`,
35
+ })
36
+
37
+ const database = admin.firestore()
38
+
39
+ // Initialize Meilisearch index.
40
+ const index = initMeilisearchIndex(config.meilisearch)
41
+
42
+ await retrieveCollectionFromFirestore(database, config, index)
43
+ }
44
+
45
+ /**
46
+ * Retrieves a collection or collection group in Firestore and aggregates the data.
47
+ * @param {FirebaseFirestore.Firestore} database
48
+ * @param {CLIConfig} config
49
+ * @param {Index} index
50
+ */
51
+ async function retrieveCollectionFromFirestore(
52
+ database: FirebaseFirestore.Firestore,
53
+ config: CLIConfig,
54
+ index: Index
55
+ ): Promise<number> {
56
+ const batch: number = parseInt(config.batchSize)
57
+
58
+ let query
59
+ let total = 0
60
+ let batches = 0
61
+ let lastDocument = null
62
+ let lastBatchSize: number = batch
63
+
64
+ while (lastBatchSize === batch) {
65
+ batches++
66
+
67
+ if (config.queryCollectionGroup) {
68
+ query = database.collectionGroup(config.sourceCollectionPath).limit(batch)
69
+ } else {
70
+ query = database.collection(config.sourceCollectionPath).limit(batch)
71
+ }
72
+
73
+ if (lastDocument !== null) {
74
+ query = query.startAfter(lastDocument)
75
+ }
76
+
77
+ const snapshot = await query.get()
78
+ const docs = snapshot.docs
79
+
80
+ if (docs.length === 0) break
81
+ total += await sendDocumentsToMeilisearch(docs, index)
82
+
83
+ if (docs.length) {
84
+ lastDocument = docs[docs.length - 1]
85
+ }
86
+ lastBatchSize = docs.length
87
+ }
88
+
89
+ logs.importData(total, batches)
90
+ return total
91
+ }
92
+
93
+ /**
94
+ * Adapts documents and indexes them in Meilisearch.
95
+ * @param {any} docs
96
+ * @param {Index} index
97
+ * @param {Change<DocumentSnapshot>} change
98
+ */
99
+ async function sendDocumentsToMeilisearch(
100
+ docs: DocumentSnapshot[],
101
+ index: Index
102
+ ): Promise<number> {
103
+ const document = docs.map(snapshot => {
104
+ return adaptDocument(snapshot.id, snapshot)
105
+ })
106
+ try {
107
+ await index.addDocuments(document, { primaryKey: '_firestore_id' })
108
+ } catch (e) {
109
+ logs.error(e as Error)
110
+ return 0
111
+ }
112
+ return document.length
113
+ }
114
+
115
+ void run()