@stoker-platform/utils 0.2.1
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/lib/src/access/collection.d.ts +9 -0
- package/lib/src/access/collection.js +59 -0
- package/lib/src/access/document.d.ts +3 -0
- package/lib/src/access/document.js +191 -0
- package/lib/src/access/getCollectionRestrictions.d.ts +7 -0
- package/lib/src/access/getCollectionRestrictions.js +71 -0
- package/lib/src/access/getRecordSubcollections.d.ts +2 -0
- package/lib/src/access/getRecordSubcollections.js +15 -0
- package/lib/src/access/getRelatedCollections.d.ts +2 -0
- package/lib/src/access/getRelatedCollections.js +24 -0
- package/lib/src/access/hasDependencyAccess.d.ts +2 -0
- package/lib/src/access/hasDependencyAccess.js +24 -0
- package/lib/src/access/isPaginationEnabled.d.ts +2 -0
- package/lib/src/access/isPaginationEnabled.js +35 -0
- package/lib/src/access/permissions.d.ts +2 -0
- package/lib/src/access/permissions.js +543 -0
- package/lib/src/access/read/getOne.d.ts +2 -0
- package/lib/src/access/read/getOne.js +19 -0
- package/lib/src/access/read/getSome.d.ts +2 -0
- package/lib/src/access/read/getSome.js +21 -0
- package/lib/src/access/roleHasOperationAccess.d.ts +2 -0
- package/lib/src/access/roleHasOperationAccess.js +7 -0
- package/lib/src/access/write/addRecord.d.ts +2 -0
- package/lib/src/access/write/addRecord.js +40 -0
- package/lib/src/access/write/deleteRecord.d.ts +2 -0
- package/lib/src/access/write/deleteRecord.js +26 -0
- package/lib/src/access/write/updateRecord.d.ts +2 -0
- package/lib/src/access/write/updateRecord.js +61 -0
- package/lib/src/getConfigValue.d.ts +12 -0
- package/lib/src/getConfigValue.js +83 -0
- package/lib/src/getCustomization.d.ts +4 -0
- package/lib/src/getCustomization.js +29 -0
- package/lib/src/getFieldCustomization.d.ts +7 -0
- package/lib/src/getFieldCustomization.js +3 -0
- package/lib/src/main.d.ts +60 -0
- package/lib/src/main.js +60 -0
- package/lib/src/operations/addInitialValues.d.ts +2 -0
- package/lib/src/operations/addInitialValues.js +27 -0
- package/lib/src/operations/addLowercaseFields.d.ts +2 -0
- package/lib/src/operations/addLowercaseFields.js +12 -0
- package/lib/src/operations/addRelationArrays.d.ts +2 -0
- package/lib/src/operations/addRelationArrays.js +60 -0
- package/lib/src/operations/addSystemFields.d.ts +2 -0
- package/lib/src/operations/addSystemFields.js +17 -0
- package/lib/src/operations/getDateRange.d.ts +5 -0
- package/lib/src/operations/getDateRange.js +56 -0
- package/lib/src/operations/getExtendedSchema.d.ts +2 -0
- package/lib/src/operations/getExtendedSchema.js +23 -0
- package/lib/src/operations/getFinalRecord.d.ts +1 -0
- package/lib/src/operations/getFinalRecord.js +11 -0
- package/lib/src/operations/getInputSchema.d.ts +15 -0
- package/lib/src/operations/getInputSchema.js +352 -0
- package/lib/src/operations/getLowercaseFields.d.ts +2 -0
- package/lib/src/operations/getLowercaseFields.js +15 -0
- package/lib/src/operations/getSingleFieldRelations.d.ts +2 -0
- package/lib/src/operations/getSingleFieldRelations.js +27 -0
- package/lib/src/operations/getZodSchema.d.ts +15 -0
- package/lib/src/operations/getZodSchema.js +303 -0
- package/lib/src/operations/isDeleteSentinel.d.ts +1 -0
- package/lib/src/operations/isDeleteSentinel.js +4 -0
- package/lib/src/operations/isSortingEnabled.d.ts +2 -0
- package/lib/src/operations/isSortingEnabled.js +7 -0
- package/lib/src/operations/isValidUniqueFieldValue.d.ts +1 -0
- package/lib/src/operations/isValidUniqueFieldValue.js +25 -0
- package/lib/src/operations/parseDate.d.ts +1 -0
- package/lib/src/operations/parseDate.js +4 -0
- package/lib/src/operations/prepareDenormalized.d.ts +8 -0
- package/lib/src/operations/prepareDenormalized.js +312 -0
- package/lib/src/operations/removeDeleteSentinels.d.ts +2 -0
- package/lib/src/operations/removeDeleteSentinels.js +15 -0
- package/lib/src/operations/removeDeletedFields.d.ts +2 -0
- package/lib/src/operations/removeDeletedFields.js +14 -0
- package/lib/src/operations/removeEmptyStrings.d.ts +2 -0
- package/lib/src/operations/removeEmptyStrings.js +14 -0
- package/lib/src/operations/removePrivateFields.d.ts +2 -0
- package/lib/src/operations/removePrivateFields.js +14 -0
- package/lib/src/operations/removeUndefined.d.ts +2 -0
- package/lib/src/operations/removeUndefined.js +14 -0
- package/lib/src/operations/retryOperation.d.ts +1 -0
- package/lib/src/operations/retryOperation.js +21 -0
- package/lib/src/operations/runHooks.d.ts +17 -0
- package/lib/src/operations/runHooks.js +18 -0
- package/lib/src/operations/sanitizeDownloadFilename.d.ts +1 -0
- package/lib/src/operations/sanitizeDownloadFilename.js +18 -0
- package/lib/src/operations/sanitizeEmailInput.d.ts +5 -0
- package/lib/src/operations/sanitizeEmailInput.js +73 -0
- package/lib/src/operations/updateFieldReference.d.ts +2 -0
- package/lib/src/operations/updateFieldReference.js +14 -0
- package/lib/src/operations/validateRecord.d.ts +2 -0
- package/lib/src/operations/validateRecord.js +18 -0
- package/lib/src/operations/validateStorageName.d.ts +1 -0
- package/lib/src/operations/validateStorageName.js +19 -0
- package/lib/src/schema/getAccessFields.d.ts +2 -0
- package/lib/src/schema/getAccessFields.js +62 -0
- package/lib/src/schema/getCollection.d.ts +2 -0
- package/lib/src/schema/getCollection.js +3 -0
- package/lib/src/schema/getDependencyFields.d.ts +6 -0
- package/lib/src/schema/getDependencyFields.js +22 -0
- package/lib/src/schema/getField.d.ts +2 -0
- package/lib/src/schema/getField.js +3 -0
- package/lib/src/schema/getFieldNames.d.ts +2 -0
- package/lib/src/schema/getFieldNames.js +3 -0
- package/lib/src/schema/getIndexFields.d.ts +9 -0
- package/lib/src/schema/getIndexFields.js +184 -0
- package/lib/src/schema/getInverseRelationType.d.ts +1 -0
- package/lib/src/schema/getInverseRelationType.js +15 -0
- package/lib/src/schema/getPathCollections.d.ts +2 -0
- package/lib/src/schema/getPathCollections.js +12 -0
- package/lib/src/schema/getRecordSystemFields.d.ts +2 -0
- package/lib/src/schema/getRecordSystemFields.js +11 -0
- package/lib/src/schema/getRelationLists.d.ts +5 -0
- package/lib/src/schema/getRelationLists.js +13 -0
- package/lib/src/schema/getSubcollections.d.ts +2 -0
- package/lib/src/schema/getSubcollections.js +9 -0
- package/lib/src/schema/getSystemFieldsSchema.d.ts +2 -0
- package/lib/src/schema/getSystemFieldsSchema.js +52 -0
- package/lib/src/schema/isDependencyField.d.ts +2 -0
- package/lib/src/schema/isDependencyField.js +18 -0
- package/lib/src/schema/isIncludedField.d.ts +2 -0
- package/lib/src/schema/isIncludedField.js +18 -0
- package/lib/src/schema/isRelationField.d.ts +2 -0
- package/lib/src/schema/isRelationField.js +3 -0
- package/lib/src/schema/system-fields.d.ts +2 -0
- package/lib/src/schema/system-fields.js +13 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { isRelationField } from "../schema/isRelationField.js";
|
|
2
|
+
import { getDependencyIndexFields, getRoleExcludedFields } from "../schema/getIndexFields.js";
|
|
3
|
+
import { isDependencyField } from "../schema/isDependencyField.js";
|
|
4
|
+
import { getFieldNames } from "../schema/getFieldNames.js";
|
|
5
|
+
import { getField } from "../schema/getField.js";
|
|
6
|
+
import { isDeleteSentinel } from "./isDeleteSentinel.js";
|
|
7
|
+
import { removeDeleteSentinels } from "./removeDeleteSentinels.js";
|
|
8
|
+
import { getSingleFieldRelations } from "./getSingleFieldRelations.js";
|
|
9
|
+
import { getLowercaseFields } from "./getLowercaseFields.js";
|
|
10
|
+
import { getRecordSystemFields } from "../schema/getRecordSystemFields.js";
|
|
11
|
+
export const prepareDenormalized = (operation,
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
batch, path, docId, record, schema, collectionSchema,
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
options, allRoleGroups, arrayUnion, arrayRemove, deleteField, dependencyRef, uniqueRef, privateRef, twoWayIncludeRef, twoWayDependencyRef, twoWayPrivateRef, originalRecord, noDelete, batchSize) => {
|
|
16
|
+
const { fields } = collectionSchema;
|
|
17
|
+
fields
|
|
18
|
+
.filter((field) => "unique" in field && field.unique)
|
|
19
|
+
.forEach((field) => {
|
|
20
|
+
if (operation !== "delete" &&
|
|
21
|
+
(typeof record[field.name] === "string" || typeof record[field.name] === "number")) {
|
|
22
|
+
batch.set(uniqueRef(field, record[field.name].toString().toLowerCase().replace(/\s/g, "---").replaceAll("/", "|||")), {
|
|
23
|
+
id: docId,
|
|
24
|
+
Collection_Path: path,
|
|
25
|
+
});
|
|
26
|
+
if (batchSize)
|
|
27
|
+
batchSize.size++;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
fields.forEach((field) => {
|
|
31
|
+
if (isDependencyField(field, collectionSchema, schema)) {
|
|
32
|
+
const dependencyFieldsSchema = getDependencyIndexFields(field, collectionSchema, schema);
|
|
33
|
+
const dependencyFields = {};
|
|
34
|
+
if (record[field.name] !== undefined) {
|
|
35
|
+
dependencyFields[field.name] = record[field.name];
|
|
36
|
+
if (isRelationField(field)) {
|
|
37
|
+
dependencyFields[`${field.name}_Array`] = record[`${field.name}_Array`];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
dependencyFieldsSchema.forEach((dependencyField) => {
|
|
41
|
+
if (record[dependencyField.name] !== undefined) {
|
|
42
|
+
if (isRelationField(dependencyField)) {
|
|
43
|
+
dependencyFields[`${dependencyField.name}_Array`] = record[`${dependencyField.name}_Array`];
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
dependencyFields[dependencyField.name] = record[dependencyField.name];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (Object.keys(dependencyFields).length > 0) {
|
|
51
|
+
if (operation === "create") {
|
|
52
|
+
dependencyFields.Collection_Path = path;
|
|
53
|
+
dependencyFields.Collection_Path_String = path.join("/");
|
|
54
|
+
batch.set(dependencyRef(field), dependencyFields);
|
|
55
|
+
}
|
|
56
|
+
if (operation === "update") {
|
|
57
|
+
batch.update(dependencyRef(field), dependencyFields);
|
|
58
|
+
}
|
|
59
|
+
if (operation === "delete") {
|
|
60
|
+
batch.delete(dependencyRef(field));
|
|
61
|
+
}
|
|
62
|
+
if (batchSize)
|
|
63
|
+
batchSize.size++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const roleGroups = allRoleGroups[collectionSchema.labels.collection];
|
|
68
|
+
for (const group of roleGroups) {
|
|
69
|
+
const excludedFields = getRoleExcludedFields(group, collectionSchema);
|
|
70
|
+
const recordToUpdate = { ...record };
|
|
71
|
+
excludedFields.forEach((field) => {
|
|
72
|
+
delete recordToUpdate[field.name];
|
|
73
|
+
delete recordToUpdate[`${field.name}_Array`];
|
|
74
|
+
delete recordToUpdate[`${field.name}_Single`];
|
|
75
|
+
delete recordToUpdate[`${field.name}_Lowercase`];
|
|
76
|
+
});
|
|
77
|
+
if (Object.keys(recordToUpdate).length > 0) {
|
|
78
|
+
if (operation === "create") {
|
|
79
|
+
recordToUpdate.Collection_Path ||= path;
|
|
80
|
+
recordToUpdate.Collection_Path_String = path.join("/");
|
|
81
|
+
batch.set(privateRef(group.key), recordToUpdate);
|
|
82
|
+
}
|
|
83
|
+
if (operation === "update") {
|
|
84
|
+
batch.update(privateRef(group.key), recordToUpdate);
|
|
85
|
+
}
|
|
86
|
+
if (operation === "delete") {
|
|
87
|
+
batch.delete(privateRef(group.key));
|
|
88
|
+
}
|
|
89
|
+
if (batchSize)
|
|
90
|
+
batchSize.size++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const getDeleteFieldBatchSize = (targetSchema, targetField) => {
|
|
94
|
+
let batchSize = 1;
|
|
95
|
+
if (isDependencyField(targetField, targetSchema, schema)) {
|
|
96
|
+
batchSize++;
|
|
97
|
+
}
|
|
98
|
+
targetSchema.fields.forEach((targetSchemaField) => {
|
|
99
|
+
if (isDependencyField(targetSchemaField, targetSchema, schema)) {
|
|
100
|
+
const targetIndexFields = JSON.parse(getFieldNames(getDependencyIndexFields(targetSchemaField, targetSchema, schema)));
|
|
101
|
+
if (targetIndexFields.includes(targetField.name)) {
|
|
102
|
+
batchSize++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const targetRoleGroups = allRoleGroups[targetSchema.labels.collection];
|
|
107
|
+
for (const group of targetRoleGroups) {
|
|
108
|
+
if (group.fields.some((groupField) => groupField.name === targetField.name) &&
|
|
109
|
+
isRelationField(targetField)) {
|
|
110
|
+
batchSize++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return batchSize;
|
|
114
|
+
};
|
|
115
|
+
const deleteFields = (field, targetSchema, targetField, id, relationPath) => {
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
+
const fieldUpdate = {};
|
|
118
|
+
fieldUpdate[`${targetField.name}.${docId}`] = deleteField();
|
|
119
|
+
fieldUpdate[`${targetField.name}_Array`] = arrayRemove(docId);
|
|
120
|
+
fieldUpdate[`${targetField.name}_Single`] = deleteField();
|
|
121
|
+
batch.update(twoWayIncludeRef(relationPath, id), fieldUpdate);
|
|
122
|
+
if (isDependencyField(targetField, targetSchema, schema)) {
|
|
123
|
+
batch.update(twoWayDependencyRef(field, targetField.name, id), {
|
|
124
|
+
[`${targetField.name}.${docId}`]: deleteField(),
|
|
125
|
+
[`${targetField.name}_Array`]: arrayRemove(docId),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
targetSchema.fields.forEach((targetSchemaField) => {
|
|
129
|
+
if (isDependencyField(targetSchemaField, targetSchema, schema)) {
|
|
130
|
+
const targetIndexFields = JSON.parse(getFieldNames(getDependencyIndexFields(targetSchemaField, targetSchema, schema)));
|
|
131
|
+
if (targetIndexFields.includes(targetField.name)) {
|
|
132
|
+
batch.update(twoWayDependencyRef(field, targetSchemaField.name, id), {
|
|
133
|
+
[`${targetField.name}_Array`]: arrayRemove(docId),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const targetRoleGroups = allRoleGroups[targetSchema.labels.collection];
|
|
139
|
+
for (const group of targetRoleGroups) {
|
|
140
|
+
if (group.fields.some((groupField) => groupField.name === targetField.name) &&
|
|
141
|
+
isRelationField(targetField)) {
|
|
142
|
+
batch.update(twoWayPrivateRef(field, group.key, id), fieldUpdate);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
if (operation === "delete")
|
|
147
|
+
return;
|
|
148
|
+
const twoWayFields = fields.filter((field) => isRelationField(field) && field.twoWay);
|
|
149
|
+
if (options?.noTwoWay)
|
|
150
|
+
return;
|
|
151
|
+
for (const field of twoWayFields) {
|
|
152
|
+
if (!batchSize)
|
|
153
|
+
throw new Error("VALIDATION_ERROR: batchSize is required");
|
|
154
|
+
const targetSchema = schema.collections[field.collection];
|
|
155
|
+
const targetField = getField(targetSchema.fields, field.twoWay);
|
|
156
|
+
const targetSingleFieldRelations = getSingleFieldRelations(targetSchema, targetSchema.fields);
|
|
157
|
+
const targetSingleFieldRelationsNames = Array.from(targetSingleFieldRelations).map((field) => field.name);
|
|
158
|
+
if (!targetField)
|
|
159
|
+
throw new Error(`SCHEMA_ERROR: Field ${field.twoWay} not found in collection ${field.collection}`);
|
|
160
|
+
if (isRelationField(targetField)) {
|
|
161
|
+
if (record[`${field.name}_Array`]) {
|
|
162
|
+
for (const [id, relation] of Object.entries(record[field.name])) {
|
|
163
|
+
if (operation === "update" && originalRecord && originalRecord[`${field.name}_Array`]?.includes(id))
|
|
164
|
+
continue;
|
|
165
|
+
const finalRecord = { ...originalRecord, ...record };
|
|
166
|
+
removeDeleteSentinels(finalRecord);
|
|
167
|
+
const includeFields = {};
|
|
168
|
+
if (targetField.includeFields) {
|
|
169
|
+
targetField.includeFields.forEach((includeField) => {
|
|
170
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
171
|
+
if (finalRecord[includeField] !== undefined) {
|
|
172
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
173
|
+
includeFields[includeField] = finalRecord[includeField];
|
|
174
|
+
const includeFieldSchema = getField(collectionSchema.fields, includeField);
|
|
175
|
+
const lowercaseFields = getLowercaseFields(collectionSchema, [includeFieldSchema]);
|
|
176
|
+
if (lowercaseFields.size === 1) {
|
|
177
|
+
includeFields[`${includeField}_Lowercase`] =
|
|
178
|
+
finalRecord[`${includeField}_Lowercase`];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
184
|
+
const fieldUpdate = {
|
|
185
|
+
[`${field.twoWay}.${docId}`]: {
|
|
186
|
+
Collection_Path: path,
|
|
187
|
+
...includeFields,
|
|
188
|
+
},
|
|
189
|
+
[`${field.twoWay}_Array`]: arrayUnion(docId),
|
|
190
|
+
};
|
|
191
|
+
if (targetSingleFieldRelationsNames.includes(targetField.name)) {
|
|
192
|
+
fieldUpdate[`${field.twoWay}_Single`] = {
|
|
193
|
+
Collection_Path: path,
|
|
194
|
+
...includeFields,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const systemFields = getRecordSystemFields(record);
|
|
198
|
+
batch.update(twoWayIncludeRef(relation.Collection_Path, id), {
|
|
199
|
+
...fieldUpdate,
|
|
200
|
+
...systemFields,
|
|
201
|
+
});
|
|
202
|
+
batchSize.size++;
|
|
203
|
+
if (isDependencyField(targetField, targetSchema, schema)) {
|
|
204
|
+
batch.update(twoWayDependencyRef(field, targetField.name, id), {
|
|
205
|
+
[`${targetField.name}.${docId}`]: {
|
|
206
|
+
Collection_Path: path,
|
|
207
|
+
...includeFields,
|
|
208
|
+
},
|
|
209
|
+
[`${targetField.name}_Array`]: arrayUnion(docId),
|
|
210
|
+
});
|
|
211
|
+
batchSize.size++;
|
|
212
|
+
}
|
|
213
|
+
targetSchema.fields.forEach((targetSchemaField) => {
|
|
214
|
+
if (isDependencyField(targetSchemaField, targetSchema, schema)) {
|
|
215
|
+
const targetIndexFields = JSON.parse(getFieldNames(getDependencyIndexFields(targetSchemaField, targetSchema, schema)));
|
|
216
|
+
const dependencyFieldUpdate = {};
|
|
217
|
+
if (targetIndexFields.includes(targetField.name)) {
|
|
218
|
+
dependencyFieldUpdate[`${field.twoWay}_Array`] = arrayUnion(docId);
|
|
219
|
+
}
|
|
220
|
+
Object.keys(systemFields).forEach((systemField) => {
|
|
221
|
+
if (targetIndexFields.includes(systemField)) {
|
|
222
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
223
|
+
dependencyFieldUpdate[systemField] = systemFields[systemField];
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
if (Object.keys(dependencyFieldUpdate).length > 0) {
|
|
227
|
+
batch.update(twoWayDependencyRef(field, targetSchemaField.name, id), dependencyFieldUpdate);
|
|
228
|
+
batchSize.size++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const targetRoleGroups = allRoleGroups[targetSchema.labels.collection];
|
|
233
|
+
for (const group of targetRoleGroups) {
|
|
234
|
+
if (group.fields.some((groupField) => groupField.name === targetField.name)) {
|
|
235
|
+
const groupFieldUpdate = { ...fieldUpdate };
|
|
236
|
+
Object.keys(systemFields).forEach((systemField) => {
|
|
237
|
+
if (group.fields.some((groupField) => groupField.name === systemField)) {
|
|
238
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
239
|
+
groupFieldUpdate[systemField] = systemFields[systemField];
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
if (Object.keys(groupFieldUpdate).length > 0) {
|
|
243
|
+
batch.update(twoWayPrivateRef(field, group.key, id), groupFieldUpdate);
|
|
244
|
+
batchSize.size++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else
|
|
252
|
+
throw new Error(`SCHEMA_ERROR: Invalid field type: ${targetField.type}`);
|
|
253
|
+
}
|
|
254
|
+
if (batchSize && batchSize.size > 500) {
|
|
255
|
+
throw new Error(`VALIDATION_ERROR: The number of operations in the Firestore transaction has exceeded the limit of 500. This is likely due to a large number of two way updates, roles, dependencies on the collection, unique field checks, entity restrictions (in permissions when dealing with user collections) or relation hierarchy checks.`);
|
|
256
|
+
}
|
|
257
|
+
for (const field of twoWayFields) {
|
|
258
|
+
if (!batchSize)
|
|
259
|
+
throw new Error("VALIDATION_ERROR: batchSize is required");
|
|
260
|
+
const targetSchema = schema.collections[field.collection];
|
|
261
|
+
const targetField = getField(targetSchema.fields, field.twoWay);
|
|
262
|
+
if (!targetField)
|
|
263
|
+
throw new Error(`SCHEMA_ERROR: Field ${field.twoWay} not found in collection ${field.collection}`);
|
|
264
|
+
if (isRelationField(targetField)) {
|
|
265
|
+
if (operation === "update") {
|
|
266
|
+
if (originalRecord &&
|
|
267
|
+
!(record[field.name] && isDeleteSentinel(record[field.name])) &&
|
|
268
|
+
record[`${field.name}_Array`] &&
|
|
269
|
+
originalRecord[`${field.name}_Array`]?.length > 0) {
|
|
270
|
+
for (const [id, relation] of Object.entries(originalRecord[field.name])) {
|
|
271
|
+
if (!record[`${field.name}_Array`].includes(id) && !noDelete?.get(field.name)?.includes(id)) {
|
|
272
|
+
batchSize.size += getDeleteFieldBatchSize(targetSchema, targetField);
|
|
273
|
+
if (batchSize.size <= 500) {
|
|
274
|
+
deleteFields(field, targetSchema, targetField, id, relation.Collection_Path);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else
|
|
282
|
+
throw new Error(`SCHEMA_ERROR: Invalid field type: ${targetField.type}`);
|
|
283
|
+
}
|
|
284
|
+
for (const field of twoWayFields) {
|
|
285
|
+
if (!batchSize)
|
|
286
|
+
throw new Error("VALIDATION_ERROR: batchSize is required");
|
|
287
|
+
const targetSchema = schema.collections[field.collection];
|
|
288
|
+
const targetField = getField(targetSchema.fields, field.twoWay);
|
|
289
|
+
if (!targetField)
|
|
290
|
+
throw new Error(`SCHEMA_ERROR: Field ${field.twoWay} not found in collection ${field.collection}`);
|
|
291
|
+
if (isRelationField(targetField)) {
|
|
292
|
+
if (operation === "update") {
|
|
293
|
+
if (originalRecord &&
|
|
294
|
+
record[field.name] &&
|
|
295
|
+
isDeleteSentinel(record[field.name]) &&
|
|
296
|
+
originalRecord[`${field.name}_Array`]?.length > 0) {
|
|
297
|
+
for (const [id, relation] of Object.entries(originalRecord[field.name])) {
|
|
298
|
+
if (!noDelete?.get(field.name)?.includes(id)) {
|
|
299
|
+
batchSize.size += getDeleteFieldBatchSize(targetSchema, targetField);
|
|
300
|
+
if (batchSize.size <= 500) {
|
|
301
|
+
deleteFields(field, targetSchema, targetField, id, relation.Collection_Path);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else
|
|
309
|
+
throw new Error(`SCHEMA_ERROR: Invalid field type: ${targetField.type}`);
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isDeleteSentinel } from "./isDeleteSentinel.js";
|
|
2
|
+
/* eslint-disable security/detect-object-injection */
|
|
3
|
+
export const removeDeleteSentinels = (obj) => {
|
|
4
|
+
for (const key in obj) {
|
|
5
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
if (isDeleteSentinel(obj[key])) {
|
|
9
|
+
delete obj[key];
|
|
10
|
+
}
|
|
11
|
+
else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
12
|
+
removeDeleteSentinels(obj[key]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const removeDeletedFields = (document, fieldReferences) => {
|
|
2
|
+
Object.keys(document).forEach((key) => {
|
|
3
|
+
let hasReference = false;
|
|
4
|
+
fieldReferences.forEach((fieldReference) => {
|
|
5
|
+
if (fieldReference.has(key)) {
|
|
6
|
+
hasReference = true;
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
if (!hasReference) {
|
|
10
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
11
|
+
delete document[key];
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* eslint-disable security/detect-object-injection */
|
|
2
|
+
export const removeEmptyStrings = (record) => {
|
|
3
|
+
for (const key in record) {
|
|
4
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
5
|
+
continue;
|
|
6
|
+
}
|
|
7
|
+
if (record[key] === "") {
|
|
8
|
+
delete record[key];
|
|
9
|
+
}
|
|
10
|
+
else if (typeof record[key] === "object" && record[key] !== null) {
|
|
11
|
+
removeEmptyStrings(record[key]);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getField } from "../schema/getField.js";
|
|
2
|
+
import cloneDeep from "lodash/cloneDeep.js";
|
|
3
|
+
export const removePrivateFields = (record, schema) => {
|
|
4
|
+
const { fields } = schema;
|
|
5
|
+
const privateFieldsRemoved = cloneDeep(record);
|
|
6
|
+
Object.keys(record).filter((key) => {
|
|
7
|
+
const field = getField(fields, key);
|
|
8
|
+
if (field?.access) {
|
|
9
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
10
|
+
delete privateFieldsRemoved[key];
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return privateFieldsRemoved;
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* eslint-disable security/detect-object-injection */
|
|
2
|
+
export const removeUndefined = (record) => {
|
|
3
|
+
for (const key in record) {
|
|
4
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
5
|
+
continue;
|
|
6
|
+
}
|
|
7
|
+
if (record[key] === undefined) {
|
|
8
|
+
delete record[key];
|
|
9
|
+
}
|
|
10
|
+
else if (typeof record[key] === "object" && record[key] !== null) {
|
|
11
|
+
removeUndefined(record[key]);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const retryOperation: (callback: any, args: unknown[], errorCallback?: any, delay?: number) => Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
|
+
export const retryOperation = async (callback, args, errorCallback, delay = 1000) => {
|
|
3
|
+
let retries = 5;
|
|
4
|
+
while (retries > 0) {
|
|
5
|
+
try {
|
|
6
|
+
await callback(...args);
|
|
7
|
+
break;
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
retries--;
|
|
11
|
+
if (retries === 0) {
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
if (errorCallback) {
|
|
15
|
+
errorCallback(error);
|
|
16
|
+
}
|
|
17
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
18
|
+
delay *= 2;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CollectionCustomization, PreOperationHookArgs, PreWriteHookArgs, PostWriteHookArgs, PostOperationHookArgs, PreDuplicateHookArgs, PostWriteErrorHookArgs, PreReadHookArgs, PostReadHookArgs, GlobalConfig, PreFileAddHookArgs, PreFileUpdateHookArgs, PostFileAddHookArgs, PostFileUpdateHookArgs } from "@stoker-platform/types";
|
|
2
|
+
export declare function runHooks(hookName: "preOperation", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PreOperationHookArgs): Promise<void>;
|
|
3
|
+
export declare function runHooks(hookName: "preRead", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PreReadHookArgs): Promise<void>;
|
|
4
|
+
export declare function runHooks(hookName: "postRead", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PostReadHookArgs): Promise<void>;
|
|
5
|
+
export declare function runHooks(hookName: "preOperation", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PreOperationHookArgs): Promise<void>;
|
|
6
|
+
export declare function runHooks(hookName: "preDuplicate", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PreDuplicateHookArgs): Promise<void>;
|
|
7
|
+
export declare function runHooks(hookName: "preWrite", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PreWriteHookArgs): Promise<void>;
|
|
8
|
+
export declare function runHooks(hookName: "postWrite", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PostWriteHookArgs): Promise<void>;
|
|
9
|
+
export declare function runHooks(hookName: "postWriteError", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PostWriteErrorHookArgs): Promise<{
|
|
10
|
+
resolved: boolean;
|
|
11
|
+
retry?: boolean;
|
|
12
|
+
} | void>;
|
|
13
|
+
export declare function runHooks(hookName: "postOperation", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PostOperationHookArgs): Promise<void>;
|
|
14
|
+
export declare function runHooks(hookName: "preFileAdd", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PreFileAddHookArgs): Promise<void>;
|
|
15
|
+
export declare function runHooks(hookName: "preFileUpdate", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PreFileUpdateHookArgs): Promise<void>;
|
|
16
|
+
export declare function runHooks(hookName: "postFileAdd", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PostFileAddHookArgs): Promise<void>;
|
|
17
|
+
export declare function runHooks(hookName: "postFileUpdate", globalConfig: GlobalConfig, customization: CollectionCustomization, args?: PostFileUpdateHookArgs): Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { tryPromise } from "../getConfigValue.js";
|
|
2
|
+
const hook = async (name, callback, args) => {
|
|
3
|
+
if (callback) {
|
|
4
|
+
const value = await tryPromise(callback, args);
|
|
5
|
+
if (value === false)
|
|
6
|
+
throw new Error(`CANCELLED: Operation cancelled by ${name}`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
export async function runHooks(hookName, globalConfig, customization, args) {
|
|
11
|
+
const value = await hook(hookName, customization.custom?.[hookName], args);
|
|
12
|
+
for (const field of customization.fields) {
|
|
13
|
+
await hook(hookName, field.custom?.[hookName], args);
|
|
14
|
+
}
|
|
15
|
+
await hook(hookName, globalConfig?.[hookName], args);
|
|
16
|
+
if (hookName === "postWriteError")
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sanitizeDownloadFilename: (filename: string) => string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { validateStorageName } from "./validateStorageName.js";
|
|
2
|
+
export const sanitizeDownloadFilename = (filename) => {
|
|
3
|
+
if (!filename || typeof filename !== "string") {
|
|
4
|
+
return "download";
|
|
5
|
+
}
|
|
6
|
+
// Remove control characters
|
|
7
|
+
// eslint-disable-next-line no-control-regex
|
|
8
|
+
let cleaned = filename.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
|
|
9
|
+
cleaned = cleaned.trim();
|
|
10
|
+
if (!cleaned) {
|
|
11
|
+
return "download";
|
|
12
|
+
}
|
|
13
|
+
const validationError = validateStorageName(cleaned);
|
|
14
|
+
if (validationError) {
|
|
15
|
+
return "download";
|
|
16
|
+
}
|
|
17
|
+
return cleaned;
|
|
18
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const sanitizeEmailAddress: (input: string | undefined | null) => string;
|
|
2
|
+
export declare const sanitizeEmailAddressArray: (input: string | string[] | undefined | null) => string[];
|
|
3
|
+
export declare const sanitizeEmailAddressOrArray: (input: string | string[] | undefined | null) => string | string[];
|
|
4
|
+
export declare const sanitizeEmailSubject: (input: string | undefined | null) => string;
|
|
5
|
+
export declare const sanitizeEmailBody: (input: string | undefined | null) => string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const sanitizeEmailAddress = (input) => {
|
|
2
|
+
if (!input || typeof input !== "string") {
|
|
3
|
+
return "";
|
|
4
|
+
}
|
|
5
|
+
// Remove newlines and carriage returns that could be used for SMTP injection
|
|
6
|
+
let sanitized = input.replace(/[\r\n]/g, "");
|
|
7
|
+
// Remove null bytes
|
|
8
|
+
sanitized = sanitized.replace(/\0/g, "");
|
|
9
|
+
// Remove control characters (except tab)
|
|
10
|
+
// eslint-disable-next-line no-control-regex
|
|
11
|
+
sanitized = sanitized.replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "");
|
|
12
|
+
sanitized = sanitized.trim();
|
|
13
|
+
// Limit length to prevent buffer overflow attacks
|
|
14
|
+
const maxLength = 254; // RFC 5321 maximum email length
|
|
15
|
+
if (sanitized.length > maxLength) {
|
|
16
|
+
throw new Error("Email address is too long");
|
|
17
|
+
}
|
|
18
|
+
return sanitized;
|
|
19
|
+
};
|
|
20
|
+
export const sanitizeEmailAddressArray = (input) => {
|
|
21
|
+
if (!input) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const array = Array.isArray(input) ? input : [input];
|
|
25
|
+
return array.map((email) => sanitizeEmailAddress(email)).filter((email) => email.length > 0);
|
|
26
|
+
};
|
|
27
|
+
export const sanitizeEmailAddressOrArray = (input) => {
|
|
28
|
+
if (!input) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(input)) {
|
|
32
|
+
const sanitized = input.map((email) => sanitizeEmailAddress(email)).filter((email) => email.length > 0);
|
|
33
|
+
return sanitized;
|
|
34
|
+
}
|
|
35
|
+
const sanitized = sanitizeEmailAddress(input);
|
|
36
|
+
return sanitized || "";
|
|
37
|
+
};
|
|
38
|
+
export const sanitizeEmailSubject = (input) => {
|
|
39
|
+
if (!input || typeof input !== "string") {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
// Remove newlines and carriage returns that could be used for header injection
|
|
43
|
+
let sanitized = input.replace(/[\r\n]/g, " ");
|
|
44
|
+
// Remove null bytes
|
|
45
|
+
sanitized = sanitized.replace(/\0/g, "");
|
|
46
|
+
// Remove control characters (except tab)
|
|
47
|
+
// eslint-disable-next-line no-control-regex
|
|
48
|
+
sanitized = sanitized.replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "");
|
|
49
|
+
sanitized = sanitized.trim();
|
|
50
|
+
// Limit length to prevent buffer overflow attacks
|
|
51
|
+
const maxLength = 998; // RFC 5322 maximum header line length
|
|
52
|
+
if (sanitized.length > maxLength) {
|
|
53
|
+
throw new Error("Email subject is too long");
|
|
54
|
+
}
|
|
55
|
+
return sanitized;
|
|
56
|
+
};
|
|
57
|
+
export const sanitizeEmailBody = (input) => {
|
|
58
|
+
if (!input || typeof input !== "string") {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
// Remove null bytes
|
|
62
|
+
let sanitized = input.replace(/\0/g, "");
|
|
63
|
+
// Remove control characters that could be problematic (keep newlines for formatting)
|
|
64
|
+
// But remove standalone \r\n sequences that could be interpreted as SMTP commands
|
|
65
|
+
sanitized = sanitized.replace(/\r\n\r\n/g, "\n\n");
|
|
66
|
+
sanitized = sanitized.replace(/\r\n/g, "\n");
|
|
67
|
+
// Limit length to prevent buffer overflow attacks
|
|
68
|
+
const maxLength = 5000000; // 5MB limit for email body
|
|
69
|
+
if (sanitized.length > maxLength) {
|
|
70
|
+
throw new Error("Email body is too long");
|
|
71
|
+
}
|
|
72
|
+
return sanitized;
|
|
73
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const updateFieldReference = (update, fieldReference) => {
|
|
2
|
+
const updateKeys = Object.keys(update);
|
|
3
|
+
fieldReference.forEach((key) => {
|
|
4
|
+
if (!(key in updateKeys)) {
|
|
5
|
+
fieldReference.delete(key);
|
|
6
|
+
}
|
|
7
|
+
});
|
|
8
|
+
updateKeys.forEach((key) => {
|
|
9
|
+
fieldReference.add(key);
|
|
10
|
+
});
|
|
11
|
+
if (!fieldReference.has("id")) {
|
|
12
|
+
fieldReference.add("id");
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { CollectionCustomization, StokerRecord, PreValidateHookArgs, CollectionSchema, CollectionsSchema } from "@stoker-platform/types";
|
|
2
|
+
export declare const validateRecord: (operation: "create" | "update", record: StokerRecord, collection: CollectionSchema, customization: CollectionCustomization, args: PreValidateHookArgs, schema: CollectionsSchema) => Promise<StokerRecord>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getZodSchema } from "./getZodSchema.js";
|
|
2
|
+
import { tryPromise } from "../getConfigValue.js";
|
|
3
|
+
const preValidate = async (callback, args) => {
|
|
4
|
+
if (callback) {
|
|
5
|
+
const validation = await tryPromise(callback, args);
|
|
6
|
+
if (!validation.valid)
|
|
7
|
+
throw new Error(`VALIDATION_ERROR: ${validation.message}`);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
export const validateRecord = async (operation, record, collection, customization, args, schema) => {
|
|
11
|
+
await preValidate(customization.custom?.preValidate, args);
|
|
12
|
+
for (const field of customization.fields) {
|
|
13
|
+
await preValidate(field.custom?.preValidate, args);
|
|
14
|
+
}
|
|
15
|
+
const zodSchema = getZodSchema(operation, collection, schema);
|
|
16
|
+
zodSchema.parse(record);
|
|
17
|
+
return record;
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const validateStorageName: (name: string) => string | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const validateStorageName = (name) => {
|
|
2
|
+
const trimmed = name.trim();
|
|
3
|
+
if (!trimmed)
|
|
4
|
+
return "Name cannot be empty";
|
|
5
|
+
if (trimmed.includes("/"))
|
|
6
|
+
return "Name cannot contain /";
|
|
7
|
+
if (/[\r\n]/.test(trimmed))
|
|
8
|
+
return "Name cannot contain line breaks";
|
|
9
|
+
if (/[#[\]*?]/.test(trimmed))
|
|
10
|
+
return "Name cannot contain any of # [ ] * ?";
|
|
11
|
+
if (trimmed.includes(".."))
|
|
12
|
+
return "Name cannot contain ..";
|
|
13
|
+
if (trimmed === ".")
|
|
14
|
+
return "Name cannot be .";
|
|
15
|
+
const byteLength = new TextEncoder().encode(trimmed).length;
|
|
16
|
+
if (byteLength > 1024)
|
|
17
|
+
return "Name must be at most 1024 bytes";
|
|
18
|
+
return null;
|
|
19
|
+
};
|