@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.
Files changed (126) hide show
  1. package/lib/src/access/collection.d.ts +9 -0
  2. package/lib/src/access/collection.js +59 -0
  3. package/lib/src/access/document.d.ts +3 -0
  4. package/lib/src/access/document.js +191 -0
  5. package/lib/src/access/getCollectionRestrictions.d.ts +7 -0
  6. package/lib/src/access/getCollectionRestrictions.js +71 -0
  7. package/lib/src/access/getRecordSubcollections.d.ts +2 -0
  8. package/lib/src/access/getRecordSubcollections.js +15 -0
  9. package/lib/src/access/getRelatedCollections.d.ts +2 -0
  10. package/lib/src/access/getRelatedCollections.js +24 -0
  11. package/lib/src/access/hasDependencyAccess.d.ts +2 -0
  12. package/lib/src/access/hasDependencyAccess.js +24 -0
  13. package/lib/src/access/isPaginationEnabled.d.ts +2 -0
  14. package/lib/src/access/isPaginationEnabled.js +35 -0
  15. package/lib/src/access/permissions.d.ts +2 -0
  16. package/lib/src/access/permissions.js +543 -0
  17. package/lib/src/access/read/getOne.d.ts +2 -0
  18. package/lib/src/access/read/getOne.js +19 -0
  19. package/lib/src/access/read/getSome.d.ts +2 -0
  20. package/lib/src/access/read/getSome.js +21 -0
  21. package/lib/src/access/roleHasOperationAccess.d.ts +2 -0
  22. package/lib/src/access/roleHasOperationAccess.js +7 -0
  23. package/lib/src/access/write/addRecord.d.ts +2 -0
  24. package/lib/src/access/write/addRecord.js +40 -0
  25. package/lib/src/access/write/deleteRecord.d.ts +2 -0
  26. package/lib/src/access/write/deleteRecord.js +26 -0
  27. package/lib/src/access/write/updateRecord.d.ts +2 -0
  28. package/lib/src/access/write/updateRecord.js +61 -0
  29. package/lib/src/getConfigValue.d.ts +12 -0
  30. package/lib/src/getConfigValue.js +83 -0
  31. package/lib/src/getCustomization.d.ts +4 -0
  32. package/lib/src/getCustomization.js +29 -0
  33. package/lib/src/getFieldCustomization.d.ts +7 -0
  34. package/lib/src/getFieldCustomization.js +3 -0
  35. package/lib/src/main.d.ts +60 -0
  36. package/lib/src/main.js +60 -0
  37. package/lib/src/operations/addInitialValues.d.ts +2 -0
  38. package/lib/src/operations/addInitialValues.js +27 -0
  39. package/lib/src/operations/addLowercaseFields.d.ts +2 -0
  40. package/lib/src/operations/addLowercaseFields.js +12 -0
  41. package/lib/src/operations/addRelationArrays.d.ts +2 -0
  42. package/lib/src/operations/addRelationArrays.js +60 -0
  43. package/lib/src/operations/addSystemFields.d.ts +2 -0
  44. package/lib/src/operations/addSystemFields.js +17 -0
  45. package/lib/src/operations/getDateRange.d.ts +5 -0
  46. package/lib/src/operations/getDateRange.js +56 -0
  47. package/lib/src/operations/getExtendedSchema.d.ts +2 -0
  48. package/lib/src/operations/getExtendedSchema.js +23 -0
  49. package/lib/src/operations/getFinalRecord.d.ts +1 -0
  50. package/lib/src/operations/getFinalRecord.js +11 -0
  51. package/lib/src/operations/getInputSchema.d.ts +15 -0
  52. package/lib/src/operations/getInputSchema.js +352 -0
  53. package/lib/src/operations/getLowercaseFields.d.ts +2 -0
  54. package/lib/src/operations/getLowercaseFields.js +15 -0
  55. package/lib/src/operations/getSingleFieldRelations.d.ts +2 -0
  56. package/lib/src/operations/getSingleFieldRelations.js +27 -0
  57. package/lib/src/operations/getZodSchema.d.ts +15 -0
  58. package/lib/src/operations/getZodSchema.js +303 -0
  59. package/lib/src/operations/isDeleteSentinel.d.ts +1 -0
  60. package/lib/src/operations/isDeleteSentinel.js +4 -0
  61. package/lib/src/operations/isSortingEnabled.d.ts +2 -0
  62. package/lib/src/operations/isSortingEnabled.js +7 -0
  63. package/lib/src/operations/isValidUniqueFieldValue.d.ts +1 -0
  64. package/lib/src/operations/isValidUniqueFieldValue.js +25 -0
  65. package/lib/src/operations/parseDate.d.ts +1 -0
  66. package/lib/src/operations/parseDate.js +4 -0
  67. package/lib/src/operations/prepareDenormalized.d.ts +8 -0
  68. package/lib/src/operations/prepareDenormalized.js +312 -0
  69. package/lib/src/operations/removeDeleteSentinels.d.ts +2 -0
  70. package/lib/src/operations/removeDeleteSentinels.js +15 -0
  71. package/lib/src/operations/removeDeletedFields.d.ts +2 -0
  72. package/lib/src/operations/removeDeletedFields.js +14 -0
  73. package/lib/src/operations/removeEmptyStrings.d.ts +2 -0
  74. package/lib/src/operations/removeEmptyStrings.js +14 -0
  75. package/lib/src/operations/removePrivateFields.d.ts +2 -0
  76. package/lib/src/operations/removePrivateFields.js +14 -0
  77. package/lib/src/operations/removeUndefined.d.ts +2 -0
  78. package/lib/src/operations/removeUndefined.js +14 -0
  79. package/lib/src/operations/retryOperation.d.ts +1 -0
  80. package/lib/src/operations/retryOperation.js +21 -0
  81. package/lib/src/operations/runHooks.d.ts +17 -0
  82. package/lib/src/operations/runHooks.js +18 -0
  83. package/lib/src/operations/sanitizeDownloadFilename.d.ts +1 -0
  84. package/lib/src/operations/sanitizeDownloadFilename.js +18 -0
  85. package/lib/src/operations/sanitizeEmailInput.d.ts +5 -0
  86. package/lib/src/operations/sanitizeEmailInput.js +73 -0
  87. package/lib/src/operations/updateFieldReference.d.ts +2 -0
  88. package/lib/src/operations/updateFieldReference.js +14 -0
  89. package/lib/src/operations/validateRecord.d.ts +2 -0
  90. package/lib/src/operations/validateRecord.js +18 -0
  91. package/lib/src/operations/validateStorageName.d.ts +1 -0
  92. package/lib/src/operations/validateStorageName.js +19 -0
  93. package/lib/src/schema/getAccessFields.d.ts +2 -0
  94. package/lib/src/schema/getAccessFields.js +62 -0
  95. package/lib/src/schema/getCollection.d.ts +2 -0
  96. package/lib/src/schema/getCollection.js +3 -0
  97. package/lib/src/schema/getDependencyFields.d.ts +6 -0
  98. package/lib/src/schema/getDependencyFields.js +22 -0
  99. package/lib/src/schema/getField.d.ts +2 -0
  100. package/lib/src/schema/getField.js +3 -0
  101. package/lib/src/schema/getFieldNames.d.ts +2 -0
  102. package/lib/src/schema/getFieldNames.js +3 -0
  103. package/lib/src/schema/getIndexFields.d.ts +9 -0
  104. package/lib/src/schema/getIndexFields.js +184 -0
  105. package/lib/src/schema/getInverseRelationType.d.ts +1 -0
  106. package/lib/src/schema/getInverseRelationType.js +15 -0
  107. package/lib/src/schema/getPathCollections.d.ts +2 -0
  108. package/lib/src/schema/getPathCollections.js +12 -0
  109. package/lib/src/schema/getRecordSystemFields.d.ts +2 -0
  110. package/lib/src/schema/getRecordSystemFields.js +11 -0
  111. package/lib/src/schema/getRelationLists.d.ts +5 -0
  112. package/lib/src/schema/getRelationLists.js +13 -0
  113. package/lib/src/schema/getSubcollections.d.ts +2 -0
  114. package/lib/src/schema/getSubcollections.js +9 -0
  115. package/lib/src/schema/getSystemFieldsSchema.d.ts +2 -0
  116. package/lib/src/schema/getSystemFieldsSchema.js +52 -0
  117. package/lib/src/schema/isDependencyField.d.ts +2 -0
  118. package/lib/src/schema/isDependencyField.js +18 -0
  119. package/lib/src/schema/isIncludedField.d.ts +2 -0
  120. package/lib/src/schema/isIncludedField.js +18 -0
  121. package/lib/src/schema/isRelationField.d.ts +2 -0
  122. package/lib/src/schema/isRelationField.js +3 -0
  123. package/lib/src/schema/system-fields.d.ts +2 -0
  124. package/lib/src/schema/system-fields.js +13 -0
  125. package/lib/tsconfig.tsbuildinfo +1 -0
  126. 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,2 @@
