@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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { initializeFirebase, fetchCurrentSchema, runChildProcess } from "@stoker-platform/node-client";
|
|
2
|
+
export const exportFirestoreData = async () => {
|
|
3
|
+
await initializeFirebase();
|
|
4
|
+
const schema = await fetchCurrentSchema();
|
|
5
|
+
const bucket = process.env.FB_FIRESTORE_EXPORT_BUCKET;
|
|
6
|
+
let collections = [];
|
|
7
|
+
for (let i = 0; i < Object.keys(schema.collections).length; i++) {
|
|
8
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
9
|
+
collections.push(Object.keys(schema.collections)[i]);
|
|
10
|
+
if (i == Object.keys(schema.collections).length - 1 || collections.length == 100) {
|
|
11
|
+
console.log(`Exporting ${collections.length} collections...`);
|
|
12
|
+
try {
|
|
13
|
+
await runChildProcess("gcloud", [
|
|
14
|
+
"firestore",
|
|
15
|
+
"export",
|
|
16
|
+
`gs://${bucket}/`,
|
|
17
|
+
`--collection-ids=${collections.join(",")}`,
|
|
18
|
+
`--project=${process.env.GCP_PROJECT}`,
|
|
19
|
+
"--quiet",
|
|
20
|
+
]);
|
|
21
|
+
collections = [];
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error("Error exporting Firestore data.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
process.exit();
|
|
29
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { runChildProcess, initializeFirebase } from "@stoker-platform/node-client";
|
|
2
|
+
import spawn from "cross-spawn";
|
|
3
|
+
import { generateSchema } from "../schema/generateSchema.js";
|
|
4
|
+
import { lintSchema } from "../../lint/lintSchema.js";
|
|
5
|
+
export const deployTTLs = async () => {
|
|
6
|
+
const firebaseConfigString = process.env.STOKER_FB_WEB_APP_CONFIG;
|
|
7
|
+
if (!firebaseConfigString)
|
|
8
|
+
throw new Error("Firebase web app config not found.");
|
|
9
|
+
const firebaseConfig = JSON.parse(firebaseConfigString);
|
|
10
|
+
const projectId = firebaseConfig.projectId;
|
|
11
|
+
await initializeFirebase();
|
|
12
|
+
const currentSchema = await generateSchema();
|
|
13
|
+
await lintSchema(true);
|
|
14
|
+
const ttlsToAdd = [];
|
|
15
|
+
const ttlsToRemove = [];
|
|
16
|
+
const deployTTL = (collectionName, ttl) => {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
return runChildProcess("gcloud", [
|
|
19
|
+
"firestore",
|
|
20
|
+
"fields",
|
|
21
|
+
"ttls",
|
|
22
|
+
"update",
|
|
23
|
+
ttl,
|
|
24
|
+
`--collection-group=${collectionName}`,
|
|
25
|
+
"--enable-ttl",
|
|
26
|
+
`--project=${projectId}`,
|
|
27
|
+
"--quiet",
|
|
28
|
+
"--async",
|
|
29
|
+
])
|
|
30
|
+
.then(() => resolve())
|
|
31
|
+
.catch((error) => reject(error));
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const removeTTL = (collectionName, ttl) => {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
return runChildProcess("gcloud", [
|
|
37
|
+
"firestore",
|
|
38
|
+
"fields",
|
|
39
|
+
"ttls",
|
|
40
|
+
"update",
|
|
41
|
+
ttl,
|
|
42
|
+
`--collection-group=${collectionName}`,
|
|
43
|
+
"--disable-ttl",
|
|
44
|
+
`--project=${projectId}`,
|
|
45
|
+
"--quiet",
|
|
46
|
+
"--async",
|
|
47
|
+
])
|
|
48
|
+
.then(() => resolve())
|
|
49
|
+
.catch((error) => reject(error));
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const existingTTL = {};
|
|
53
|
+
const firestoreCollections = Object.values(currentSchema.collections)
|
|
54
|
+
.map((collection) => collection.labels.collection)
|
|
55
|
+
.concat("system_write_log");
|
|
56
|
+
for (const collection of firestoreCollections) {
|
|
57
|
+
const spawnPromise = new Promise((resolve, reject) => {
|
|
58
|
+
const childProcess = spawn("gcloud", [
|
|
59
|
+
"firestore",
|
|
60
|
+
"fields",
|
|
61
|
+
"ttls",
|
|
62
|
+
"list",
|
|
63
|
+
`--collection-group=${collection}`,
|
|
64
|
+
`--project=${projectId}`,
|
|
65
|
+
]);
|
|
66
|
+
let stdout = "";
|
|
67
|
+
let stderr = "";
|
|
68
|
+
childProcess.stdout?.on("data", (data) => {
|
|
69
|
+
stdout += data.toString();
|
|
70
|
+
});
|
|
71
|
+
childProcess.stderr?.on("data", (data) => {
|
|
72
|
+
stderr += data.toString();
|
|
73
|
+
});
|
|
74
|
+
childProcess.on("close", (code) => {
|
|
75
|
+
if (code === 0) {
|
|
76
|
+
resolve({ stdout, stderr });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
reject(new Error(`Error retrieving ${collection} TTL`));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
const { stdout, stderr } = await spawnPromise;
|
|
84
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
85
|
+
existingTTL[collection] = stdout;
|
|
86
|
+
if (stderr) {
|
|
87
|
+
console.log(`Retrieve ${collection} TTL: ${stderr}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (currentSchema.config.writeLogTTL) {
|
|
91
|
+
if (!existingTTL["system_write_log"].includes("system_write_log/fields/TTL")) {
|
|
92
|
+
ttlsToAdd.push(deployTTL("system_write_log", "TTL"));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(`TTL already exists for Write Log.\n`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (existingTTL["system_write_log"]) {
|
|
99
|
+
ttlsToRemove.push(removeTTL("system_write_log", "TTL"));
|
|
100
|
+
}
|
|
101
|
+
Object.values(currentSchema.collections).forEach((collection) => {
|
|
102
|
+
const { labels, ttl } = collection;
|
|
103
|
+
const path = `databases/(default)/collectionGroups/${labels.collection}/fields/`;
|
|
104
|
+
if (ttl) {
|
|
105
|
+
if (!existingTTL[labels.collection].includes(`${path}${ttl}\n`)) {
|
|
106
|
+
ttlsToAdd.push(deployTTL(labels.collection, ttl));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.log(`TTL ${ttl} already exists for collection ${labels.collection}.\n`);
|
|
110
|
+
}
|
|
111
|
+
if (existingTTL[labels.collection].includes(path) && !existingTTL[labels.collection].includes(`${ttl}\n`)) {
|
|
112
|
+
ttlsToRemove.push(removeTTL(labels.collection, existingTTL[labels.collection].split(path)[1].split("\n")[0]));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (existingTTL[labels.collection]) {
|
|
116
|
+
ttlsToRemove.push(removeTTL(labels.collection, existingTTL[labels.collection].split(path)[1].split("\n")[0]));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
await Promise.all(ttlsToRemove);
|
|
121
|
+
await Promise.all(ttlsToAdd);
|
|
122
|
+
process.exit();
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
throw new Error("Error deploying TTLs", { cause: error });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fetchCurrentSchema, initializeFirebase } from "@stoker-platform/node-client";
|
|
2
|
+
import { getFirestore, FieldValue } from "firebase-admin/firestore";
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export const liveUpdate = async (options) => {
|
|
5
|
+
await initializeFirebase();
|
|
6
|
+
const db = getFirestore();
|
|
7
|
+
const batch = db.batch();
|
|
8
|
+
const currentSchema = await fetchCurrentSchema();
|
|
9
|
+
const deployId = db.collection("system_deployment").doc("latest_deploy").collection("deploy_history").doc().id;
|
|
10
|
+
const versionInfo = {
|
|
11
|
+
version: currentSchema.version,
|
|
12
|
+
force: options.secure || false,
|
|
13
|
+
refresh: options.refresh || false,
|
|
14
|
+
time: FieldValue.serverTimestamp(),
|
|
15
|
+
payload: options.payload || {},
|
|
16
|
+
};
|
|
17
|
+
batch.set(db.collection("system_deployment").doc("latest_deploy"), versionInfo);
|
|
18
|
+
batch.set(db.collection("system_deployment").doc("latest_deploy").collection("deploy_history").doc(deployId), versionInfo);
|
|
19
|
+
await batch.commit();
|
|
20
|
+
console.info("LIVE UPDATE TRIGGERED");
|
|
21
|
+
process.exit();
|
|
22
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { initializeFirebase } from "@stoker-platform/node-client";
|
|
2
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
3
|
+
export const activateMaintenanceMode = async () => {
|
|
4
|
+
await initializeFirebase();
|
|
5
|
+
const db = getFirestore();
|
|
6
|
+
await db.collection("system_deployment").doc("maintenance_mode").set({ active: true });
|
|
7
|
+
console.info("MAINTENANCE MODE ENGAGED");
|
|
8
|
+
process.exit();
|
|
9
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { initializeFirebase } from "@stoker-platform/node-client";
|
|
2
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
3
|
+
export const disableMaintenanceMode = async () => {
|
|
4
|
+
await initializeFirebase();
|
|
5
|
+
const db = getFirestore();
|
|
6
|
+
await db.collection("system_deployment").doc("maintenance_mode").set({ active: false });
|
|
7
|
+
console.info("MAINTENANCE MODE DISENGAGED");
|
|
8
|
+
process.exit();
|
|
9
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { initializeFirebase } from "@stoker-platform/node-client";
|
|
2
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export const setDeploymentStatus = async (status) => {
|
|
5
|
+
await initializeFirebase();
|
|
6
|
+
const db = getFirestore();
|
|
7
|
+
if (!["idle", "in_progress"].includes(status)) {
|
|
8
|
+
throw new Error("Invalid deployment status");
|
|
9
|
+
}
|
|
10
|
+
await db.runTransaction(async (transaction) => {
|
|
11
|
+
if (status === "in_progress") {
|
|
12
|
+
const deploymentStatus = await transaction.get(db.collection("system_deployment").doc("status"));
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
14
|
+
if (deploymentStatus.exists && deploymentStatus.data().status === "in_progress") {
|
|
15
|
+
throw new Error("Deployment already in progress. Please try again later.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
transaction.set(db.collection("system_deployment").doc("status"), { status });
|
|
19
|
+
});
|
|
20
|
+
console.info(`DEPLOYMENT STATUS SET TO ${status.toUpperCase()}\n`);
|
|
21
|
+
return;
|
|
22
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { generateSchema } from "../schema/generateSchema.js";
|
|
2
|
+
import { lintSchema } from "../../lint/lintSchema.js";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { dirname, resolve } from "path";
|
|
5
|
+
import { writeFileSync } from "fs";
|
|
6
|
+
export const generateFirestoreIndexes = async () => {
|
|
7
|
+
if (!process.env.URL_FIRESTORE_INDEXES) {
|
|
8
|
+
throw new Error("STOKER_FIRESTORE_INDEXES_URL is not set");
|
|
9
|
+
}
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const schema = await generateSchema();
|
|
13
|
+
await lintSchema(true);
|
|
14
|
+
const indexesResponse = await fetch(process.env.URL_FIRESTORE_INDEXES, {
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({ schema }),
|
|
19
|
+
method: "POST",
|
|
20
|
+
});
|
|
21
|
+
const indexes = await indexesResponse.json();
|
|
22
|
+
writeFileSync(resolve(__dirname, process.cwd(), "firebase-rules", "firestore.indexes.json"), JSON.stringify(indexes, null, 4));
|
|
23
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { generateSchema } from "../schema/generateSchema.js";
|
|
2
|
+
import { lintSchema } from "../../lint/lintSchema.js";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { dirname, resolve } from "path";
|
|
5
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
6
|
+
export const generateFirestoreRules = async () => {
|
|
7
|
+
if (!process.env.URL_FIRESTORE_RULES) {
|
|
8
|
+
throw new Error("STOKER_FIRESTORE_RULES_URL is not set");
|
|
9
|
+
}
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const schema = await generateSchema();
|
|
13
|
+
await lintSchema(true);
|
|
14
|
+
const rulesResponse = await fetch(process.env.URL_FIRESTORE_RULES, {
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({ schema }),
|
|
19
|
+
method: "POST",
|
|
20
|
+
});
|
|
21
|
+
let rules = await rulesResponse.text();
|
|
22
|
+
const customRules = readFileSync(resolve(__dirname, process.cwd(), "firebase-rules", "firestore.custom.rules"), "utf8");
|
|
23
|
+
const trimLastTwoBraces = (input) => {
|
|
24
|
+
let trimmed = input.trimEnd();
|
|
25
|
+
let numTrimmed = 0;
|
|
26
|
+
while (numTrimmed < 2 && trimmed.endsWith("}")) {
|
|
27
|
+
trimmed = trimmed.slice(0, -1).trimEnd();
|
|
28
|
+
numTrimmed++;
|
|
29
|
+
}
|
|
30
|
+
return trimmed;
|
|
31
|
+
};
|
|
32
|
+
rules = trimLastTwoBraces(rules);
|
|
33
|
+
rules = `${rules}\n\n // CUSTOM SECURITY RULES\n\n ${customRules.split("\n").join("\n ")}\n }\n}`;
|
|
34
|
+
writeFileSync(resolve(__dirname, process.cwd(), "firebase-rules", "firestore.rules"), rules);
|
|
35
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { generateSchema } from "../schema/generateSchema.js";
|
|
2
|
+
import { lintSchema } from "../../lint/lintSchema.js";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { dirname, resolve } from "path";
|
|
5
|
+
import { writeFileSync } from "fs";
|
|
6
|
+
export const generateStorageRules = async () => {
|
|
7
|
+
if (!process.env.URL_STORAGE_RULES) {
|
|
8
|
+
throw new Error("STOKER_STORAGE_RULES_URL is not set");
|
|
9
|
+
}
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const schema = await generateSchema();
|
|
13
|
+
await lintSchema(true);
|
|
14
|
+
const rulesResponse = await fetch(process.env.URL_STORAGE_RULES, {
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({ schema }),
|
|
19
|
+
method: "POST",
|
|
20
|
+
});
|
|
21
|
+
const rules = await rulesResponse.text();
|
|
22
|
+
writeFileSync(resolve(__dirname, process.cwd(), "firebase-rules", "storage.rules"), rules);
|
|
23
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { tryPromise } from "@stoker-platform/node-client";
|
|
2
|
+
import { getRelationLists, roleHasOperationAccess } from "@stoker-platform/utils";
|
|
3
|
+
import { ServerValue } from "firebase-admin/database";
|
|
4
|
+
import { readdir, readFile } from "fs/promises";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { pathToFileURL } from "url";
|
|
7
|
+
export const generateSchema = async (includeComputedFields = false) => {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const newSchema = {};
|
|
10
|
+
const path = join(process.cwd(), "lib", "main.js");
|
|
11
|
+
const url = pathToFileURL(path).href;
|
|
12
|
+
const globalConfigFile = await import(url);
|
|
13
|
+
const globalConfig = globalConfigFile.default("node");
|
|
14
|
+
const projectData = await readFile(join(process.cwd(), "project-data.json"), "utf8");
|
|
15
|
+
const projectDataJson = JSON.parse(projectData);
|
|
16
|
+
const version = projectDataJson.version || 1;
|
|
17
|
+
newSchema.version = version;
|
|
18
|
+
newSchema.published_time = ServerValue.TIMESTAMP;
|
|
19
|
+
const collections = await readdir(join(process.cwd(), "lib", "collections"));
|
|
20
|
+
const { roles, firebase } = globalConfig;
|
|
21
|
+
const configSchema = {
|
|
22
|
+
roles,
|
|
23
|
+
permissionsIndexExemption: false,
|
|
24
|
+
};
|
|
25
|
+
if (firebase?.permissionsIndexExemption)
|
|
26
|
+
configSchema.permissionsIndexExemption = true;
|
|
27
|
+
firebase?.writeLogIndexExemption
|
|
28
|
+
? (configSchema.writeLogIndexExemption = firebase?.writeLogIndexExemption || [])
|
|
29
|
+
: null;
|
|
30
|
+
firebase?.writeLogTTL ? (configSchema.writeLogTTL = firebase?.writeLogTTL) : null;
|
|
31
|
+
newSchema.config = configSchema;
|
|
32
|
+
const fullSchema = [];
|
|
33
|
+
for (const collection of collections) {
|
|
34
|
+
if (globalConfig.disabledCollections?.includes(collection))
|
|
35
|
+
continue;
|
|
36
|
+
const path = join(process.cwd(), "lib", "collections", collection);
|
|
37
|
+
const url = pathToFileURL(path).href;
|
|
38
|
+
const schema = await import(url);
|
|
39
|
+
fullSchema.push(schema.default("node"));
|
|
40
|
+
}
|
|
41
|
+
newSchema.collections = {};
|
|
42
|
+
for (const collection of collections) {
|
|
43
|
+
if (globalConfig.disabledCollections?.includes(collection))
|
|
44
|
+
continue;
|
|
45
|
+
const path = join(process.cwd(), "lib", "collections", collection);
|
|
46
|
+
const url = pathToFileURL(path).href;
|
|
47
|
+
const schema = await import(url);
|
|
48
|
+
const persistSchema = schema.default("node");
|
|
49
|
+
const { labels, access, preloadCache, admin } = persistSchema;
|
|
50
|
+
const { serverReadOnly } = access;
|
|
51
|
+
if (!includeComputedFields) {
|
|
52
|
+
persistSchema.fields = persistSchema.fields.filter((field) => field.type !== "Computed");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
for (const field of persistSchema.fields) {
|
|
56
|
+
if (field.type === "Computed") {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
delete field.formula;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (admin) {
|
|
63
|
+
const filters = ((await tryPromise(admin.filters)) || []);
|
|
64
|
+
const cards = (await tryPromise(admin.cards));
|
|
65
|
+
const calendar = (await tryPromise(admin.calendar));
|
|
66
|
+
const statusField = (await tryPromise(admin.statusField));
|
|
67
|
+
const readRoles = roles.filter((role) => roleHasOperationAccess(persistSchema, role, "read"));
|
|
68
|
+
const nonStandardRoles = (serverReadOnly || []).concat(preloadCache?.roles || []);
|
|
69
|
+
const standardRoles = readRoles.filter((role) => !nonStandardRoles.includes(role));
|
|
70
|
+
const noPreloadCacheRoles = readRoles.filter((role) => !preloadCache?.roles.includes(role));
|
|
71
|
+
const addUnscheduledRoles = calendar?.unscheduled?.roles && !noPreloadCacheRoles.length;
|
|
72
|
+
if (calendar?.unscheduled) {
|
|
73
|
+
filters.push({
|
|
74
|
+
type: "select",
|
|
75
|
+
field: calendar.startField,
|
|
76
|
+
roles: calendar?.unscheduled.roles,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const relationListFields = getRelationLists(labels.collection, fullSchema);
|
|
80
|
+
if (relationListFields.size > 0) {
|
|
81
|
+
for (const relationListField of relationListFields.values()) {
|
|
82
|
+
const existingRelationFilter = filters.find((filter) => filter.type === "relation" && filter.field === relationListField.field);
|
|
83
|
+
if (existingRelationFilter && existingRelationFilter.type === "relation") {
|
|
84
|
+
if (existingRelationFilter.roles) {
|
|
85
|
+
const newRoles = new Set(existingRelationFilter.roles);
|
|
86
|
+
for (const role of relationListField.roles || noPreloadCacheRoles) {
|
|
87
|
+
if (noPreloadCacheRoles.includes(role)) {
|
|
88
|
+
newRoles.add(role);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
existingRelationFilter.roles = Array.from(newRoles);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
filters.push({
|
|
96
|
+
type: "relation",
|
|
97
|
+
field: relationListField.field,
|
|
98
|
+
roles: relationListField.roles?.filter((role) => noPreloadCacheRoles.includes(role)) ||
|
|
99
|
+
noPreloadCacheRoles,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (statusField || cards?.statusField) {
|
|
105
|
+
const existingStatusFieldIndex = filters.findIndex((filter) => "field" in filter && filter.field === (statusField?.field || cards?.statusField));
|
|
106
|
+
const newFilter = {
|
|
107
|
+
type: "status",
|
|
108
|
+
};
|
|
109
|
+
if (addUnscheduledRoles) {
|
|
110
|
+
newFilter.roles = calendar?.unscheduled?.roles;
|
|
111
|
+
}
|
|
112
|
+
filters.push(newFilter);
|
|
113
|
+
if (existingStatusFieldIndex !== -1) {
|
|
114
|
+
filters.splice(existingStatusFieldIndex, 1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (calendar) {
|
|
118
|
+
const existingRangeIndex = filters.findIndex((filter) => filter.type === "range");
|
|
119
|
+
if (existingRangeIndex === -1) {
|
|
120
|
+
if (noPreloadCacheRoles.length) {
|
|
121
|
+
filters.push({
|
|
122
|
+
type: "range",
|
|
123
|
+
field: calendar.startField,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (noPreloadCacheRoles.length === 0) {
|
|
129
|
+
filters.splice(existingRangeIndex, 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if ((cards || admin.images) && standardRoles.length) {
|
|
134
|
+
filters.push({
|
|
135
|
+
type: "select",
|
|
136
|
+
field: "Last_Save_At",
|
|
137
|
+
roles: standardRoles,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const persistFilters = [];
|
|
141
|
+
filters.forEach((filter) => {
|
|
142
|
+
const persistFilter = {};
|
|
143
|
+
if (filter.type === "status") {
|
|
144
|
+
if (statusField || cards?.statusField) {
|
|
145
|
+
const field = statusField?.field || cards?.statusField;
|
|
146
|
+
if (field) {
|
|
147
|
+
persistFilter.field = field;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
persistFilter.field = filter.field;
|
|
153
|
+
}
|
|
154
|
+
if ("roles" in filter && filter.roles?.length) {
|
|
155
|
+
persistFilter.roles = filter.roles;
|
|
156
|
+
}
|
|
157
|
+
if (addUnscheduledRoles) {
|
|
158
|
+
if (persistFilter.roles?.length) {
|
|
159
|
+
persistFilter.roles = persistFilter.roles?.filter((role) => calendar.unscheduled?.roles?.includes(role));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
persistFilter.roles = calendar.unscheduled?.roles;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (filter.type === "range") {
|
|
166
|
+
persistFilter.range = true;
|
|
167
|
+
}
|
|
168
|
+
if (filter.type !== "status" && filter.field === "Last_Save_At" && filter.type === "select") {
|
|
169
|
+
persistFilter.standalone = true;
|
|
170
|
+
}
|
|
171
|
+
persistFilters.push(persistFilter);
|
|
172
|
+
});
|
|
173
|
+
persistSchema.queries = persistFilters;
|
|
174
|
+
}
|
|
175
|
+
delete persistSchema.custom;
|
|
176
|
+
delete persistSchema.admin;
|
|
177
|
+
for (const field of persistSchema.fields) {
|
|
178
|
+
delete field.custom;
|
|
179
|
+
delete field.admin;
|
|
180
|
+
}
|
|
181
|
+
newSchema.collections[collection.split(".")[0]] = persistSchema;
|
|
182
|
+
}
|
|
183
|
+
return newSchema;
|
|
184
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getDatabase } from "firebase-admin/database";
|
|
2
|
+
import { initializeFirebase } from "@stoker-platform/node-client";
|
|
3
|
+
import { generateSchema } from "./generateSchema.js";
|
|
4
|
+
import { lintSchema } from "../../lint/lintSchema.js";
|
|
5
|
+
export const persistSchema = async () => {
|
|
6
|
+
await initializeFirebase();
|
|
7
|
+
const rtdb = getDatabase();
|
|
8
|
+
const ref = rtdb.ref("schema");
|
|
9
|
+
const schema = await generateSchema(true);
|
|
10
|
+
await lintSchema(true);
|
|
11
|
+
await ref.push(schema);
|
|
12
|
+
console.info("Schema persisted successfully.");
|
|
13
|
+
process.exit();
|
|
14
|
+
};
|