@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,9 @@
1
+ import { addRecord as addStokerRecord, initializeStoker } from "@stoker-platform/node-client";
2
+ import { join } from "path";
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export const addRecord = async (options) => {
5
+ await initializeStoker(options.mode || "production", options.tenant, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
6
+ const result = await addStokerRecord(options.path.split("/"), JSON.parse(options.data), options.userData ? JSON.parse(options.userData) : undefined, options.user);
7
+ console.log(JSON.stringify(result, null, 2));
8
+ process.exit();
9
+ };
@@ -0,0 +1,205 @@
1
+ import { input, password, select } from "@inquirer/prompts";
2
+ import { addRecord, fetchCurrentSchema, getOne, initializeStoker } from "@stoker-platform/node-client";
3
+ import { getField, getFieldCustomization, isRelationField, tryFunction } from "@stoker-platform/utils";
4
+ import { Timestamp } from "firebase-admin/firestore";
5
+ import { join } from "node:path";
6
+ export const addRecordPrompt = async (tenantId, collectionName, fullAccess, mode, relationIds) => {
7
+ const { getCustomizationFile } = (await initializeStoker(mode || "production", tenantId, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections")));
8
+ const schema = await fetchCurrentSchema();
9
+ // eslint-disable-next-line security/detect-object-injection
10
+ const collection = schema.collections[collectionName];
11
+ if (!collection)
12
+ throw new Error(`Collection ${collectionName} not found in schema`);
13
+ if (collection.parentCollection)
14
+ throw new Error("The CLI prompt does not support adding to subcollections");
15
+ const { labels, fields, softDelete } = collection;
16
+ const record = {};
17
+ const user = {};
18
+ const customization = getCustomizationFile(labels.collection, schema);
19
+ for (const field of fields) {
20
+ const fieldCustomization = getFieldCustomization(field, customization);
21
+ if (!("autoIncrement" in field && field.autoIncrement) &&
22
+ !fieldCustomization.custom?.initialValue &&
23
+ !(collection.auth && field.name === "User_ID") &&
24
+ !(softDelete && (field.name === softDelete.archivedField || field.name === softDelete.timestampField)) &&
25
+ !(isRelationField(field) && ["ManyToMany", "ManyToOne"].includes(field.type)) &&
26
+ field.type !== "Embedding" &&
27
+ field.type !== "Array" &&
28
+ field.type !== "Map" &&
29
+ !(field.type === "String" && tryFunction(fieldCustomization.admin?.image))) {
30
+ if (isRelationField(field)) {
31
+ let value;
32
+ if (relationIds?.[field.collection]) {
33
+ value = relationIds[field.collection];
34
+ }
35
+ else {
36
+ value = await input({
37
+ message: `${labels.collection}- ${field.name}${field.required ? "*" : ""}`,
38
+ });
39
+ }
40
+ if (value) {
41
+ /* eslint-disable security/detect-object-injection */
42
+ const relationRecord = await getOne([field.collection], value);
43
+ record[field.name] ||= {};
44
+ record[field.name][value] = {
45
+ Collection_Path: relationRecord.Collection_Path,
46
+ };
47
+ if (field.includeFields) {
48
+ for (const includeField of field.includeFields) {
49
+ if (relationRecord[includeField]) {
50
+ record[field.name][value][includeField] = relationRecord[includeField];
51
+ }
52
+ }
53
+ }
54
+ /* eslint-enable security/detect-object-injection */
55
+ }
56
+ }
57
+ else if (field.type === "Timestamp") {
58
+ const value = await input({
59
+ message: `${labels.collection}- ${field.name}${field.required ? "*" : ""}`,
60
+ });
61
+ if (value) {
62
+ record[field.name] = Timestamp.fromMillis(Number(value));
63
+ }
64
+ }
65
+ else if (field.type !== "Boolean") {
66
+ if ("values" in field && field.values) {
67
+ record[field.name] = await select({
68
+ message: `${labels.collection}- ${field.name}${field.required ? "*" : ""}`,
69
+ choices: field.values.map((value) => {
70
+ return { value: value };
71
+ }),
72
+ });
73
+ }
74
+ else {
75
+ const value = await input({
76
+ message: `${labels.collection}- ${field.name}${field.required ? "*" : ""}`,
77
+ });
78
+ if (field.type === "Number") {
79
+ record[field.name] = Number(value);
80
+ }
81
+ else {
82
+ record[field.name] = value;
83
+ }
84
+ }
85
+ }
86
+ else {
87
+ record[field.name] = await select({
88
+ message: `${labels.collection}- ${field.name}${field.required ? "*" : ""}`,
89
+ choices: [
90
+ { name: "TRUE", value: true },
91
+ { name: "FALSE", value: false },
92
+ ],
93
+ });
94
+ }
95
+ }
96
+ }
97
+ if (collection.auth) {
98
+ user.permissions = {};
99
+ user.permissions.Role = record.Role;
100
+ user.permissions.collections = {};
101
+ for (const [collectionName, collection] of Object.entries(schema.collections)) {
102
+ // eslint-disable-next-line security/detect-object-injection
103
+ user.permissions.collections[collectionName] = {
104
+ operations: [],
105
+ };
106
+ const assignable = collection.access.operations.assignable;
107
+ for (const operationType of ["read", "create", "update", "delete"]) {
108
+ const operationTypeUpper = (operationType.charAt(0).toUpperCase() + operationType.slice(1));
109
+ const operation = collection.access.operations[operationType];
110
+ if (typeof operation === "object" && operation.includes(record.Role)) {
111
+ if (fullAccess) {
112
+ // eslint-disable-next-line security/detect-object-injection
113
+ user.permissions.collections[collectionName].operations.push(operationTypeUpper);
114
+ }
115
+ else {
116
+ if (!(assignable === true ||
117
+ (typeof assignable === "object" && assignable?.includes(record.Role)))) {
118
+ // eslint-disable-next-line security/detect-object-injection
119
+ user.permissions.collections[collectionName].operations.push(operationTypeUpper);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ if (collection.auth && collection.access.auth?.includes(record.Role)) {
125
+ // eslint-disable-next-line security/detect-object-injection
126
+ user.permissions.collections[collectionName].auth = true;
127
+ }
128
+ if (collection.access.attributeRestrictions) {
129
+ for (const attributeRestriction of collection.access.attributeRestrictions) {
130
+ if ("roles" in attributeRestriction) {
131
+ const role = attributeRestriction.roles.find((role) => role.role === record.Role);
132
+ if (!role)
133
+ continue;
134
+ if (role.assignable)
135
+ continue;
136
+ if (attributeRestriction.type === "Record_Owner") {
137
+ // eslint-disable-next-line security/detect-object-injection
138
+ user.permissions.collections[collectionName].recordOwner = { active: true };
139
+ }
140
+ if (attributeRestriction.type === "Record_User") {
141
+ // eslint-disable-next-line security/detect-object-injection
142
+ user.permissions.collections[collectionName].recordUser = { active: true };
143
+ }
144
+ if (attributeRestriction.type === "Record_Property") {
145
+ // eslint-disable-next-line security/detect-object-injection
146
+ user.permissions.collections[collectionName].recordProperty = { active: true };
147
+ }
148
+ }
149
+ }
150
+ }
151
+ if (collection.access.entityRestrictions?.restrictions) {
152
+ if (collection.access.entityRestrictions?.assignable?.includes(record.Role))
153
+ continue;
154
+ let hasEntityRestrictions = false;
155
+ for (const entityRestriction of collection.access.entityRestrictions.restrictions) {
156
+ if ("roles" in entityRestriction) {
157
+ const role = entityRestriction.roles.find((role) => role.role === record.Role);
158
+ if (!role)
159
+ continue;
160
+ if (entityRestriction.type === "Individual") {
161
+ // eslint-disable-next-line security/detect-object-injection
162
+ user.permissions.collections[collectionName].individualEntities = [];
163
+ hasEntityRestrictions = true;
164
+ }
165
+ if (entityRestriction.type === "Parent") {
166
+ // eslint-disable-next-line security/detect-object-injection
167
+ const collectionField = getField(collection.fields, entityRestriction.collectionField);
168
+ if (isRelationField(collectionField)) {
169
+ // eslint-disable-next-line security/detect-object-injection
170
+ user.permissions.collections[collectionName].parentEntities = [];
171
+ hasEntityRestrictions = true;
172
+ }
173
+ }
174
+ if (entityRestriction.type === "Parent_Property") {
175
+ const collectionField = getField(collection.fields, entityRestriction.collectionField);
176
+ if (isRelationField(collectionField)) {
177
+ // eslint-disable-next-line security/detect-object-injection
178
+ user.permissions.collections[collectionName].parentPropertyEntities = {};
179
+ hasEntityRestrictions = true;
180
+ }
181
+ }
182
+ }
183
+ }
184
+ if (hasEntityRestrictions) {
185
+ // eslint-disable-next-line security/detect-object-injection
186
+ user.permissions.collections[collectionName].restrictEntities = true;
187
+ }
188
+ }
189
+ }
190
+ user.password = await password({
191
+ message: `${labels.collection}- Password*`,
192
+ mask: true,
193
+ });
194
+ }
195
+ for (const field of Object.entries(record)) {
196
+ const [fieldName, fieldValue] = field;
197
+ if (fieldValue === "") {
198
+ // eslint-disable-next-line security/detect-object-injection
199
+ delete record[fieldName];
200
+ }
201
+ }
202
+ const result = await addRecord([labels.collection], record, user);
203
+ console.log(result);
204
+ return result;
205
+ };
@@ -0,0 +1,59 @@
1
+ import { input } from "@inquirer/prompts";
2
+ import { fetchCurrentSchema, initializeFirebase, initializeStoker } from "@stoker-platform/node-client";
3
+ import { getFirestore } from "firebase-admin/firestore";
4
+ import { join } from "path";
5
+ import { retryOperation, isRelationField } from "@stoker-platform/utils";
6
+ import { addRecordPrompt } from "./addRecordPrompt.js";
7
+ export const addTenant = async () => {
8
+ await initializeFirebase();
9
+ const db = getFirestore();
10
+ const doc = await db.collection("tenants").add({});
11
+ const tenantId = doc.id;
12
+ await initializeStoker("production", tenantId, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
13
+ const schema = await fetchCurrentSchema();
14
+ const collections = Object.entries(schema.collections);
15
+ for (const collectionSchema of collections) {
16
+ const [collectionName, collection] = collectionSchema;
17
+ const { fields } = collection;
18
+ for (const field of fields) {
19
+ if ("autoIncrement" in field && field.autoIncrement) {
20
+ const initialValue = await input({
21
+ message: `Initial value for ${field.name} in collection ${collectionName}`,
22
+ });
23
+ await db
24
+ .collection("tenants")
25
+ .doc(tenantId)
26
+ .collection("system_auto_increment")
27
+ .doc(collectionName)
28
+ .collection("fields")
29
+ .doc(field.name)
30
+ .set({ number: parseInt(initialValue) });
31
+ }
32
+ }
33
+ }
34
+ const collectionRecordCreated = [];
35
+ const relationIds = {};
36
+ for (const [collectionName, collection] of Object.entries(schema.collections)) {
37
+ if (collection.auth || collection.singleton) {
38
+ for (const field of collection.fields) {
39
+ if (isRelationField(field) && field.required) {
40
+ if (!collectionRecordCreated.includes(field.collection)) {
41
+ await retryOperation(async () => {
42
+ const result = await addRecordPrompt(tenantId, field.collection, collection.auth);
43
+ relationIds[field.collection] = result.id;
44
+ }, [], (error) => console.log(error), 0, 100);
45
+ collectionRecordCreated.push(field.collection);
46
+ }
47
+ }
48
+ }
49
+ if (!collectionRecordCreated.includes(collectionName)) {
50
+ await retryOperation(async () => {
51
+ await addRecordPrompt(tenantId, collectionName, collection.auth, undefined, relationIds);
52
+ }, [], (error) => console.log(error), 0, 100);
53
+ collectionRecordCreated.push(collectionName);
54
+ }
55
+ }
56
+ }
57
+ console.log(`Tenant ${tenantId} created successfully.`);
58
+ return;
59
+ };
@@ -0,0 +1,10 @@
1
+ import { runChildProcess } from "@stoker-platform/node-client";
2
+ export const buildWebApp = async () => {
3
+ try {
4
+ await runChildProcess("npm", ["exec", "--package=@stoker-platform/web-app", "--", "build-web-app"]);
5
+ process.exit();
6
+ }
7
+ catch {
8
+ throw new Error("Error building the web app.");
9
+ }
10
+ };
@@ -0,0 +1,157 @@
1
+ import { runChildProcess } from "@stoker-platform/node-client";
2
+ import { unlink, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ export const customDomain = async (options) => {
6
+ if (!process.env.GCP_PROJECT) {
7
+ throw new Error("GCP_PROJECT environment variable is not set.");
8
+ }
9
+ const projectId = process.env.GCP_PROJECT;
10
+ const token = await runChildProcess("gcloud", ["auth", "print-access-token"]).catch(() => {
11
+ throw new Error("Error getting Google Cloud identity token.");
12
+ });
13
+ const hostingResponse = await fetch(`https://firebasehosting.googleapis.com/v1beta1/projects/${projectId}/sites/${projectId}/customDomains?parent=projects/${projectId}/sites/${projectId}&customDomainId=${options.domain}`, {
14
+ method: "POST",
15
+ headers: {
16
+ Authorization: `Bearer ${token}`,
17
+ "Content-Type": "application/json",
18
+ "X-Goog-User-Project": projectId,
19
+ },
20
+ body: "{}",
21
+ });
22
+ const hostingResponseJson = await hostingResponse.json();
23
+ console.log(hostingResponseJson);
24
+ if (!hostingResponse.ok) {
25
+ throw new Error("Error adding custom domain.");
26
+ }
27
+ const listKeysResponse = await fetch(`https://recaptchaenterprise.googleapis.com/v1/projects/${projectId}/keys`, {
28
+ method: "GET",
29
+ headers: {
30
+ Authorization: `Bearer ${token}`,
31
+ "Content-Type": "application/json",
32
+ "X-Goog-User-Project": projectId,
33
+ },
34
+ });
35
+ const listKeysJson = await listKeysResponse.json();
36
+ const appCheckKey = listKeysJson.keys?.find((key) => key.displayName === "Firebase App Check");
37
+ if (!appCheckKey) {
38
+ throw new Error("Could not find existing App Check key");
39
+ }
40
+ const recaptchaResponse = await fetch(`https://recaptchaenterprise.googleapis.com/v1/projects/${projectId}/keys/${appCheckKey.name.split("/").pop()}?updateMask=webSettings.allowedDomains`, {
41
+ method: "PATCH",
42
+ headers: {
43
+ Authorization: `Bearer ${token}`,
44
+ "Content-Type": "application/json",
45
+ "X-Goog-User-Project": projectId,
46
+ },
47
+ body: JSON.stringify({
48
+ webSettings: {
49
+ ...appCheckKey.webSettings,
50
+ allowedDomains: [...(appCheckKey.webSettings.allowedDomains || []), options.domain],
51
+ },
52
+ }),
53
+ });
54
+ const recaptchaResponseJson = await recaptchaResponse.json();
55
+ console.log(recaptchaResponseJson);
56
+ if (!recaptchaResponse.ok) {
57
+ throw new Error("Failed to update Recaptcha key");
58
+ }
59
+ const authResponse = await fetch(`https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config`, {
60
+ method: "GET",
61
+ headers: {
62
+ Authorization: `Bearer ${token}`,
63
+ Accept: "application/json",
64
+ "Content-Type": "application/json",
65
+ "X-Goog-User-Project": projectId,
66
+ },
67
+ });
68
+ const authResponseJson = await authResponse.json();
69
+ console.log(authResponseJson);
70
+ if (!authResponse.ok) {
71
+ throw new Error("Error getting Firebase Auth authorized domains.");
72
+ }
73
+ const auth = await fetch(`https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config`, {
74
+ method: "PATCH",
75
+ headers: {
76
+ Authorization: `Bearer ${token}`,
77
+ Accept: "application/json",
78
+ "Content-Type": "application/json",
79
+ "X-Goog-User-Project": projectId,
80
+ },
81
+ body: JSON.stringify({
82
+ ...authResponseJson,
83
+ authorizedDomains: [...(authResponseJson.authorizedDomains || []), options.domain],
84
+ }),
85
+ });
86
+ const authUpdateResponse = await auth.json();
87
+ console.log(authUpdateResponse);
88
+ if (!auth.ok) {
89
+ throw new Error("Error updating Firebase Auth authorized domains.");
90
+ }
91
+ const apiKeys = await runChildProcess("gcloud", [
92
+ "services",
93
+ "api-keys",
94
+ "list",
95
+ `--project=${projectId}`,
96
+ "--quiet",
97
+ "--format=json",
98
+ ]);
99
+ const apiKeysJson = JSON.parse(apiKeys);
100
+ const apiKey = apiKeysJson[0];
101
+ if (!apiKey) {
102
+ throw new Error("Error getting Firebase API key.");
103
+ }
104
+ const allowedReferrers = `--allowed-referrers=${[...apiKey.restrictions.browserKeyRestrictions.allowedReferrers, `https://${options.domain}`].join(",")}`;
105
+ const apiKeyUpdateArgs = [
106
+ "services",
107
+ "api-keys",
108
+ "update",
109
+ apiKey.uid,
110
+ allowedReferrers,
111
+ `--project=${projectId}`,
112
+ "--quiet",
113
+ ];
114
+ await runChildProcess("gcloud", apiKeyUpdateArgs);
115
+ if (!process.env.FB_STORAGE_CORS) {
116
+ const corsConfigString = await runChildProcess("gcloud", [
117
+ "storage",
118
+ "buckets",
119
+ "describe",
120
+ `gs://${projectId}`,
121
+ `--project=${projectId}`,
122
+ "--format=json",
123
+ ]);
124
+ const corsConfigJson = JSON.parse(corsConfigString);
125
+ const existingCors = corsConfigJson.cors || [];
126
+ const allOrigins = new Set();
127
+ for (const corsEntry of existingCors) {
128
+ if (corsEntry.origin) {
129
+ for (const origin of corsEntry.origin) {
130
+ allOrigins.add(origin);
131
+ }
132
+ }
133
+ }
134
+ allOrigins.add(`https://${options.domain}`);
135
+ const corsToWrite = [
136
+ {
137
+ origin: Array.from(allOrigins),
138
+ method: ["GET"],
139
+ maxAgeSeconds: 3600,
140
+ },
141
+ ];
142
+ await writeFile("cors.json", JSON.stringify(corsToWrite));
143
+ await runChildProcess("gcloud", [
144
+ "storage",
145
+ "buckets",
146
+ "update",
147
+ `gs://${projectId}`,
148
+ "--cors-file",
149
+ "cors.json",
150
+ `--project=${projectId}`,
151
+ "--quiet",
152
+ ]).catch(() => {
153
+ throw new Error("Error updating Cloud Storage CORS.");
154
+ });
155
+ await unlink(join(process.cwd(), "cors.json"));
156
+ }
157
+ };
@@ -0,0 +1,51 @@
1
+ import { runChildProcess } from "@stoker-platform/node-client";
2
+ import { readFile, rm, unlink, writeFile } from "fs/promises";
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ export const deleteProject = async (options) => {
6
+ if (!process.env.GCP_PROJECT) {
7
+ throw new Error("GCP_PROJECT is not set.");
8
+ }
9
+ await runChildProcess("gcloud", ["projects", "delete", process.env.GCP_PROJECT, "--quiet"]).catch(() => {
10
+ throw new Error("Error deleting project.");
11
+ });
12
+ console.log("Project deleted.");
13
+ if (!options.testMode) {
14
+ const projectData = JSON.parse(await readFile(join(process.cwd(), "project-data.json"), "utf8"));
15
+ delete projectData.projects[process.env.GCP_PROJECT];
16
+ projectData.deleted_projects.push(process.env.GCP_PROJECT);
17
+ await writeFile(join(process.cwd(), "project-data.json"), JSON.stringify(projectData, null, 4));
18
+ console.log("Project deleted from project data.");
19
+ }
20
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
21
+ if (existsSync(join(process.cwd(), ".env", `.env.${process.env.GCP_PROJECT}`))) {
22
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
23
+ await unlink(join(process.cwd(), ".env", `.env.${process.env.GCP_PROJECT}`));
24
+ console.log("System environment file deleted.");
25
+ }
26
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
27
+ if (existsSync(join(process.cwd(), ".env", `.env.project.${process.env.GCP_PROJECT}`))) {
28
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
29
+ await unlink(join(process.cwd(), ".env", `.env.project.${process.env.GCP_PROJECT}`));
30
+ console.log("Project environment file deleted.");
31
+ }
32
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
33
+ if (existsSync(join(process.cwd(), ".migration", process.env.GCP_PROJECT))) {
34
+ await rm(join(process.cwd(), ".migration", process.env.GCP_PROJECT), { recursive: true, force: true });
35
+ console.log("Project migration folder deleted.");
36
+ }
37
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
38
+ const firebaserc = JSON.parse(await readFile(join(process.cwd(), ".firebaserc"), "utf8"));
39
+ if (firebaserc.targets) {
40
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41
+ delete firebaserc.targets[process.env.GCP_PROJECT];
42
+ console.log("Project removed from .firebaserc targets.");
43
+ }
44
+ if (firebaserc.etags) {
45
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
46
+ delete firebaserc.etags[process.env.GCP_PROJECT];
47
+ console.log("Project removed from .firebaserc etags.");
48
+ }
49
+ await writeFile(join(process.cwd(), ".firebaserc"), JSON.stringify(firebaserc, null, 2));
50
+ process.exit();
51
+ };
@@ -0,0 +1,11 @@
1
+ import { deleteRecord as deleteStokerRecord, initializeStoker } from "@stoker-platform/node-client";
2
+ import { join } from "path";
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export const deleteRecord = async (options) => {
5
+ await initializeStoker(options.mode || "production", options.tenant, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
6
+ const deletedRecord = await deleteStokerRecord(options.path.split("/"), options.id, options.user, {
7
+ force: options.force,
8
+ });
9
+ console.log(JSON.stringify(deletedRecord, null, 2));
10
+ process.exit();
11
+ };
@@ -0,0 +1,49 @@
1
+ import { fetchCurrentSchema, initializeFirebase } from "@stoker-platform/node-client";
2
+ import { getAuth } from "firebase-admin/auth";
3
+ import { getFirestore } from "firebase-admin/firestore";
4
+ import { getStorage } from "firebase-admin/storage";
5
+ import { getApp } from "firebase-admin/app";
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ export const deleteTenant = async (options) => {
8
+ await initializeFirebase();
9
+ const app = getApp();
10
+ const auth = getAuth();
11
+ const db = getFirestore();
12
+ const storage = getStorage();
13
+ const schema = await fetchCurrentSchema();
14
+ const users = await db.collection("tenants").doc(options.tenant).collection("system_user_permissions").get();
15
+ for (const snapshot of users.docs) {
16
+ await auth.deleteUser(snapshot.id);
17
+ }
18
+ const deleteCollectionRecursively = async (collectionRef) => {
19
+ const snapshots = await collectionRef.get();
20
+ for (const doc of snapshots.docs) {
21
+ const subcollections = await doc.ref.listCollections();
22
+ for (const subcollection of subcollections) {
23
+ await deleteCollectionRecursively(subcollection);
24
+ }
25
+ await doc.ref.delete();
26
+ }
27
+ };
28
+ const tenantRef = db.collection("tenants").doc(options.tenant);
29
+ const subcollections = await tenantRef.listCollections();
30
+ for (const subcollection of subcollections) {
31
+ if (subcollection.id === "system_auto_increment" || subcollection.id === "system_fields") {
32
+ for (const collection of Object.values(schema.collections)) {
33
+ const subcollections = await subcollection.doc(collection.labels.collection).listCollections();
34
+ for (const subcollection of subcollections) {
35
+ await deleteCollectionRecursively(subcollection);
36
+ }
37
+ }
38
+ }
39
+ await deleteCollectionRecursively(subcollection);
40
+ }
41
+ await tenantRef.delete();
42
+ const bucket = storage.bucket(app.options.projectId);
43
+ const [files] = await bucket.getFiles({
44
+ prefix: `${options.tenant}/`,
45
+ });
46
+ await Promise.all(files.map((file) => file.delete()));
47
+ console.log("Tenant deleted.");
48
+ process.exit();
49
+ };
@@ -0,0 +1,25 @@
1
+ import { initializeStoker, getOne as getOneStoker } from "@stoker-platform/node-client";
2
+ import { join } from "path";
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export const getOne = async (options) => {
5
+ await initializeStoker(options.mode || "production", options.tenant, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
6
+ const path = options.path.split("/");
7
+ const id = path.pop();
8
+ const getOneOptions = {};
9
+ if (options.subcollections) {
10
+ getOneOptions.subcollections = {
11
+ depth: options.subcollections,
12
+ };
13
+ }
14
+ if (options.relations) {
15
+ getOneOptions.relations = {
16
+ depth: options.relations,
17
+ };
18
+ }
19
+ if (options.user) {
20
+ getOneOptions.user = options.user;
21
+ }
22
+ const result = await getOneStoker(path, id, getOneOptions);
23
+ console.log(JSON.stringify(result, null, 2));
24
+ process.exit();
25
+ };
@@ -0,0 +1,28 @@
1
+ import { initializeStoker, getSome as getSomeStoker } from "@stoker-platform/node-client";
2
+ import { join } from "path";
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export const getSome = async (options) => {
5
+ await initializeStoker(options.mode || "production", options.tenant, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
6
+ const path = options.path.split("/");
7
+ let constraints = [];
8
+ if (options.constraints) {
9
+ constraints = JSON.parse(options.constraints);
10
+ }
11
+ const getSomeOptions = {};
12
+ if (options.subcollections) {
13
+ getSomeOptions.subcollections = {
14
+ depth: options.subcollections,
15
+ };
16
+ }
17
+ if (options.relations) {
18
+ getSomeOptions.relations = {
19
+ depth: options.relations,
20
+ };
21
+ }
22
+ if (options.user) {
23
+ getSomeOptions.user = options.user;
24
+ }
25
+ const result = await getSomeStoker(path, constraints, getSomeOptions);
26
+ console.log(JSON.stringify(result.docs, null, 2));
27
+ process.exit();
28
+ };
@@ -0,0 +1,16 @@
1
+ import { fileURLToPath } from "url";
2
+ import { resolve, dirname, join } from "path";
3
+ import { existsSync, cpSync, renameSync } from "fs";
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ export const initProject = async (options) => {
6
+ if (existsSync("firebase.json") && !options.force) {
7
+ console.log("Please run this command from an empty directory");
8
+ process.exit();
9
+ }
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ cpSync(resolve(__dirname, "..", "..", "..", "init-files"), process.cwd(), { recursive: true });
13
+ renameSync(join(process.cwd(), ".##gitignore##"), join(process.cwd(), ".gitignore"));
14
+ renameSync(join(process.cwd(), "functions", ".##gitignore##"), join(process.cwd(), "functions", ".gitignore"));
15
+ process.exit();
16
+ };