1
+ import { StokerRecord } from "@stoker-platform/types";
2
+ export declare const removeDeleteSentinels: (obj: StokerRecord) => void;
@@ -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,2 @@
1
+ import { StokerRecord } from "@stoker-platform/types";
2
+ export declare const removeDeletedFields: (document: StokerRecord, fieldReferences: Map<unknown, Set<string>>) => void;
@@ -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,2 @@
1
+ import { StokerRecord } from "@stoker-platform/types";
2
+ export declare const removeEmptyStrings: (record: StokerRecord | Partial<StokerRecord>) => void;
@@ -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,2 @@
1
+ import { CollectionSchema, StokerRecord } from "@stoker-platform/types";
2
+ export declare const removePrivateFields: (record: StokerRecord, schema: CollectionSchema) => StokerRecord;
@@ -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,2 @@
1
+ import { StokerRecord } from "@stoker-platform/types";
2
+ export declare const removeUndefined: (record: StokerRecord | Partial<StokerRecord>) => void;
@@ -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,2 @@
1
+ import { StokerRecord } from "@stoker-platform/types";
2
+ export declare const updateFieldReference: (update: StokerRecord, fieldReference: Set<string>) => void;
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ import type { CollectionField, CollectionSchema, StokerRole } from "@stoker-platform/types";
2
+ export declare const getAccessFields: (collection: CollectionSchema, role: StokerRole) => CollectionField[];