@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,124 @@
|
|
|
1
|
+
import { fetchCurrentSchema, initializeStoker } from "@stoker-platform/node-client";
|
|
2
|
+
import { getDependencyIndexFields, getLowercaseFields, getRoleGroups, getSingleFieldRelations, isDependencyField, isRelationField, } from "@stoker-platform/utils";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import isEqual from "lodash/isEqual.js";
|
|
5
|
+
import isEmpty from "lodash/isEmpty.js";
|
|
6
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
export const auditDenormalized = async (options) => {
|
|
9
|
+
await initializeStoker(options.mode || "production", options.tenant, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
|
|
10
|
+
const schema = await fetchCurrentSchema();
|
|
11
|
+
const db = getFirestore();
|
|
12
|
+
for (const [collectionName, collectionSchema] of Object.entries(schema.collections)) {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
const collectionData = {};
|
|
15
|
+
console.log(`Loading ${collectionName}...`);
|
|
16
|
+
const collectionSnapshot = await db.collectionGroup(collectionName).get();
|
|
17
|
+
console.log(`Auditing ${collectionName}...`);
|
|
18
|
+
collectionSnapshot.forEach((doc) => {
|
|
19
|
+
if (!doc.ref.path.includes(`tenants/${options.tenant}`)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
collectionData[doc.id] = doc.data();
|
|
23
|
+
});
|
|
24
|
+
const singleFieldRelations = getSingleFieldRelations(collectionSchema, collectionSchema.fields);
|
|
25
|
+
const singleFieldRelationNames = Array.from(singleFieldRelations).map((field) => field.name);
|
|
26
|
+
for (const field of collectionSchema.fields) {
|
|
27
|
+
if (isDependencyField(field, collectionSchema, schema)) {
|
|
28
|
+
const dependencySnapshot = await db
|
|
29
|
+
.collection("tenants")
|
|
30
|
+
.doc(options.tenant)
|
|
31
|
+
.collection("system_fields")
|
|
32
|
+
.doc(collectionName)
|
|
33
|
+
.collection(`${collectionName}-${field.name}`)
|
|
34
|
+
.get();
|
|
35
|
+
dependencySnapshot.forEach((dependency) => {
|
|
36
|
+
const dependencyData = dependency.data();
|
|
37
|
+
const indexFields = getDependencyIndexFields(field, collectionSchema, schema);
|
|
38
|
+
if (!indexFields.some((indexField) => indexField.name === field.name))
|
|
39
|
+
indexFields.push(field);
|
|
40
|
+
for (const indexField of indexFields) {
|
|
41
|
+
if (indexField.name === "Collection_Path_String")
|
|
42
|
+
continue;
|
|
43
|
+
if (isRelationField(indexField)) {
|
|
44
|
+
if (!getDependencyIndexFields(field, collectionSchema, schema).includes(indexField)) {
|
|
45
|
+
const dependencyValue = dependencyData[indexField.name];
|
|
46
|
+
const collectionValue = collectionData[dependency.id]?.[indexField.name];
|
|
47
|
+
if (!isEqual(dependencyValue, collectionValue)) {
|
|
48
|
+
console.log(`${collectionName} ${dependency.id} ${field.name} Dependency: ${indexField.name} - ${JSON.stringify(dependencyValue)} !== ${JSON.stringify(collectionValue)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const dependencyValue = dependencyData[`${indexField.name}_Array`];
|
|
52
|
+
const collectionValue = collectionData[dependency.id]?.[`${indexField.name}_Array`];
|
|
53
|
+
if (!(isEmpty(dependencyValue) && isEmpty(collectionValue)) &&
|
|
54
|
+
!isEqual(dependencyValue, collectionValue)) {
|
|
55
|
+
console.log(`${collectionName} ${dependency.id} ${field.name} Dependency: ${indexField.name} - ${JSON.stringify(dependencyValue)} !== ${JSON.stringify(collectionValue)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const dependencyValue = dependencyData[indexField.name];
|
|
60
|
+
const collectionValue = collectionData[dependency.id]?.[indexField.name];
|
|
61
|
+
if (!isEqual(dependencyValue, collectionValue)) {
|
|
62
|
+
console.log(`${collectionName} ${dependency.id} ${field.name} Dependency: ${indexField.name} - ${JSON.stringify(dependencyValue)} !== ${JSON.stringify(collectionValue)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const roleGroups = getRoleGroups(collectionSchema, schema);
|
|
70
|
+
for (const roleGroup of roleGroups) {
|
|
71
|
+
const dependencySnapshot = await db
|
|
72
|
+
.collection("tenants")
|
|
73
|
+
.doc(options.tenant)
|
|
74
|
+
.collection("system_fields")
|
|
75
|
+
.doc(collectionName)
|
|
76
|
+
.collection(`${collectionName}-${roleGroup.key}`)
|
|
77
|
+
.get();
|
|
78
|
+
dependencySnapshot.forEach((dependency) => {
|
|
79
|
+
const dependencyData = dependency.data();
|
|
80
|
+
const lowercaseFields = getLowercaseFields(collectionSchema, roleGroup.fields);
|
|
81
|
+
for (const indexField of roleGroup.fields) {
|
|
82
|
+
if (indexField.name === "Collection_Path_String")
|
|
83
|
+
continue;
|
|
84
|
+
if (isRelationField(indexField)) {
|
|
85
|
+
const dependencyValue = {
|
|
86
|
+
[indexField.name]: dependencyData[indexField.name],
|
|
87
|
+
[`${indexField.name}_Array`]: dependencyData[`${indexField.name}_Array`],
|
|
88
|
+
};
|
|
89
|
+
const collectionValue = {
|
|
90
|
+
[indexField.name]: collectionData[dependency.id]?.[indexField.name],
|
|
91
|
+
[`${indexField.name}_Array`]: collectionData[dependency.id]?.[`${indexField.name}_Array`],
|
|
92
|
+
};
|
|
93
|
+
if (singleFieldRelationNames.includes(indexField.name)) {
|
|
94
|
+
dependencyValue[`${indexField.name}_Single`] = dependencyData[`${indexField.name}_Single`];
|
|
95
|
+
collectionValue[`${indexField.name}_Single`] =
|
|
96
|
+
collectionData[dependency.id]?.[`${indexField.name}_Single`];
|
|
97
|
+
}
|
|
98
|
+
if (!isEqual(dependencyValue, collectionValue)) {
|
|
99
|
+
console.log(`${collectionName} ${dependency.id} Private ${roleGroup.key}: ${indexField.name} - ${JSON.stringify(dependencyValue)} !== ${JSON.stringify(collectionValue)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const dependencyValue = dependencyData[indexField.name];
|
|
104
|
+
const collectionValue = collectionData[dependency.id]?.[indexField.name];
|
|
105
|
+
if (!isEqual(dependencyValue, collectionValue)) {
|
|
106
|
+
console.log(`${collectionName} ${dependency.id} Private ${roleGroup.key}: ${indexField.name} - ${JSON.stringify(dependencyValue)} !== ${JSON.stringify(collectionValue)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (Array.from(lowercaseFields)
|
|
110
|
+
.map((field) => field.name)
|
|
111
|
+
.includes(indexField.name)) {
|
|
112
|
+
const dependencyValue = dependencyData[`${indexField.name}_Lowercase`];
|
|
113
|
+
const collectionValue = collectionData[dependency.id]?.[`${indexField.name}_Lowercase`];
|
|
114
|
+
if (!isEqual(dependencyValue, collectionValue)) {
|
|
115
|
+
console.log(`${collectionName} ${dependency.id} Private ${roleGroup.key}: ${indexField.name}_Lowercase - ${JSON.stringify(dependencyValue)} !== ${JSON.stringify(collectionValue)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
console.log(`${collectionName} audited.\n`);
|
|
122
|
+
}
|
|
123
|
+
process.exit();
|
|
124
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { fetchCurrentSchema, initializeStoker } from "@stoker-platform/node-client";
|
|
2
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
export const auditPermissions = async (options) => {
|
|
6
|
+
await initializeStoker(options.mode || "production", options.tenant, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
|
|
7
|
+
const schema = await fetchCurrentSchema();
|
|
8
|
+
const db = getFirestore();
|
|
9
|
+
const dbMain = getFirestore();
|
|
10
|
+
const mismatches = [];
|
|
11
|
+
const permissions = await db.collection("tenants").doc(options.tenant).collection("system_user_permissions").get();
|
|
12
|
+
for (const authCollection of Object.values(schema.collections)) {
|
|
13
|
+
if (authCollection.auth) {
|
|
14
|
+
for (const doc of permissions.docs) {
|
|
15
|
+
const permission = doc.data();
|
|
16
|
+
if (permission.Collection !== authCollection.labels.collection)
|
|
17
|
+
continue;
|
|
18
|
+
for (const collection of Object.values(schema.collections)) {
|
|
19
|
+
const { labels, access } = collection;
|
|
20
|
+
if (access.auth) {
|
|
21
|
+
if (!access.auth.includes(permission.Role) && permission.collections[labels.collection]?.auth) {
|
|
22
|
+
mismatches.push(`*** User ${doc.id} has excess auth permission for ${labels.collection} collection ***`);
|
|
23
|
+
}
|
|
24
|
+
if (access.auth.includes(permission.Role) && !permission.collections[labels.collection]?.auth) {
|
|
25
|
+
mismatches.push(`User ${doc.id} is missing auth permission for ${labels.collection} collection`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const operation of Object.entries(access.operations)) {
|
|
29
|
+
const [operationName, operationValue] = operation;
|
|
30
|
+
if (operationName !== "assignable") {
|
|
31
|
+
const operationUpper = operationName.charAt(0).toUpperCase() + operationName.slice(1);
|
|
32
|
+
if (operationValue.includes(permission.Role) &&
|
|
33
|
+
!permission.collections[labels.collection]?.operations?.includes(operationUpper)) {
|
|
34
|
+
mismatches.push(`User ${doc.id} is missing ${operationUpper} permission for ${labels.collection} collection`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (permission.collections[labels.collection]?.operations) {
|
|
39
|
+
for (const operation of permission.collections[labels.collection].operations) {
|
|
40
|
+
const accessOperation = access.operations[operation.toLowerCase()];
|
|
41
|
+
if (accessOperation && !accessOperation?.includes(permission.Role)) {
|
|
42
|
+
mismatches.push(`User ${doc.id} has excess ${operation} permission for ${labels.collection} collection`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (access.attributeRestrictions) {
|
|
47
|
+
for (const restriction of access.attributeRestrictions) {
|
|
48
|
+
for (const restrictionRole of restriction.roles) {
|
|
49
|
+
if (restrictionRole.role === permission.Role) {
|
|
50
|
+
let assignmentType = undefined;
|
|
51
|
+
if (restriction.type === "Record_Owner") {
|
|
52
|
+
assignmentType = "recordOwner";
|
|
53
|
+
}
|
|
54
|
+
else if (restriction.type === "Record_User") {
|
|
55
|
+
assignmentType = "recordUser";
|
|
56
|
+
}
|
|
57
|
+
else if (restriction.type === "Record_Property") {
|
|
58
|
+
assignmentType = "recordProperty";
|
|
59
|
+
}
|
|
60
|
+
if (!restrictionRole.assignable &&
|
|
61
|
+
assignmentType &&
|
|
62
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
63
|
+
!permission.collections[labels.collection]?.[assignmentType]?.active) {
|
|
64
|
+
mismatches.push(`User ${doc.id} is missing ${restriction.type} attribute restriction for ${labels.collection} collection`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (access.entityRestrictions?.assignable?.includes(permission.Role))
|
|
71
|
+
continue;
|
|
72
|
+
const hasEntityRestriction = access.entityRestrictions?.restrictions?.some((entityRestriction) => entityRestriction.roles.some((role) => role.role === permission.Role));
|
|
73
|
+
if (hasEntityRestriction && !permission.collections?.[labels.collection]?.restrictEntities) {
|
|
74
|
+
mismatches.push(`User ${doc.id} is missing entity restriction for ${labels.collection} collection`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
console.log(mismatches.join("\n\n"));
|
|
81
|
+
if (options.email && mismatches.length > 0) {
|
|
82
|
+
await dbMain.collection("system_mail").add({
|
|
83
|
+
to: options.email,
|
|
84
|
+
message: {
|
|
85
|
+
subject: `Stoker Permissions Audit`,
|
|
86
|
+
text: mismatches.join("\n\n"),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
console.log(`Email sent to ${options.email}`);
|
|
90
|
+
}
|
|
91
|
+
process.exit();
|
|
92
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { fetchCurrentSchema, getFirestorePathRef, initializeStoker } from "@stoker-platform/node-client";
|
|
2
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import isEqual from "lodash/isEqual.js";
|
|
5
|
+
import { getField, getLowercaseFields, getSingleFieldRelations } from "@stoker-platform/utils";
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
export const auditRelations = async (options) => {
|
|
8
|
+
await initializeStoker(options.mode || "production", options.tenant, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
|
|
9
|
+
const schema = await fetchCurrentSchema();
|
|
10
|
+
const db = getFirestore();
|
|
11
|
+
for (const [collectionName, collectionSchema] of Object.entries(schema.collections)) {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
const collectionData = {};
|
|
14
|
+
console.log(`Loading ${collectionName}...`);
|
|
15
|
+
const collectionSnapshot = await db.collectionGroup(collectionName).get();
|
|
16
|
+
console.log(`Auditing ${collectionName}...`);
|
|
17
|
+
collectionSnapshot.forEach((doc) => {
|
|
18
|
+
if (!doc.ref.path.includes(`tenants/${options.tenant}`)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
collectionData[doc.id] = doc.data();
|
|
22
|
+
});
|
|
23
|
+
const singleFieldRelations = getSingleFieldRelations(collectionSchema, collectionSchema.fields);
|
|
24
|
+
const singleFieldRelationNames = Array.from(singleFieldRelations).map((field) => field.name);
|
|
25
|
+
for (const doc of collectionSnapshot.docs) {
|
|
26
|
+
if (!doc.ref.path.includes(`tenants/${options.tenant}`)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const record = doc.data();
|
|
30
|
+
for (const field of collectionSchema.fields) {
|
|
31
|
+
if ("collection" in field) {
|
|
32
|
+
field.includeFields ||= [];
|
|
33
|
+
field.includeFields.push("Collection_Path");
|
|
34
|
+
field.includeFields.push("deleted");
|
|
35
|
+
const relationCollection = schema.collections[field.collection];
|
|
36
|
+
if (record[field.name]) {
|
|
37
|
+
for (const relationRecord of Object.entries(record[field.name])) {
|
|
38
|
+
const [id, relation] = relationRecord;
|
|
39
|
+
const mainRelation = relation;
|
|
40
|
+
const ref = getFirestorePathRef(db, mainRelation.Collection_Path, options.tenant);
|
|
41
|
+
const sourceRef = await ref.doc(id).get();
|
|
42
|
+
const source = sourceRef.data();
|
|
43
|
+
if (!source) {
|
|
44
|
+
if (field.preserve) {
|
|
45
|
+
for (const includeField of field.includeFields) {
|
|
46
|
+
const includeFieldsSchema = [];
|
|
47
|
+
field.includeFields.forEach((includeField) => {
|
|
48
|
+
if (includeField !== "Collection_Path" && includeField !== "deleted") {
|
|
49
|
+
const field = getField(relationCollection.fields, includeField);
|
|
50
|
+
includeFieldsSchema.push(field);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const relationLowercaseFields = getLowercaseFields(relationCollection, includeFieldsSchema);
|
|
54
|
+
const fieldsToRemove = Object.keys(mainRelation).filter((key) => !field.includeFields?.includes(key) &&
|
|
55
|
+
!(key.endsWith("_Lowercase") &&
|
|
56
|
+
Array.from(relationLowercaseFields)
|
|
57
|
+
.map((field) => field.name)
|
|
58
|
+
.includes(key.replace("_Lowercase", ""))));
|
|
59
|
+
if (includeField === "deleted" && !mainRelation.deleted) {
|
|
60
|
+
console.log(`${collectionName} ${doc.id} - Relation ${id} in field ${field.name} does not have "deleted" property`);
|
|
61
|
+
}
|
|
62
|
+
if (includeField !== "deleted" &&
|
|
63
|
+
Array.from(relationLowercaseFields)
|
|
64
|
+
.map((field) => field.name)
|
|
65
|
+
.includes(includeField)) {
|
|
66
|
+
if (mainRelation[`${includeField}_Lowercase`] !==
|
|
67
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
68
|
+
mainRelation[includeField]?.toLowerCase()) {
|
|
69
|
+
console.log(
|
|
70
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
71
|
+
`${collectionName} ${doc.id} - Field ${field.name} ${includeField}_Lowercase ${mainRelation[`${includeField}_Lowercase`]} !== ${mainRelation[includeField]?.toLowerCase()}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const removeField of fieldsToRemove) {
|
|
75
|
+
console.log(
|
|
76
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
77
|
+
`${collectionName} ${doc.id} - Field ${field.name} ${removeField} is not in include fields`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(`${collectionName} ${doc.id} - Source record at ${mainRelation.Collection_Path.join("/")}/${id} not found`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (field.twoWay) {
|
|
88
|
+
const sourceField = relationCollection.fields.find((sourceField) => sourceField.name === field.twoWay);
|
|
89
|
+
if (!sourceField) {
|
|
90
|
+
console.log(`${collectionName} ${doc.id} - Two way field ${field.twoWay} not found in source collection`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
94
|
+
const sourceRelation = source?.[sourceField.name]?.[doc.id];
|
|
95
|
+
if (!((sourceRelation &&
|
|
96
|
+
(source?.[`${sourceField.name}_Array`] || []).includes(doc.id) &&
|
|
97
|
+
(record?.[`${field.name}_Array`] || []).includes(id)) ||
|
|
98
|
+
("preserve" in sourceField &&
|
|
99
|
+
sourceField.preserve &&
|
|
100
|
+
sourceRelation?.deleted &&
|
|
101
|
+
(source?.[`${sourceField.name}_Array`] || []).includes(doc.id)))) {
|
|
102
|
+
console.log(`${collectionName} ${doc.id} - Invalid two way relation ${field.name} ${id} found in record ${id} in source collection ${field.collection}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!record[`${field.name}_Array`]?.includes(id)) {
|
|
107
|
+
console.log(`${collectionName} ${doc.id} - Field ${field.name} ${id} not found in ${field.name}_Array`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (singleFieldRelationNames.includes(field.name)) {
|
|
111
|
+
if (!isEqual(record[`${field.name}_Single`], mainRelation)) {
|
|
112
|
+
console.log(`${collectionName} ${doc.id} - Field ${field.name} does not have a single relation`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const includeField of field.includeFields) {
|
|
117
|
+
const relationCollection = schema.collections[field.collection];
|
|
118
|
+
let lowercaseFields = new Set();
|
|
119
|
+
if (includeField !== "Collection_Path" && includeField !== "deleted") {
|
|
120
|
+
const includeFieldSchema = getField(relationCollection.fields, includeField);
|
|
121
|
+
lowercaseFields = getLowercaseFields(relationCollection, [includeFieldSchema]);
|
|
122
|
+
}
|
|
123
|
+
if (includeField !== "deleted") {
|
|
124
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
125
|
+
if (!isEqual(mainRelation[includeField], source[includeField])) {
|
|
126
|
+
console.log(
|
|
127
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
128
|
+
`${collectionName} ${doc.id} - Field ${field.name} ${includeField} ${mainRelation[includeField]} !== ${source[includeField]}`);
|
|
129
|
+
}
|
|
130
|
+
if (lowercaseFields.size === 1) {
|
|
131
|
+
if (!isEqual(mainRelation[`${includeField}_Lowercase`], source[`${includeField}_Lowercase`])) {
|
|
132
|
+
console.log(
|
|
133
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
134
|
+
`${collectionName} ${doc.id} - Field ${field.name} ${includeField}_Lowercase ${mainRelation[`${includeField}_Lowercase`]} !== ${source[`${includeField}_Lowercase`]}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (singleFieldRelationNames.includes(field.name)) {
|
|
138
|
+
if (!isEqual(
|
|
139
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
140
|
+
record[`${field.name}_Single`]?.[includeField],
|
|
141
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
142
|
+
source[includeField])) {
|
|
143
|
+
console.log(
|
|
144
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
145
|
+
`${collectionName} ${doc.id} - Field ${field.name}_Single ${includeField} ${record[`${field.name}_Single`]?.[includeField]} !== ${source[includeField]}`);
|
|
146
|
+
}
|
|
147
|
+
if (lowercaseFields.size === 1) {
|
|
148
|
+
if (!isEqual(record[`${field.name}_Single`]?.[`${includeField}_Lowercase`], source[`${includeField}_Lowercase`])) {
|
|
149
|
+
console.log(
|
|
150
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
151
|
+
`${collectionName} ${doc.id} - Field ${field.name}_Single ${includeField}_Lowercase ${record[`${field.name}_Single`]?.[`${includeField}_Lowercase`]} !== ${source[`${includeField}_Lowercase`]}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
if (mainRelation.deleted) {
|
|
158
|
+
console.log(`${collectionName} ${doc.id} - Field ${field.name} has invalid deleted property`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const includeFieldsSchema = [];
|
|
163
|
+
field.includeFields.forEach((includeField) => {
|
|
164
|
+
if (includeField !== "Collection_Path" && includeField !== "deleted") {
|
|
165
|
+
const field = getField(relationCollection.fields, includeField);
|
|
166
|
+
includeFieldsSchema.push(field);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const relationLowercaseFields = getLowercaseFields(relationCollection, includeFieldsSchema);
|
|
170
|
+
const fieldsToRemove = Object.keys(mainRelation).filter((key) => !field.includeFields?.includes(key) &&
|
|
171
|
+
!(key.endsWith("_Lowercase") &&
|
|
172
|
+
Array.from(relationLowercaseFields)
|
|
173
|
+
.map((field) => field.name)
|
|
174
|
+
.includes(key.replace("_Lowercase", ""))));
|
|
175
|
+
for (const removeField of fieldsToRemove) {
|
|
176
|
+
console.log(`${collectionName} ${doc.id} - Field ${field.name} ${removeField} is not in include fields`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
console.log(`${collectionName} audited.\n`);
|
|
184
|
+
}
|
|
185
|
+
process.exit();
|
|
186
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fetchCurrentSchema, getCollectionRefs, initializeStoker } from "@stoker-platform/node-client";
|
|
2
|
+
import { tryPromise, getRange } from "@stoker-platform/utils";
|
|
3
|
+
import { Filter, getFirestore } from "firebase-admin/firestore";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
export const explainPreloadQueries = async (options) => {
|
|
7
|
+
const { getGlobalConfigModule } = await initializeStoker("production", options.tenant, join(process.cwd(), "lib", "main.js"), join(process.cwd(), "lib", "collections"));
|
|
8
|
+
const globalConfig = getGlobalConfigModule();
|
|
9
|
+
const schema = await fetchCurrentSchema();
|
|
10
|
+
const db = getFirestore();
|
|
11
|
+
const permissionsSnapshot = await db
|
|
12
|
+
.collection("tenants")
|
|
13
|
+
.doc(options.tenant)
|
|
14
|
+
.collection("system_user_permissions")
|
|
15
|
+
.doc(options.id)
|
|
16
|
+
.get();
|
|
17
|
+
if (!permissionsSnapshot.exists) {
|
|
18
|
+
throw new Error("User not found");
|
|
19
|
+
}
|
|
20
|
+
const permissions = permissionsSnapshot.data();
|
|
21
|
+
const timezone = await tryPromise(globalConfig.timezone);
|
|
22
|
+
const preloadConfigSync = await tryPromise(globalConfig.preload?.sync);
|
|
23
|
+
const preloadConfig = await tryPromise(globalConfig.preload?.async);
|
|
24
|
+
const preloadCollections = [];
|
|
25
|
+
if (preloadConfigSync)
|
|
26
|
+
preloadCollections.push(...preloadConfigSync);
|
|
27
|
+
if (preloadConfig)
|
|
28
|
+
preloadCollections.push(...preloadConfig);
|
|
29
|
+
if (!preloadCollections.length)
|
|
30
|
+
process.exit();
|
|
31
|
+
for (const collection of preloadCollections) {
|
|
32
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
33
|
+
const collectionSchema = schema.collections[collection];
|
|
34
|
+
const { preloadCache } = collectionSchema;
|
|
35
|
+
if (!preloadCache?.roles.includes(permissions?.Role))
|
|
36
|
+
continue;
|
|
37
|
+
const rangeConstraints = preloadCache?.range;
|
|
38
|
+
const constraints = (await tryPromise(collectionSchema.custom?.preloadCacheConstraints));
|
|
39
|
+
const orQueries = (await tryPromise(collectionSchema.custom?.preloadCacheOrQueries));
|
|
40
|
+
const queries = getCollectionRefs(options.tenant, [collection], schema, options.id, permissions).map((ref) => {
|
|
41
|
+
const disjunctions = [];
|
|
42
|
+
if (rangeConstraints) {
|
|
43
|
+
const { start, end } = getRange(rangeConstraints, timezone);
|
|
44
|
+
const rangeQueries = rangeConstraints.fields.map((field) => {
|
|
45
|
+
return Filter.and(Filter.where(field, ">=", start), Filter.where(field, "<=", end));
|
|
46
|
+
});
|
|
47
|
+
disjunctions.push(Filter.and(Filter.or(...rangeQueries)));
|
|
48
|
+
}
|
|
49
|
+
if (orQueries) {
|
|
50
|
+
disjunctions.push(Filter.and(Filter.or(...orQueries.map((constraint) => Filter.where(...constraint)))));
|
|
51
|
+
}
|
|
52
|
+
if (constraints) {
|
|
53
|
+
disjunctions.push(Filter.and(...constraints.map((constraint) => Filter.where(...constraint))));
|
|
54
|
+
}
|
|
55
|
+
return ref.where(Filter.and(...disjunctions));
|
|
56
|
+
});
|
|
57
|
+
console.log(`${collection}:`);
|
|
58
|
+
for (const query of queries) {
|
|
59
|
+
const metrics = await query.explain({ analyze: options.analyze });
|
|
60
|
+
console.log(JSON.stringify(metrics.metrics));
|
|
61
|
+
}
|
|
62
|
+
console.log("\n");
|
|
63
|
+
}
|
|
64
|
+
process.exit();
|
|
65
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { initializeFirebase } from "@stoker-platform/node-client";
|
|
2
|
+
import { getAuth } from "firebase-admin/auth";
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export const getUser = async (options) => {
|
|
5
|
+
await initializeFirebase();
|
|
6
|
+
const auth = getAuth();
|
|
7
|
+
const user = await auth.getUser(options.id);
|
|
8
|
+
console.log(JSON.stringify(user));
|
|
9
|
+
process.exit();
|
|
10
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
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 getUserPermissions = async (options) => {
|
|
5
|
+
await initializeFirebase();
|
|
6
|
+
const db = getFirestore();
|
|
7
|
+
const usersRef = await db
|
|
8
|
+
.collection("tenants")
|
|
9
|
+
.doc(options.tenant)
|
|
10
|
+
.collection("system_user_permissions")
|
|
11
|
+
.doc(options.id)
|
|
12
|
+
.get();
|
|
13
|
+
if (!usersRef.exists) {
|
|
14
|
+
console.log("User not found");
|
|
15
|
+
process.exit();
|
|
16
|
+
}
|
|
17
|
+
console.log(JSON.stringify(usersRef.data()));
|
|
18
|
+
process.exit();
|
|
19
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
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 getUserRecord = async (options) => {
|
|
5
|
+
await initializeFirebase();
|
|
6
|
+
const db = getFirestore();
|
|
7
|
+
const usersRef = await db
|
|
8
|
+
.collection("tenants")
|
|
9
|
+
.doc(options.tenant)
|
|
10
|
+
.collection(options.collection)
|
|
11
|
+
.where("User_ID", "==", options.id)
|
|
12
|
+
.get();
|
|
13
|
+
if (usersRef.empty) {
|
|
14
|
+
console.log("User not found");
|
|
15
|
+
process.exit();
|
|
16
|
+
}
|
|
17
|
+
const user = usersRef.docs[0].data();
|
|
18
|
+
console.log(JSON.stringify(user));
|
|
19
|
+
process.exit();
|
|
20
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { initializeFirebase, runChildProcess } from "@stoker-platform/node-client";
|
|
2
|
+
export const listProjects = async () => {
|
|
3
|
+
await initializeFirebase();
|
|
4
|
+
await runChildProcess("gcloud", ["projects", "list"]).catch(() => {
|
|
5
|
+
throw new Error("Error getting Google Cloud projects.");
|
|
6
|
+
});
|
|
7
|
+
process.exit();
|
|
8
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { fetchCurrentSchema, initializeFirebase } from "@stoker-platform/node-client";
|
|
2
|
+
import { getAuth } from "firebase-admin/auth";
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export const setUserCollection = async (options) => {
|
|
5
|
+
await initializeFirebase();
|
|
6
|
+
const schema = await fetchCurrentSchema();
|
|
7
|
+
if (!Object.keys(schema.collections).includes(options.collection))
|
|
8
|
+
throw new Error(`Collection "${options.collection}" does not exist.`);
|
|
9
|
+
const auth = getAuth();
|
|
10
|
+
const user = await auth.getUser(options.id);
|
|
11
|
+
await auth.setCustomUserClaims(options.id, { ...user.customClaims, collection: options.collection });
|
|
12
|
+
console.log("User collection updated successfully.");
|
|
13
|
+
process.exit();
|
|
14
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { initializeFirebase } from "@stoker-platform/node-client";
|
|
2
|
+
import { getAuth } from "firebase-admin/auth";
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export const setUserDocument = async (options) => {
|
|
5
|
+
await initializeFirebase();
|
|
6
|
+
const auth = getAuth();
|
|
7
|
+
const user = await auth.getUser(options.id);
|
|
8
|
+
await auth.setCustomUserClaims(options.id, { ...user.customClaims, doc: options.doc });
|
|
9
|
+
console.log("User document updated successfully.");
|
|
10
|
+
process.exit();
|
|
11
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { fetchCurrentSchema, initializeFirebase } from "@stoker-platform/node-client";
|
|
2
|
+
import { getAuth } from "firebase-admin/auth";
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export const setUserRole = async (options) => {
|
|
5
|
+
await initializeFirebase();
|
|
6
|
+
const schema = await fetchCurrentSchema();
|
|
7
|
+
if (!schema.config.roles.includes(options.role))
|
|
8
|
+
throw new Error(`Role "${options.role}" does not exist.`);
|
|
9
|
+
const auth = getAuth();
|
|
10
|
+
const user = await auth.getUser(options.id);
|
|
11
|
+
await auth.setCustomUserClaims(options.id, { ...user.customClaims, role: options.role });
|
|
12
|
+
console.log("User role updated successfully.");
|
|
13
|
+
process.exit();
|
|
14
|
+
};
|