@stoker-platform/cli 0.5.12

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 (94) hide show
  1. package/LICENSE.md +102 -0
  2. package/init-files/.##gitignore## +102 -0
  3. package/init-files/.devcontainer/devcontainer.json +14 -0
  4. package/init-files/.env/.env +70 -0
  5. package/init-files/.eslintrc.cjs +35 -0
  6. package/init-files/.firebaserc +6 -0
  7. package/init-files/.prettierignore +86 -0
  8. package/init-files/.prettierrc +5 -0
  9. package/init-files/bin/build.js +221 -0
  10. package/init-files/bin/shim.js +159 -0
  11. package/init-files/extensions/firestore-send-email.env +7 -0
  12. package/init-files/external.package.json +4 -0
  13. package/init-files/firebase-rules/database.rules.json +9 -0
  14. package/init-files/firebase-rules/firestore.custom.rules +19 -0
  15. package/init-files/firebase-rules/firestore.indexes.json +0 -0
  16. package/init-files/firebase-rules/firestore.rules +0 -0
  17. package/init-files/firebase-rules/storage.rules +0 -0
  18. package/init-files/firebase.hosting.json +122 -0
  19. package/init-files/firebase.json +52 -0
  20. package/init-files/functions/.##gitignore## +14 -0
  21. package/init-files/functions/.eslintrc.cjs +28 -0
  22. package/init-files/functions/package.json +46 -0
  23. package/init-files/functions/prompts/chat.prompt +17 -0
  24. package/init-files/functions/src/index.ts +457 -0
  25. package/init-files/functions/tsconfig.dev.json +3 -0
  26. package/init-files/functions/tsconfig.json +17 -0
  27. package/init-files/icons/logo-large.png +0 -0
  28. package/init-files/icons/logo-small.png +0 -0
  29. package/init-files/ops.js +25 -0
  30. package/init-files/package.json +53 -0
  31. package/init-files/project-data.json +5 -0
  32. package/init-files/remoteconfig.template.json +1 -0
  33. package/init-files/src/collections/Inbox.ts +444 -0
  34. package/init-files/src/collections/Outbox.ts +270 -0
  35. package/init-files/src/collections/Settings.ts +44 -0
  36. package/init-files/src/collections/Users.ts +138 -0
  37. package/init-files/src/main.ts +245 -0
  38. package/init-files/src/utils.ts +3 -0
  39. package/init-files/src/vite-env.d.ts +1 -0
  40. package/init-files/test/test.ts +5 -0
  41. package/init-files/tsconfig.json +23 -0
  42. package/init-files/vitest.config.ts +9 -0
  43. package/lib/package.json +45 -0
  44. package/lib/src/data/exportToBigQuery.js +41 -0
  45. package/lib/src/data/seedData.js +347 -0
  46. package/lib/src/deploy/applySchema.js +43 -0
  47. package/lib/src/deploy/cloud-functions/getFunctionsData.js +18 -0
  48. package/lib/src/deploy/deployProject.js +116 -0
  49. package/lib/src/deploy/firestore-export/exportFirestoreData.js +29 -0
  50. package/lib/src/deploy/firestore-ttl/deployTTLs.js +127 -0
  51. package/lib/src/deploy/live-update/liveUpdate.js +22 -0
  52. package/lib/src/deploy/maintenance/activateMaintenanceMode.js +9 -0
  53. package/lib/src/deploy/maintenance/disableMaintenanceMode.js +9 -0
  54. package/lib/src/deploy/maintenance/setDeploymentStatus.js +22 -0
  55. package/lib/src/deploy/rules-indexes/generateFirestoreIndexes.js +23 -0
  56. package/lib/src/deploy/rules-indexes/generateFirestoreRules.js +35 -0
  57. package/lib/src/deploy/rules-indexes/generateStorageRules.js +23 -0
  58. package/lib/src/deploy/schema/generateSchema.js +184 -0
  59. package/lib/src/deploy/schema/persistSchema.js +14 -0
  60. package/lib/src/lint/lintSchema.js +1491 -0
  61. package/lib/src/lint/securityReport.js +223 -0
  62. package/lib/src/main.js +460 -0
  63. package/lib/src/migration/firestore/migrateFirestore.js +8 -0
  64. package/lib/src/migration/firestore/operations/deleteField.js +58 -0
  65. package/lib/src/migration/migrateAll.js +30 -0
  66. package/lib/src/ops/auditDenormalized.js +124 -0
  67. package/lib/src/ops/auditPermissions.js +92 -0
  68. package/lib/src/ops/auditRelations.js +186 -0
  69. package/lib/src/ops/explainPreloadQueries.js +65 -0
  70. package/lib/src/ops/getUser.js +10 -0
  71. package/lib/src/ops/getUserPermissions.js +19 -0
  72. package/lib/src/ops/getUserRecord.js +20 -0
  73. package/lib/src/ops/listProjects.js +8 -0
  74. package/lib/src/ops/setUserCollection.js +14 -0
  75. package/lib/src/ops/setUserDocument.js +11 -0
  76. package/lib/src/ops/setUserRole.js +14 -0
  77. package/lib/src/project/addProject.js +935 -0
  78. package/lib/src/project/addRecord.js +9 -0
  79. package/lib/src/project/addRecordPrompt.js +205 -0
  80. package/lib/src/project/addTenant.js +59 -0
  81. package/lib/src/project/buildWebApp.js +10 -0
  82. package/lib/src/project/customDomain.js +157 -0
  83. package/lib/src/project/deleteProject.js +51 -0
  84. package/lib/src/project/deleteRecord.js +11 -0
  85. package/lib/src/project/deleteTenant.js +49 -0
  86. package/lib/src/project/getOne.js +25 -0
  87. package/lib/src/project/getSome.js +28 -0
  88. package/lib/src/project/initProject.js +16 -0
  89. package/lib/src/project/prepareEmulatorData.js +125 -0
  90. package/lib/src/project/setProject.js +13 -0
  91. package/lib/src/project/startEmulators.js +30 -0
  92. package/lib/src/project/updateRecord.js +9 -0
  93. package/lib/tsconfig.tsbuildinfo +1 -0
  94. package/package.json +45 -0
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env -S node --no-warnings
2
+ import { existsSync } from "fs";
3
+ import { Command } from "commander";
4
+ import pkg from "../package.json" with { type: "json" };
5
+ import { join } from "path";
6
+ import dotenv from "dotenv";
7
+ if (!existsSync("firebase.json") && process.argv[2] && process.argv[2] !== "init") {
8
+ throw new Error("Please run this command from the root of your Stoker project");
9
+ }
10
+ if (!process.env.GCP_PROJECT &&
11
+ process.argv[2] &&
12
+ ![
13
+ "init",
14
+ "set-project",
15
+ "build-web-app",
16
+ "lint-schema",
17
+ "security-report",
18
+ "generate-firestore-indexes",
19
+ "generate-firestore-rules",
20
+ "generate-storage-rules",
21
+ "add-project",
22
+ "list-projects",
23
+ ].includes(process.argv[2])) {
24
+ throw new Error("Please set the GCP_PROJECT environment variable");
25
+ }
26
+ else {
27
+ dotenv.config({ path: join(process.cwd(), ".env", `.env.${process.env.GCP_PROJECT}`), quiet: true });
28
+ }
29
+ if (process.env.GCP_PROJECT) {
30
+ const projectEnvFile = join(process.cwd(), ".env", `.env.project.${process.env.GCP_PROJECT}`);
31
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
32
+ if (existsSync(projectEnvFile)) {
33
+ dotenv.config({ path: projectEnvFile, quiet: true });
34
+ }
35
+ else {
36
+ dotenv.config({ path: join(process.cwd(), ".env", ".env"), quiet: true });
37
+ }
38
+ }
39
+ else {
40
+ dotenv.config({ path: join(process.cwd(), ".env", ".env"), quiet: true });
41
+ }
42
+ import { initProject } from "./project/initProject.js";
43
+ import { buildWebApp } from "./project/buildWebApp.js";
44
+ import { startEmulators, startWebAppEmulators } from "./project/startEmulators.js";
45
+ import { activateMaintenanceMode } from "./deploy/maintenance/activateMaintenanceMode.js";
46
+ import { disableMaintenanceMode } from "./deploy/maintenance/disableMaintenanceMode.js";
47
+ import { setDeploymentStatus } from "./deploy/maintenance/setDeploymentStatus.js";
48
+ import { persistSchema } from "./deploy/schema/persistSchema.js";
49
+ import { liveUpdate } from "./deploy/live-update/liveUpdate.js";
50
+ import { deployTTLs } from "./deploy/firestore-ttl/deployTTLs.js";
51
+ import { generateFirestoreIndexes } from "./deploy/rules-indexes/generateFirestoreIndexes.js";
52
+ import { generateFirestoreRules } from "./deploy/rules-indexes/generateFirestoreRules.js";
53
+ import { generateStorageRules } from "./deploy/rules-indexes/generateStorageRules.js";
54
+ import { deployProject } from "./deploy/deployProject.js";
55
+ import { migrateAll } from "./migration/migrateAll.js";
56
+ import { exportFirestoreData } from "./deploy/firestore-export/exportFirestoreData.js";
57
+ import { exportToBigQuery } from "./data/exportToBigQuery.js";
58
+ import { addProject } from "./project/addProject.js";
59
+ import { deleteProject } from "./project/deleteProject.js";
60
+ import { addRecord } from "./project/addRecord.js";
61
+ import { updateRecord } from "./project/updateRecord.js";
62
+ import { deleteRecord } from "./project/deleteRecord.js";
63
+ import { getOne } from "./project/getOne.js";
64
+ import { getSome } from "./project/getSome.js";
65
+ import { lintSchema } from "./lint/lintSchema.js";
66
+ import { securityReport } from "./lint/securityReport.js";
67
+ import { addRecordPrompt } from "./project/addRecordPrompt.js";
68
+ import { seedData } from "./data/seedData.js";
69
+ import { prepareEmulatorData } from "./project/prepareEmulatorData.js";
70
+ import { setProject } from "./project/setProject.js";
71
+ import { setUserRole } from "./ops/setUserRole.js";
72
+ import { setUserCollection } from "./ops/setUserCollection.js";
73
+ import { setUserDocument } from "./ops/setUserDocument.js";
74
+ import { getUser } from "./ops/getUser.js";
75
+ import { getUserRecord } from "./ops/getUserRecord.js";
76
+ import { getUserPermissions } from "./ops/getUserPermissions.js";
77
+ import { auditPermissions } from "./ops/auditPermissions.js";
78
+ import { explainPreloadQueries } from "./ops/explainPreloadQueries.js";
79
+ import { auditDenormalized } from "./ops/auditDenormalized.js";
80
+ import { auditRelations } from "./ops/auditRelations.js";
81
+ import { listProjects } from "./ops/listProjects.js";
82
+ import { customDomain } from "./project/customDomain.js";
83
+ import { applySchema } from "./deploy/applySchema.js";
84
+ import { addTenant } from "./project/addTenant.js";
85
+ import { deleteTenant } from "./project/deleteTenant.js";
86
+ const program = new Command();
87
+ program.name("stoker").description("Stoker Platform CLI").version(pkg.version);
88
+ program
89
+ .command("init")
90
+ .description("bootstrap a new Stoker project")
91
+ .option("-f, --force", "force initialization in a non-empty directory")
92
+ .action((options) => {
93
+ initProject(options);
94
+ });
95
+ program
96
+ .command("set-project")
97
+ .description('select a project to work with. Must be used in conjunction with `export GCP_PROJECT="<PROJECT_NAME>"`')
98
+ .action(() => {
99
+ setProject();
100
+ });
101
+ program
102
+ .command("emulator-data")
103
+ .description("Copy live app data into the Firebase Emulator Suite")
104
+ .action(() => {
105
+ prepareEmulatorData();
106
+ });
107
+ program
108
+ .command("start")
109
+ .description("start the Firebase Emulator Suite")
110
+ .option("-t, --test-mode", "start the Firebase Emulator Suite in test mode")
111
+ .action(() => {
112
+ startEmulators();
113
+ });
114
+ program
115
+ .command("start-web-app")
116
+ .description("start the web app Firebase Emulator Suite")
117
+ .action(() => {
118
+ startWebAppEmulators();
119
+ });
120
+ program
121
+ .command("build-web-app")
122
+ .description("build the web app")
123
+ .action(() => {
124
+ buildWebApp();
125
+ });
126
+ program
127
+ .command("lint-schema")
128
+ .description("lint the Stoker schema")
129
+ .action(() => {
130
+ lintSchema();
131
+ });
132
+ program
133
+ .command("security-report")
134
+ .description("run the security report")
135
+ .action(() => {
136
+ securityReport();
137
+ });
138
+ program
139
+ .command("deployment")
140
+ .description("toggle deployment status")
141
+ .requiredOption("-s, --status <status>", "idle / in_progress")
142
+ .action((options) => {
143
+ setDeploymentStatus(options.status).then(() => {
144
+ process.exit(0);
145
+ });
146
+ });
147
+ program
148
+ .command("maintenance")
149
+ .description("toggle maintenance mode")
150
+ .requiredOption("-s, --status <status>", "on / off")
151
+ .action((options) => {
152
+ if (options.status === "on") {
153
+ activateMaintenanceMode();
154
+ }
155
+ else {
156
+ disableMaintenanceMode();
157
+ }
158
+ });
159
+ program
160
+ .command("live-update")
161
+ .description("trigger a live update")
162
+ .option("-s, --secure", "enforce latest security rules")
163
+ .option("-r, --refresh", "force page refresh")
164
+ .option("-p, --payload <payload>", "additional payload to send to the client")
165
+ .action((options) => {
166
+ liveUpdate(options);
167
+ });
168
+ program
169
+ .command("persist-schema")
170
+ .description("persist schema to Firebase")
171
+ .action(() => {
172
+ persistSchema();
173
+ });
174
+ program
175
+ .command("deploy-ttls")
176
+ .description("deploy Firestore TTLs")
177
+ .action(() => {
178
+ deployTTLs();
179
+ });
180
+ program
181
+ .command("generate-firestore-indexes")
182
+ .description("generate Firestore indexes")
183
+ .action(() => {
184
+ generateFirestoreIndexes();
185
+ });
186
+ program
187
+ .command("generate-firestore-rules")
188
+ .description("generate Firestore security rules")
189
+ .action(() => {
190
+ generateFirestoreRules();
191
+ });
192
+ program
193
+ .command("generate-storage-rules")
194
+ .description("generate Cloud Storage security rules")
195
+ .action(() => {
196
+ generateStorageRules();
197
+ });
198
+ program
199
+ .command("deploy")
200
+ .description("deploy project")
201
+ .option("-e, --export", "export Firestore data before deployment")
202
+ .option("-i, --initial", "initial deployment")
203
+ .option("-r, --retry", "retry failed deployment")
204
+ .option("-s, --secure", "enforce latest security rules")
205
+ .option("--no-firestore-rules", "skip Firestore security rules generation")
206
+ .option("--no-storage-rules", "skip Cloud Storage security rules generation")
207
+ .option("--no-admin", "skip admin app deployment")
208
+ .option("--no-maintenance-on", "skip maintenance mode activation")
209
+ .option("--no-maintenance-off", "skip maintenance mode deactivation")
210
+ .option("--no-refresh", "skip live update page refresh")
211
+ .option("--no-migrate", "skip database migration")
212
+ .action((options) => {
213
+ deployProject(options);
214
+ });
215
+ program
216
+ .command("apply")
217
+ .description("apply schema to local environment")
218
+ .action(() => {
219
+ applySchema();
220
+ });
221
+ program
222
+ .command("migrate")
223
+ .description("migrate the database")
224
+ .action(() => {
225
+ migrateAll();
226
+ });
227
+ program
228
+ .command("custom-domain")
229
+ .description("set a custom domain for the project")
230
+ .requiredOption("-d, --domain <domain>", "the custom domain to set")
231
+ .action((options) => {
232
+ customDomain(options);
233
+ });
234
+ program
235
+ .command("export")
236
+ .description("export Firestore data to Cloud Storage")
237
+ .action(() => {
238
+ exportFirestoreData();
239
+ });
240
+ program
241
+ .command("bigquery")
242
+ .description("export a Firestore collection to BigQuery")
243
+ .requiredOption("-c, --collection <collection>", "collection to export to BigQuery")
244
+ .option("-f, --fields <fields>", "projection fields")
245
+ .action((options) => {
246
+ exportToBigQuery(options);
247
+ });
248
+ program
249
+ .command("seed-data")
250
+ .description("seed test data")
251
+ .requiredOption("-t, --tenant <tenant>", "the tenant to seed data for")
252
+ .requiredOption("-n, --number <number>", "number of records to seed")
253
+ .option("-r, --relations <relations>", "number of relations to seed")
254
+ .option("-s, --subcollections <subcollections>", "number of subcollection records to seed")
255
+ .option("-d", "--delay <milliseconds>", "delay between seeding records")
256
+ .option("-m, --mode <mode>", "development / production")
257
+ .action((options) => {
258
+ seedData(options);
259
+ });
260
+ program
261
+ .command("add-project")
262
+ .description("add a Google Cloud project")
263
+ .requiredOption("-n, --name <name>", "the Google Cloud Project ID for the project")
264
+ .option("--no-pitr", "disable Firestore point-in-time recovery")
265
+ .option("--no-versioning", "disable Cloud Storage versioning")
266
+ .option("-s, --soft-delete <duration>", "set Cloud Storage soft delete duration (default 30 days)")
267
+ .option("-b, --backup-recurrence <interval>", "set Firestore backup recurrence interval (default daily)")
268
+ .option("-r, --backup-retention <duration>", "set Firestore backup duration (default 7 days)")
269
+ .option("-c, --custom-cors <path>", "file path for custom Cloud Storage CORS policy")
270
+ .option("-d, --development", "specifies that this will be a development project")
271
+ .option("-t, --test-mode", "add the project in test mode")
272
+ .action((options) => {
273
+ addProject(options);
274
+ });
275
+ program
276
+ .command("delete-project")
277
+ .description("delete a Google Cloud project. Be careful!")
278
+ .option("-t, --test-mode", "delete the project in test mode")
279
+ .action((options) => {
280
+ deleteProject(options);
281
+ });
282
+ program
283
+ .command("add-tenant")
284
+ .description("add a tenant")
285
+ .action(() => {
286
+ addTenant().then(() => {
287
+ process.exit(0);
288
+ });
289
+ });
290
+ program
291
+ .command("delete-tenant")
292
+ .description("delete a tenant")
293
+ .requiredOption("-t, --tenant <tenant>", "the tenant to delete")
294
+ .action((options) => {
295
+ deleteTenant(options);
296
+ });
297
+ program
298
+ .command("add-record")
299
+ .description("add a record")
300
+ .option("-m, --mode <mode>", "development / production")
301
+ .requiredOption("-t, --tenant <tenant>", "the tenant to add the record to")
302
+ .requiredOption("-p, --path <path>", "the path of the document")
303
+ .requiredOption("-d, --data <data>", "the data to add")
304
+ .option("-a, --user-data <user>", "data for creating a user")
305
+ .option("-u, --user <user>", "the ID of the user to add the record as")
306
+ .action((options) => {
307
+ addRecord(options);
308
+ });
309
+ program
310
+ .command("add-record-prompt")
311
+ .description("add a record to a collection using terminal prompts")
312
+ .option("-m, --mode <mode>", "development / production")
313
+ .requiredOption("-t, --tenant <tenant>", "the tenant to add the record to")
314
+ .requiredOption("-c, --collection <collection>", "the collection to add the record to")
315
+ .option("-a, --full-access", "if the record is a user, grant full access to all collections")
316
+ .action((options) => {
317
+ addRecordPrompt(options.tenant, options.collection, options.fullAccess, options.mode).then(() => {
318
+ process.exit(0);
319
+ });
320
+ });
321
+ program
322
+ .command("update-record")
323
+ .description("update a record")
324
+ .option("-m, --mode <mode>", "development / production")
325
+ .requiredOption("-t, --tenant <tenant>", "the tenant of the record")
326
+ .requiredOption("-p, --path <path>", "the path of the document")
327
+ .requiredOption("-i, --id <id>", "the ID of the document")
328
+ .requiredOption("-d, --data <data>", "the data to update")
329
+ .option("-a, --user-data <user>", "data for creating, updating or deleting a user")
330
+ .option("-u, --user <user>", "the ID of the user to update the record as")
331
+ .action((options) => {
332
+ updateRecord(options);
333
+ });
334
+ program
335
+ .command("delete-record")
336
+ .description("delete a record")
337
+ .option("-m, --mode <mode>", "development / production")
338
+ .requiredOption("-t, --tenant <tenant>", "the tenant of the record")
339
+ .requiredOption("-p, --path <path>", "the path of the document")
340
+ .requiredOption("-i, --id <id>", "the ID of the document")
341
+ .option("-u, --user <user>", "the ID of the user to delete the record as")
342
+ .option("-f, --force", "force deletion of a record with soft delete enabled")
343
+ .action((options) => {
344
+ deleteRecord(options);
345
+ });
346
+ program
347
+ .command("get-one")
348
+ .description("get a record")
349
+ .option("-m, --mode <mode>", "development / production")
350
+ .requiredOption("-t, --tenant <tenant>", "the tenant of the record")
351
+ .requiredOption("-p, --path <path>", "the path of the document")
352
+ .option("-r, --relations <depth>", "retrieve relations at the specified depth")
353
+ .option("-s, --subcollections <depth>", "retrieve subcollections at the specified depth")
354
+ .option("-u, --user <user>", "the ID of the user to get the record as")
355
+ .action((options) => {
356
+ getOne(options);
357
+ });
358
+ program
359
+ .command("get-some")
360
+ .description("get multiple records")
361
+ .option("-m, --mode <mode>", "development / production")
362
+ .requiredOption("-t, --tenant <tenant>", "the tenant of the records")
363
+ .requiredOption("-p, --path <path>", "the path of the collection")
364
+ .option("-c, --constraints <constraints>", "contraints to apply to the query")
365
+ .option("-r, --relations <depth>", "retrieve relations at the specified depth")
366
+ .option("-s, --subcollections <depth>", "retrieve subcollections at the specified depth")
367
+ .option("-u, --user <user>", "the ID of the user to get the records as")
368
+ .action((options) => {
369
+ getSome(options);
370
+ });
371
+ program
372
+ .command("list-projects")
373
+ .description("list Stoker projects")
374
+ .action(() => {
375
+ listProjects();
376
+ });
377
+ program
378
+ .command("set-user-role")
379
+ .description('set the "role" custom claim for a user')
380
+ .requiredOption("-i, --id <id>", "the ID of the user")
381
+ .requiredOption("-r, --role <role>", "the role to set for the user")
382
+ .action((options) => {
383
+ setUserRole(options);
384
+ });
385
+ program
386
+ .command("set-user-collection")
387
+ .description('set the "collection" custom claim for a user')
388
+ .requiredOption("-i, --id <id>", "the ID of the user")
389
+ .requiredOption("-c, --collection <collection>", "the collection to set for the user")
390
+ .action((options) => {
391
+ setUserCollection(options);
392
+ });
393
+ program
394
+ .command("set-user-document")
395
+ .description('set the "doc" custom claim for a user')
396
+ .requiredOption("-i, --id <id>", "the ID of the user")
397
+ .requiredOption("-d, --doc <document>", "the document ID to set for the user")
398
+ .action((options) => {
399
+ setUserDocument(options);
400
+ });
401
+ program
402
+ .command("get-user")
403
+ .description("retrieve a Firebase user")
404
+ .requiredOption("-i, --id <id>", "the ID of the user")
405
+ .action((options) => {
406
+ getUser(options);
407
+ });
408
+ program
409
+ .command("get-user-record")
410
+ .description("retrieve a Firestore user record")
411
+ .requiredOption("-t, --tenant <tenant>", "the tenant to get the user record from")
412
+ .requiredOption("-c, --collection <collection>", "the collection the user exists in")
413
+ .requiredOption("-i, --id <id>", "the ID of the user")
414
+ .action((options) => {
415
+ getUserRecord(options);
416
+ });
417
+ program
418
+ .command("get-user-permissions")
419
+ .description("retrieve a Firestore user permissions record")
420
+ .option("-m, --mode <mode>", "development / production")
421
+ .requiredOption("-t, --tenant <tenant>", "the tenant to get user permissions from")
422
+ .requiredOption("-i, --id <id>", "the ID of the user")
423
+ .action((options) => {
424
+ getUserPermissions(options);
425
+ });
426
+ program
427
+ .command("explain-preload")
428
+ .description("explain / analyze preload cache queries")
429
+ .requiredOption("-t, --tenant <tenant>", "the tenant to analyze queries for")
430
+ .requiredOption("-i, --id <id>", "the ID of a user to analyze queries for")
431
+ .option("-a, --analyze", "analyze queries")
432
+ .action((options) => {
433
+ explainPreloadQueries(options);
434
+ });
435
+ program
436
+ .command("audit-permissions")
437
+ .description("detect non-default permissions for roles")
438
+ .requiredOption("-t, --tenant <tenant>", "the tenant to audit permissions for")
439
+ .option("-e, --email <email>", "email address to send the audit report to")
440
+ .option("-m, --mode <mode>", "development / production")
441
+ .action((options) => {
442
+ auditPermissions(options);
443
+ });
444
+ program
445
+ .command("audit-denormalized")
446
+ .description("audit denormalized data integrity")
447
+ .requiredOption("-t, --tenant <tenant>", "the tenant to audit denormalized data for")
448
+ .option("-m, --mode <mode>", "development / production")
449
+ .action((options) => {
450
+ auditDenormalized(options);
451
+ });
452
+ program
453
+ .command("audit-relations")
454
+ .description("audit relations data integrity")
455
+ .requiredOption("-t, --tenant <tenant>", "the tenant to audit relations for")
456
+ .option("-m, --mode <mode>", "development / production")
457
+ .action((options) => {
458
+ auditRelations(options);
459
+ });
460
+ program.parse();
@@ -0,0 +1,8 @@
1
+ import { deleteField } from "./operations/deleteField.js";
2
+ export const migrateFirestore = async (currentSchema, lastSchema) => {
3
+ console.log("Migrating Firestore...");
4
+ if (lastSchema) {
5
+ await deleteField(currentSchema, lastSchema);
6
+ }
7
+ return;
8
+ };
@@ -0,0 +1,58 @@
1
+ import { getFirestore, FieldValue } from "firebase-admin/firestore";
2
+ import { appendFileSync } from "fs";
3
+ import { join } from "path";
4
+ export const deleteField = async (currentSchema, lastSchema) => {
5
+ const deletedFields = [];
6
+ const currentSchemaKeys = Object.keys(currentSchema.collections);
7
+ const lastSchemaKeys = Object.keys(lastSchema.collections);
8
+ lastSchemaKeys.forEach((collection) => {
9
+ if (currentSchemaKeys.includes(collection)) {
10
+ // eslint-disable-next-line security/detect-object-injection
11
+ for (const field of lastSchema.collections[collection].fields) {
12
+ if (
13
+ // eslint-disable-next-line security/detect-object-injection
14
+ !currentSchema.collections[collection].fields.filter((lastField) => lastField.name === field.name).length) {
15
+ deletedFields.push(`${collection}.${field.name}`);
16
+ }
17
+ }
18
+ }
19
+ });
20
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21
+ const filePath = join(process.cwd(), ".migration", process.env.GCP_PROJECT, `v${currentSchema.version.toString()}`);
22
+ const db = await getFirestore();
23
+ const bulkWriter = db.bulkWriter();
24
+ for (const field of deletedFields) {
25
+ const [collection, fieldName] = field.split(".");
26
+ // eslint-disable-next-line security/detect-object-injection
27
+ const fieldSchema = currentSchema.collections[collection].fields.find((field) => field.name === fieldName);
28
+ if (!fieldSchema)
29
+ continue;
30
+ console.log(`Deleting field ${fieldName} from collection ${collection}...`);
31
+ bulkWriter.onWriteResult((documentRef) => {
32
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
33
+ appendFileSync(filePath, `Deleted field ${fieldName} from document ${documentRef.id} in collection ${collection}\n\n`);
34
+ });
35
+ bulkWriter.onWriteError((error) => {
36
+ console.log(error);
37
+ return true;
38
+ });
39
+ const querySnapshot = await db.collectionGroup(collection).get();
40
+ for (const doc of querySnapshot.docs) {
41
+ const tenantId = doc.ref.path.split("/")[1];
42
+ if (doc.get(fieldName) !== undefined) {
43
+ bulkWriter.set(db
44
+ .collection("tenants")
45
+ .doc(tenantId)
46
+ .collection("system_migration")
47
+ .doc(currentSchema.version.toString())
48
+ .collection(collection)
49
+ .doc(doc.id), {
50
+ [fieldName]: doc.get(fieldName),
51
+ }, { merge: true });
52
+ }
53
+ bulkWriter.update(doc.ref, { [fieldName]: FieldValue.delete() });
54
+ }
55
+ }
56
+ await bulkWriter.close();
57
+ return;
58
+ };
@@ -0,0 +1,30 @@
1
+ import { initializeFirebase, fetchLastSchema } from "@stoker-platform/node-client";
2
+ import { join } from "path";
3
+ import { mkdir, writeFile } from "fs/promises";
4
+ import { existsSync } from "fs";
5
+ import { migrateFirestore } from "./firestore/migrateFirestore.js";
6
+ import { generateSchema } from "../deploy/schema/generateSchema.js";
7
+ export const migrateAll = async () => {
8
+ await initializeFirebase();
9
+ const currentSchema = await generateSchema();
10
+ const lastSchema = await fetchLastSchema();
11
+ console.log("Migration started...");
12
+ if (isNaN(currentSchema.version)) {
13
+ throw new Error("Invalid version number");
14
+ }
15
+ const date = new Date();
16
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, security/detect-non-literal-fs-filename
17
+ const migrationDir = join(process.cwd(), ".migration", process.env.GCP_PROJECT);
18
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
19
+ const migrationDirExists = existsSync(migrationDir);
20
+ if (!migrationDirExists) {
21
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
22
+ await mkdir(migrationDir, { recursive: true });
23
+ }
24
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, security/detect-non-literal-fs-filename
25
+ const filePath = join(process.cwd(), ".migration", process.env.GCP_PROJECT, `v${currentSchema.version.toString()}`);
26
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
27
+ await writeFile(filePath, `${date.toUTCString()}\n\n`);
28
+ await migrateFirestore(currentSchema, lastSchema);
29
+ process.exit();
30
+ };