@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.
- package/LICENSE.md +102 -0
- package/init-files/.##gitignore## +102 -0
- package/init-files/.devcontainer/devcontainer.json +14 -0
- package/init-files/.env/.env +70 -0
- package/init-files/.eslintrc.cjs +35 -0
- package/init-files/.firebaserc +6 -0
- package/init-files/.prettierignore +86 -0
- package/init-files/.prettierrc +5 -0
- package/init-files/bin/build.js +221 -0
- package/init-files/bin/shim.js +159 -0
- package/init-files/extensions/firestore-send-email.env +7 -0
- package/init-files/external.package.json +4 -0
- package/init-files/firebase-rules/database.rules.json +9 -0
- package/init-files/firebase-rules/firestore.custom.rules +19 -0
- package/init-files/firebase-rules/firestore.indexes.json +0 -0
- package/init-files/firebase-rules/firestore.rules +0 -0
- package/init-files/firebase-rules/storage.rules +0 -0
- package/init-files/firebase.hosting.json +122 -0
- package/init-files/firebase.json +52 -0
- package/init-files/functions/.##gitignore## +14 -0
- package/init-files/functions/.eslintrc.cjs +28 -0
- package/init-files/functions/package.json +46 -0
- package/init-files/functions/prompts/chat.prompt +17 -0
- package/init-files/functions/src/index.ts +457 -0
- package/init-files/functions/tsconfig.dev.json +3 -0
- package/init-files/functions/tsconfig.json +17 -0
- package/init-files/icons/logo-large.png +0 -0
- package/init-files/icons/logo-small.png +0 -0
- package/init-files/ops.js +25 -0
- package/init-files/package.json +53 -0
- package/init-files/project-data.json +5 -0
- package/init-files/remoteconfig.template.json +1 -0
- package/init-files/src/collections/Inbox.ts +444 -0
- package/init-files/src/collections/Outbox.ts +270 -0
- package/init-files/src/collections/Settings.ts +44 -0
- package/init-files/src/collections/Users.ts +138 -0
- package/init-files/src/main.ts +245 -0
- package/init-files/src/utils.ts +3 -0
- package/init-files/src/vite-env.d.ts +1 -0
- package/init-files/test/test.ts +5 -0
- package/init-files/tsconfig.json +23 -0
- package/init-files/vitest.config.ts +9 -0
- package/lib/package.json +45 -0
- package/lib/src/data/exportToBigQuery.js +41 -0
- package/lib/src/data/seedData.js +347 -0
- package/lib/src/deploy/applySchema.js +43 -0
- package/lib/src/deploy/cloud-functions/getFunctionsData.js +18 -0
- package/lib/src/deploy/deployProject.js +116 -0
- package/lib/src/deploy/firestore-export/exportFirestoreData.js +29 -0
- package/lib/src/deploy/firestore-ttl/deployTTLs.js +127 -0
- package/lib/src/deploy/live-update/liveUpdate.js +22 -0
- package/lib/src/deploy/maintenance/activateMaintenanceMode.js +9 -0
- package/lib/src/deploy/maintenance/disableMaintenanceMode.js +9 -0
- package/lib/src/deploy/maintenance/setDeploymentStatus.js +22 -0
- package/lib/src/deploy/rules-indexes/generateFirestoreIndexes.js +23 -0
- package/lib/src/deploy/rules-indexes/generateFirestoreRules.js +35 -0
- package/lib/src/deploy/rules-indexes/generateStorageRules.js +23 -0
- package/lib/src/deploy/schema/generateSchema.js +184 -0
- package/lib/src/deploy/schema/persistSchema.js +14 -0
- package/lib/src/lint/lintSchema.js +1491 -0
- package/lib/src/lint/securityReport.js +223 -0
- package/lib/src/main.js +460 -0
- package/lib/src/migration/firestore/migrateFirestore.js +8 -0
- package/lib/src/migration/firestore/operations/deleteField.js +58 -0
- package/lib/src/migration/migrateAll.js +30 -0
- package/lib/src/ops/auditDenormalized.js +124 -0
- package/lib/src/ops/auditPermissions.js +92 -0
- package/lib/src/ops/auditRelations.js +186 -0
- package/lib/src/ops/explainPreloadQueries.js +65 -0
- package/lib/src/ops/getUser.js +10 -0
- package/lib/src/ops/getUserPermissions.js +19 -0
- package/lib/src/ops/getUserRecord.js +20 -0
- package/lib/src/ops/listProjects.js +8 -0
- package/lib/src/ops/setUserCollection.js +14 -0
- package/lib/src/ops/setUserDocument.js +11 -0
- package/lib/src/ops/setUserRole.js +14 -0
- package/lib/src/project/addProject.js +935 -0
- package/lib/src/project/addRecord.js +9 -0
- package/lib/src/project/addRecordPrompt.js +205 -0
- package/lib/src/project/addTenant.js +59 -0
- package/lib/src/project/buildWebApp.js +10 -0
- package/lib/src/project/customDomain.js +157 -0
- package/lib/src/project/deleteProject.js +51 -0
- package/lib/src/project/deleteRecord.js +11 -0
- package/lib/src/project/deleteTenant.js +49 -0
- package/lib/src/project/getOne.js +25 -0
- package/lib/src/project/getSome.js +28 -0
- package/lib/src/project/initProject.js +16 -0
- package/lib/src/project/prepareEmulatorData.js +125 -0
- package/lib/src/project/setProject.js +13 -0
- package/lib/src/project/startEmulators.js +30 -0
- package/lib/src/project/updateRecord.js +9 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +45 -0
package/lib/src/main.js
ADDED
|
@@ -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,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
|
+
};
|