@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,3 @@
1
+ export const blueField = "bg-blue-500 dark:bg-blue-500/50 w-7 h-7 p-1.5 rounded-md text-white"
2
+ export const redField = "bg-destructive w-7 h-7 p-1.5 rounded-md text-white"
3
+ export const greenField = "bg-green-500 dark:bg-green-500/50 w-7 h-7 p-1.5 rounded-md text-white"
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,5 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ test("sample test", async () => {
4
+ expect(true).toBe(true)
5
+ })
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ESNext", "DOM"],
7
+ "moduleResolution": "Node",
8
+ "strict": true,
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "esModuleInterop": true,
12
+ "noUnusedLocals": true,
13
+ "noUnusedParameters": true,
14
+ "noImplicitReturns": true,
15
+ "skipLibCheck": true,
16
+ "outDir": "lib",
17
+
18
+ "jsx": "react-jsx"
19
+ },
20
+ "include": ["src"],
21
+ "files": ["src/vite-env.d.ts"],
22
+ "exclude": ["test", "vitest.config.ts"]
23
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config"
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ globals: true,
7
+ include: ["test/**/*"],
8
+ },
9
+ })
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@stoker-platform/cli",
3
+ "version": "0.5.11",
4
+ "type": "module",
5
+ "license": "SEE LICENSE IN LICENSE.md",
6
+ "main": "./lib/src/main.js",
7
+ "files": [
8
+ "lib",
9
+ "init-files",
10
+ "LICENSE.md"
11
+ ],
12
+ "scripts": {
13
+ "dev": "tsc --watch --preserveWatchOutput",
14
+ "lint": "tsc && eslint .",
15
+ "build": "tsc --build",
16
+ "test": "echo \"Error: no test specified\""
17
+ },
18
+ "bin": {
19
+ "stoker": "./lib/src/main.js"
20
+ },
21
+ "dependencies": {
22
+ "@faker-js/faker": "^10.1.0",
23
+ "@google-cloud/bigquery": "^8.1.1",
24
+ "@google-cloud/secret-manager": "^6.1.1",
25
+ "@google-cloud/storage": "^7.18.0",
26
+ "@inquirer/prompts": "^8.1.0",
27
+ "@stoker-platform/node-client": "0.5.8",
28
+ "@stoker-platform/types": "0.5.4",
29
+ "@stoker-platform/utils": "0.5.5",
30
+ "algoliasearch": "^5.46.2",
31
+ "commander": "^14.0.0",
32
+ "cross-spawn": "^7.0.6",
33
+ "dotenv": "^17.2.3",
34
+ "firebase-admin": "^13.6.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/cross-spawn": "^6.0.6",
38
+ "eslint-config-custom": "*",
39
+ "tsconfig": "*"
40
+ },
41
+ "engines": {
42
+ "node": ">=22",
43
+ "npm": "11"
44
+ }
45
+ }
@@ -0,0 +1,41 @@
1
+ import { BigQuery } from "@google-cloud/bigquery";
2
+ import { Storage } from "@google-cloud/storage";
3
+ import { initializeFirebase } from "@stoker-platform/node-client";
4
+ import { getApp } from "firebase-admin/app";
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ export const exportToBigQuery = async (options) => {
7
+ await initializeFirebase();
8
+ const app = getApp();
9
+ const bigqueryClient = new BigQuery({ projectId: app.options.projectId });
10
+ const storage = new Storage({ projectId: app.options.projectId });
11
+ const collection = options.collection;
12
+ // GET LATEST COLLECTION FILE
13
+ const bucket = `gs://${process.env.FB_FIRESTORE_EXPORT_BUCKET}`;
14
+ const [files] = await storage.bucket(bucket).getFiles();
15
+ const collectionFiles = files.filter((file) => file.name.includes(`all_namespaces_kind_${collection}.export_metadata`));
16
+ if (!collectionFiles.length) {
17
+ console.log(`No files for collection ${collection} found in Cloud Storage bucket.`);
18
+ process.exit();
19
+ }
20
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
21
+ const { name: latestFile } = collectionFiles.at(-1);
22
+ // CREATE BIGQUERY DATASET
23
+ const [datasets] = await bigqueryClient.getDatasets();
24
+ datasets.filter((dataset) => dataset.id === collection);
25
+ if (!datasets.length) {
26
+ const options = { location: process.env.FB_FIRESTORE_REGION };
27
+ const [dataset] = await bigqueryClient.createDataset(collection, options);
28
+ console.log(`Dataset ${dataset.id} created.`);
29
+ }
30
+ // EXPORT COLLECTION TO BIGQUERY
31
+ const datetime = new Date().toISOString().split(".")[0].replaceAll(":", "-");
32
+ const jobConfig = { sourceFormat: "DATASTORE_BACKUP" };
33
+ if (options.fields)
34
+ jobConfig.projectionFields = options.fields.split(",");
35
+ const [job] = await bigqueryClient
36
+ .dataset(collection, { location: process.env.FB_FIRESTORE_REGION })
37
+ .table(datetime)
38
+ .load(storage.bucket(bucket).file(latestFile), jobConfig);
39
+ console.log(`Job ${job.id} completed.`);
40
+ process.exit();
41
+ };
@@ -0,0 +1,347 @@
1
+ import { initializeStoker, addRecord, updateRecord, fetchCurrentSchema } from "@stoker-platform/node-client";
2
+ import { join } from "node:path";
3
+ import { faker } from "@faker-js/faker";
4
+ import { Timestamp } from "firebase-admin/firestore";
5
+ import { getField, getFieldCustomization, isRelationField, tryPromise } from "@stoker-platform/utils";
6
+ /* eslint-disable security/detect-object-injection */
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ export const seedData = async (options) => {
9
+ const records = parseInt(options.number);
10
+ const relationRecords = parseInt(options.relations);
11
+ const sucollectionRecords = parseInt(options.sucollections);
12
+ const delay = parseInt(options.delay);
13
+ if (isNaN(records)) {
14
+ throw new Error("Number of records must be a valid number");
15
+ }
16
+ if (options.relationRecords && isNaN(relationRecords)) {
17
+ throw new Error("Number of relations must be a valid number");
18
+ }
19
+ if (options.sucollectionRecords && isNaN(sucollectionRecords)) {
20
+ throw new Error("Number of subcollections must be a valid number");
21
+ }
22
+ if (options.delay && isNaN(parseInt(options))) {
23
+ throw new Error("Delay must be a valid number");
24
+ }
25
+ const { getCustomizationFile } = await initializeStoker(options.mode || "development", options.tenant, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
26
+ const schema = await fetchCurrentSchema();
27
+ const collections = options.collections || Object.keys(schema.collections);
28
+ const orderedCollections = collections.sort((a, b) => {
29
+ const schemaA = schema.collections[a];
30
+ const schemaB = schema.collections[b];
31
+ const seedOrderA = schemaA.seedOrder || Number.MAX_SAFE_INTEGER;
32
+ const seedOrderB = schemaB.seedOrder || Number.MAX_SAFE_INTEGER;
33
+ return seedOrderA - seedOrderB;
34
+ });
35
+ if (relationRecords > records) {
36
+ throw new Error("Relations records must be less than records");
37
+ }
38
+ if (sucollectionRecords > records) {
39
+ throw new Error("Subcollections records must be less than records");
40
+ }
41
+ const data = {};
42
+ const numbers = {};
43
+ const unique = {};
44
+ const twoWayProcessed = {};
45
+ const fieldCount = {};
46
+ const seedField = async (record, field, collection) => {
47
+ const customizationFile = getCustomizationFile(collection.labels.collection, schema);
48
+ const fieldCustomization = getFieldCustomization(field, customizationFile);
49
+ const { labels, auth } = collection;
50
+ switch (field.type) {
51
+ case "String": {
52
+ const getStringValue = () => {
53
+ if (auth && field.name === "Name") {
54
+ record[field.name] = faker.person.fullName();
55
+ }
56
+ else if (field.values) {
57
+ record[field.name] = field.values[Math.floor(Math.random() * field.values.length)];
58
+ }
59
+ else if (field.uuid) {
60
+ record[field.name] = faker.string.uuid();
61
+ }
62
+ else if (field.email) {
63
+ record[field.name] = faker.internet.email();
64
+ }
65
+ else if (field.ip) {
66
+ record[field.name] = faker.internet.ip();
67
+ }
68
+ else if (field.url) {
69
+ record[field.name] = faker.image.url({
70
+ width: Math.floor(Math.random() * (500 - 25 + 1)) + 25,
71
+ height: Math.floor(Math.random() * (500 - 25 + 1)) + 25,
72
+ });
73
+ }
74
+ else if (field.emoji) {
75
+ record[field.name] = faker.internet.emoji();
76
+ }
77
+ else if (field.pattern) {
78
+ const generatedValue = faker.helpers.fromRegExp(field.pattern);
79
+ record[field.name] = generatedValue.split("\\").join("").slice(1, -1);
80
+ }
81
+ else {
82
+ const minLength = field.minlength || 5;
83
+ const maxLength = field.maxlength || 500;
84
+ const length = field.length || Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;
85
+ record[field.name] = faker.string.alpha({ length });
86
+ }
87
+ };
88
+ getStringValue();
89
+ if (field.unique) {
90
+ unique[labels.collection][field.name] ||= [];
91
+ while (unique[labels.collection][field.name].includes(record[field.name])) {
92
+ getStringValue();
93
+ }
94
+ unique[labels.collection][field.name].push(record[field.name]);
95
+ }
96
+ break;
97
+ }
98
+ case "Number": {
99
+ const getNumberValue = () => {
100
+ if (field.autoIncrement) {
101
+ numbers[labels.collection][field.name] ||= 0;
102
+ numbers[labels.collection][field.name]++;
103
+ record[field.name] = numbers[labels.collection][field.name];
104
+ }
105
+ else if (field.values) {
106
+ record[field.name] = field.values[Math.floor(Math.random() * field.values.length)];
107
+ }
108
+ else if (field.decimal) {
109
+ record[field.name] = faker.number.float({
110
+ fractionDigits: field.decimal || 2,
111
+ min: field.min || 0,
112
+ max: field.max || 1000,
113
+ });
114
+ }
115
+ else {
116
+ record[field.name] = faker.number.int({
117
+ min: field.min || 0,
118
+ max: field.max || 1000,
119
+ });
120
+ }
121
+ };
122
+ getNumberValue();
123
+ if (field.unique) {
124
+ unique[labels.collection][field.name] ||= [];
125
+ while (unique[labels.collection][field.name].includes(record[field.name])) {
126
+ getNumberValue();
127
+ }
128
+ unique[labels.collection][field.name].push(record[field.name]);
129
+ }
130
+ break;
131
+ }
132
+ case "Boolean":
133
+ record[field.name] = faker.datatype.boolean();
134
+ break;
135
+ case "Timestamp": {
136
+ if (collection.ttl === field.name) {
137
+ record[field.name] = Timestamp.fromDate(new Date(Date.now() + 1000 * 60 * 60 * 24 * 30));
138
+ }
139
+ else {
140
+ const minDate = field.min ? new Date(field.min) : new Date(0);
141
+ const maxDate = field.max ? new Date(field.max) : new Date();
142
+ const randomDate = new Date(minDate.getTime() + Math.random() * (maxDate.getTime() - minDate.getTime()));
143
+ record[field.name] = Timestamp.fromDate(randomDate);
144
+ }
145
+ break;
146
+ }
147
+ case "Array": {
148
+ const isLocation = await tryPromise(fieldCustomization.admin?.location);
149
+ if (isLocation) {
150
+ record[field.name] = [faker.location.latitude(), faker.location.longitude()];
151
+ }
152
+ else {
153
+ if (field.values) {
154
+ const arrayLength = field.length ||
155
+ Math.floor(Math.random() * ((field.maxlength || 10) - (field.minlength || 1) + 1)) +
156
+ (field.minlength || 1);
157
+ record[field.name] = Array.from({ length: arrayLength }, () =>
158
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
159
+ faker.helpers.arrayElement(field.values));
160
+ }
161
+ else {
162
+ const arrayLength = field.length ||
163
+ Math.floor(Math.random() * ((field.maxlength || 10) - (field.minlength || 1) + 1)) +
164
+ (field.minlength || 1);
165
+ record[field.name] = Array.from({ length: arrayLength }, () => {
166
+ const elementLength = Math.min(Math.floor(Math.random() * 10) + 1, field.maxlength || 10);
167
+ return faker.string.alpha({ length: elementLength });
168
+ });
169
+ }
170
+ }
171
+ break;
172
+ }
173
+ case "Map": {
174
+ const isRichText = await tryPromise(fieldCustomization.admin?.richText);
175
+ record[field.name] = {};
176
+ if (!isRichText) {
177
+ const numKeys = Math.floor(Math.random() * 100) + 1;
178
+ for (let j = 0; j < numKeys; j++) {
179
+ const key = faker.string.alpha({ length: Math.floor(Math.random() * 10) + 1 });
180
+ record[field.name][key] = faker.string.alpha({ length: Math.floor(Math.random() * 10) + 1 });
181
+ }
182
+ }
183
+ else {
184
+ record[field.name] = {
185
+ ops: [
186
+ {
187
+ insert: "Test\n",
188
+ },
189
+ ],
190
+ };
191
+ }
192
+ break;
193
+ }
194
+ }
195
+ };
196
+ const getRelation = (field, record, collection) => {
197
+ if (isRelationField(field)) {
198
+ const relationCollection = schema.collections[field.collection];
199
+ const { parentCollection, singleton } = relationCollection;
200
+ if (parentCollection || singleton)
201
+ return;
202
+ const count = fieldCount[collection.labels.collection][field.name];
203
+ const relationField = {};
204
+ let numberOfRelations = 1;
205
+ if (["ManyToOne", "ManyToMany"].includes(field.type)) {
206
+ if (field.length) {
207
+ numberOfRelations = field.length;
208
+ }
209
+ else {
210
+ const min = field.min || 1;
211
+ const max = relationRecords || field.max || 5;
212
+ numberOfRelations = Math.floor(Math.random() * (max - min + 1)) + min;
213
+ }
214
+ }
215
+ for (let i = 0; i < numberOfRelations; i++) {
216
+ let relationRecord;
217
+ if (field.enforceHierarchy) {
218
+ const enforceHierarchy = field.enforceHierarchy;
219
+ const parentField = getField(collection.fields, field.enforceHierarchy.field);
220
+ const options = [];
221
+ for (const parentRecord of data[field.collection]) {
222
+ if (Object.keys(record[parentField.name]).every((parentRelation) => Object.keys(parentRecord[enforceHierarchy.recordLinkField]).includes(parentRelation))) {
223
+ options.push(parentRecord);
224
+ }
225
+ }
226
+ const index = Math.floor(Math.random() * options.length);
227
+ relationRecord = options[index];
228
+ }
229
+ else {
230
+ if (!data[field.collection]) {
231
+ throw new Error(`Collection ${field.collection} has not had any records added yet. Try changing the seed order of the collection.`);
232
+ }
233
+ relationRecord = data[field.collection][count];
234
+ }
235
+ if (relationRecord) {
236
+ fieldCount[collection.labels.collection][field.name]++;
237
+ if (fieldCount[collection.labels.collection][field.name] === records) {
238
+ fieldCount[collection.labels.collection][field.name] = 0;
239
+ }
240
+ relationField[relationRecord.id] = {
241
+ Collection_Path: relationRecord.Collection_Path,
242
+ };
243
+ if (field.includeFields) {
244
+ for (const includeField of field.includeFields) {
245
+ relationField[relationRecord.id][includeField] = relationRecord[includeField];
246
+ }
247
+ }
248
+ if (field.twoWay) {
249
+ relationRecord[field.twoWay] = {
250
+ [record.id]: {
251
+ Collection_Path: record.Collection_Path,
252
+ },
253
+ };
254
+ }
255
+ }
256
+ }
257
+ return relationField;
258
+ }
259
+ return;
260
+ };
261
+ for (const collection of orderedCollections) {
262
+ const collectionSchema = schema.collections[collection];
263
+ const { auth, fields, parentCollection, singleton, softDelete } = collectionSchema;
264
+ if (parentCollection || singleton)
265
+ continue;
266
+ data[collection] = [];
267
+ numbers[collection] = {};
268
+ unique[collection] = {};
269
+ twoWayProcessed[collection] ||= [];
270
+ fieldCount[collection] ||= {};
271
+ for (let i = 0; i < records; i++) {
272
+ const record = {};
273
+ for (const field of fields) {
274
+ if (field.type === "Embedding")
275
+ continue;
276
+ if (softDelete?.timestampField === field.name)
277
+ continue;
278
+ if (softDelete?.archivedField === field.name) {
279
+ record[field.name] = false;
280
+ continue;
281
+ }
282
+ if (field.restrictCreate === true)
283
+ continue;
284
+ if (!isRelationField(field) && !(auth && field.name === "User_ID")) {
285
+ await seedField(record, field, collectionSchema);
286
+ }
287
+ else if (isRelationField(field) &&
288
+ (field.required || field.min) &&
289
+ !(field.twoWay && (twoWayProcessed[collection].includes(field.name) || field.type === "ManyToOne"))) {
290
+ fieldCount[collection][field.name] ||= 0;
291
+ const relation = getRelation(field, record, collectionSchema);
292
+ if (relation) {
293
+ record[field.name] = relation;
294
+ }
295
+ }
296
+ }
297
+ if (delay) {
298
+ await new Promise((resolve) => setTimeout(resolve, delay));
299
+ }
300
+ const result = await addRecord([collection], record);
301
+ data[collection].push(result);
302
+ console.log(`Added record ${result.id} to collection ${collection}`);
303
+ }
304
+ for (const field of fields) {
305
+ if (isRelationField(field) && field.twoWay && field.required) {
306
+ twoWayProcessed[field.collection] ||= [];
307
+ twoWayProcessed[field.collection].push(field.twoWay);
308
+ }
309
+ }
310
+ }
311
+ for (const collection of orderedCollections) {
312
+ const collectionSchema = schema.collections[collection];
313
+ const { fields, parentCollection, singleton } = collectionSchema;
314
+ if (parentCollection || singleton)
315
+ continue;
316
+ for (const record of data[collection]) {
317
+ const updatedRecord = {};
318
+ for (const field of fields) {
319
+ if (field.restrictUpdate === true)
320
+ continue;
321
+ if (isRelationField(field) &&
322
+ (field.required || field.min) &&
323
+ !(field.twoWay && (twoWayProcessed[collection].includes(field.name) || field.type === "ManyToOne"))) {
324
+ fieldCount[collection][field.name] ||= 0;
325
+ const relation = getRelation(field, record, collectionSchema);
326
+ if (relation) {
327
+ updatedRecord[field.name] = relation;
328
+ }
329
+ }
330
+ }
331
+ if (delay) {
332
+ await new Promise((resolve) => setTimeout(resolve, delay));
333
+ }
334
+ const result = await updateRecord([collection], record.id, updatedRecord);
335
+ const index = data[collection].findIndex((r) => r.id === result.id);
336
+ data[collection] = data[collection].with(index, result);
337
+ console.log(`Added relations to record ${result.id} in collection ${collection}`);
338
+ }
339
+ for (const field of fields) {
340
+ if (isRelationField(field) && field.twoWay && !field.required) {
341
+ twoWayProcessed[field.collection] ||= [];
342
+ twoWayProcessed[field.collection].push(field.twoWay);
343
+ }
344
+ }
345
+ }
346
+ process.exit();
347
+ };
@@ -0,0 +1,43 @@
1
+ import { runChildProcess } from "@stoker-platform/node-client";
2
+ import { getFunctionsData } from "./cloud-functions/getFunctionsData.js";
3
+ import { generateSchema } from "./schema/generateSchema.js";
4
+ import { readdir, readFile, writeFile } from "fs/promises";
5
+ import { join } from "path";
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ export const applySchema = async () => {
8
+ try {
9
+ await runChildProcess("npm", ["run", "format"]);
10
+ await runChildProcess("npm", ["run", "lint"]);
11
+ await runChildProcess("npm", ["run", "build"]);
12
+ await runChildProcess("npm", ["run", "test"]);
13
+ await runChildProcess("npx", ["stoker", "lint-schema"]);
14
+ await runChildProcess("npx", ["stoker", "security-report"]);
15
+ await runChildProcess("npx", ["stoker", "generate-firestore-rules"]);
16
+ await runChildProcess("npx", ["stoker", "generate-storage-rules"]);
17
+ const schema = await generateSchema(true);
18
+ const emulatorExportDir = join(process.cwd(), "firebase-emulator-data", "database_export");
19
+ const exportFiles = await readdir(emulatorExportDir);
20
+ const targetFile = exportFiles[0];
21
+ if (!targetFile)
22
+ throw new Error("No emulator RTDB export file found in database_export directory");
23
+ const targetPath = join(emulatorExportDir, targetFile);
24
+ let emulatorData = {};
25
+ try {
26
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
27
+ const currentContent = await readFile(targetPath, "utf8");
28
+ emulatorData = JSON.parse(currentContent);
29
+ }
30
+ catch {
31
+ emulatorData = {};
32
+ }
33
+ schema.published_time = Date.now();
34
+ emulatorData.schema = { "1": schema };
35
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
36
+ await writeFile(targetPath, JSON.stringify(emulatorData, null, 4));
37
+ await getFunctionsData();
38
+ process.exit();
39
+ }
40
+ catch (error) {
41
+ throw new Error("Error applying schema.", { cause: error });
42
+ }
43
+ };
@@ -0,0 +1,18 @@
1
+ import { initializeFirebase, tryPromise } from "@stoker-platform/node-client";
2
+ import { writeFileSync } from "fs";
3
+ import { generateSchema } from "../schema/generateSchema.js";
4
+ import { join } from "path";
5
+ import { pathToFileURL } from "url";
6
+ export const getFunctionsData = async () => {
7
+ await initializeFirebase();
8
+ const path = join(process.cwd(), "lib", "main.js");
9
+ const url = pathToFileURL(path).href;
10
+ const globalConfigFile = await import(/* @vite-ignore */ url);
11
+ const config = globalConfigFile.default;
12
+ const globalConfig = config("node");
13
+ const timezone = await tryPromise(globalConfig.timezone);
14
+ const schema = await generateSchema();
15
+ const filePath = join(process.cwd(), "functions", "project-data.json");
16
+ writeFileSync(filePath, JSON.stringify({ timezone, schema }, null, 2));
17
+ return;
18
+ };
@@ -0,0 +1,116 @@
1
+ import { fetchCurrentSchema, initializeFirebase, runChildProcess } from "@stoker-platform/node-client";
2
+ import { setDeploymentStatus } from "./maintenance/setDeploymentStatus.js";
3
+ import { getFunctionsData } from "./cloud-functions/getFunctionsData.js";
4
+ import { retryOperation } from "@stoker-platform/utils";
5
+ import { generateSchema } from "./schema/generateSchema.js";
6
+ import isEqual from "lodash/isEqual.js";
7
+ import cloneDeep from "lodash/cloneDeep.js";
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ export const deployProject = async (options) => {
10
+ try {
11
+ await initializeFirebase();
12
+ if (options.retry) {
13
+ await runChildProcess("npx", ["stoker", "deployment", "--status", "idle"]);
14
+ }
15
+ if (!options.initial) {
16
+ const currentSchema = await generateSchema();
17
+ const lastSchema = await fetchCurrentSchema();
18
+ const currentScemaCopy = cloneDeep(currentSchema);
19
+ delete currentScemaCopy.published_time;
20
+ delete currentScemaCopy.version;
21
+ const lastSchemaCopy = cloneDeep(lastSchema);
22
+ delete lastSchemaCopy.published_time;
23
+ delete lastSchemaCopy.version;
24
+ if (!options.maintenanceOn && !isEqual(currentScemaCopy, lastSchemaCopy)) {
25
+ throw new Error("The schema for this project has changed. Maintenance mode cannot be disabled.");
26
+ }
27
+ }
28
+ await runChildProcess("npm", ["run", "format"]);
29
+ await runChildProcess("npm", ["run", "lint"]);
30
+ await runChildProcess("npm", ["run", "build"]);
31
+ await runChildProcess("npm", ["run", "test"]);
32
+ await runChildProcess("npx", ["stoker", "build-web-app"]);
33
+ await runChildProcess("npx", ["stoker", "lint-schema"]);
34
+ await runChildProcess("npx", ["stoker", "security-report"]);
35
+ await setDeploymentStatus("in_progress");
36
+ if (options.maintenanceOn && !options.initial)
37
+ await runChildProcess("npx", ["stoker", "maintenance", "--status", "on"]);
38
+ if (options.export) {
39
+ await runChildProcess("npx", ["stoker", "export"]);
40
+ }
41
+ if (!options.retry || options.initial) {
42
+ await runChildProcess("npx", ["stoker", "persist-schema"]);
43
+ }
44
+ await runChildProcess("npx", ["stoker", "deploy-ttls"]);
45
+ await runChildProcess("npx", ["stoker", "generate-firestore-indexes"]);
46
+ if (options.firestoreRules)
47
+ await runChildProcess("npx", ["stoker", "generate-firestore-rules"]);
48
+ if (options.storageRules)
49
+ await runChildProcess("npx", ["stoker", "generate-storage-rules"]);
50
+ await getFunctionsData();
51
+ if (options.migrate && !options.initial) {
52
+ await runChildProcess("npx", ["stoker", "migrate"]);
53
+ }
54
+ await initializeFirebase();
55
+ if (options.initial) {
56
+ await retryOperation(async () => {
57
+ await runChildProcess("npx", [
58
+ "firebase",
59
+ "deploy",
60
+ "--only",
61
+ "firestore,database,storage",
62
+ "--project",
63
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
64
+ process.env.GCP_PROJECT,
65
+ "--force",
66
+ ]);
67
+ }, [], undefined, 10000);
68
+ await retryOperation(async () => {
69
+ const output = await runChildProcess("npx", [
70
+ "firebase",
71
+ "deploy",
72
+ "--only",
73
+ "functions",
74
+ "--project",
75
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
76
+ process.env.GCP_PROJECT,
77
+ "--force",
78
+ ], undefined, { ...process.env, FUNCTIONS_DISCOVERY_TIMEOUT: "30" });
79
+ if (output.includes("HTTP Error: 400"))
80
+ throw new Error("PERMISSION_DENIED");
81
+ }, [], undefined, 10000);
82
+ await retryOperation(async () => {
83
+ await runChildProcess("npx", [
84
+ "firebase",
85
+ "deploy",
86
+ "--only",
87
+ "extensions",
88
+ "--project",
89
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
90
+ process.env.GCP_PROJECT,
91
+ "--force",
92
+ ]);
93
+ }, [], undefined, 10000);
94
+ }
95
+ else {
96
+ await runChildProcess("npx",
97
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
98
+ ["firebase", "deploy", "--project", process.env.GCP_PROJECT, "--force"], undefined, { ...process.env, FUNCTIONS_DISCOVERY_TIMEOUT: "30" });
99
+ }
100
+ if (options.admin)
101
+ await runChildProcess("npm", ["exec", "--package=@stoker-platform/web-app", "--", "deploy-web-app"]);
102
+ const liveUpdateOptions = [];
103
+ if (options.secure && !options.initial)
104
+ liveUpdateOptions.push("--secure");
105
+ if (options.refresh && !options.initial)
106
+ liveUpdateOptions.push("--refresh");
107
+ await runChildProcess("npx", ["stoker", "live-update", ...liveUpdateOptions]);
108
+ if (options.maintenanceOff)
109
+ await runChildProcess("npx", ["stoker", "maintenance", "--status", "off"]);
110
+ await setDeploymentStatus("idle");
111
+ process.exit();
112
+ }
113
+ catch (error) {
114
+ throw new Error("Error deploying project.", { cause: error });
115
+ }
116
+ };