@donotdev/functions 0.0.4 → 0.0.6

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.
@@ -0,0 +1,361 @@
1
+ // packages/functions/src/firebase/crud/aggregate.ts
2
+
3
+ /**
4
+ * @fileoverview Generic function to aggregate entities with metrics and grouping.
5
+ * @description Provides a reusable implementation for computing aggregations on Firestore collections.
6
+ * Returns only computed metrics, never raw data - optimized for analytics dashboards.
7
+ *
8
+ * @version 0.0.1
9
+ * @since 0.0.1
10
+ * @author AMBROISE PARK Consulting
11
+ */
12
+
13
+ import type { CallableRequest } from 'firebase-functions/v2/https';
14
+
15
+ import * as v from 'valibot';
16
+
17
+ import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
18
+
19
+ import { isFieldVisible } from '../../shared/schema.js';
20
+ import { DoNotDevError } from '../../shared/utils.js';
21
+ import { createBaseFunction } from '../baseFunction.js';
22
+ import { CRUD_CONFIG } from '../config/constants.js';
23
+
24
+ /** Supported aggregation operations */
25
+ export type AggregateOperation = 'count' | 'sum' | 'avg' | 'min' | 'max';
26
+
27
+ /** Single metric definition */
28
+ export interface MetricDefinition {
29
+ /** Field to aggregate ('*' for count all) */
30
+ field: string;
31
+ /** Aggregation operation */
32
+ operation: AggregateOperation;
33
+ /** Output name for this metric */
34
+ as: string;
35
+ /** Optional filter for this metric */
36
+ filter?: {
37
+ field: string;
38
+ operator: '==' | '!=' | '>' | '<' | '>=' | '<=';
39
+ value: any;
40
+ };
41
+ }
42
+
43
+ /** Group by definition */
44
+ export interface GroupByDefinition {
45
+ /** Field to group by */
46
+ field: string;
47
+ /** Metrics to compute per group */
48
+ metrics: MetricDefinition[];
49
+ }
50
+
51
+ /** Aggregation configuration */
52
+ export interface AggregateConfig {
53
+ /** Top-level metrics (computed on entire collection) */
54
+ metrics?: MetricDefinition[];
55
+ /** Group by configurations */
56
+ groupBy?: GroupByDefinition[];
57
+ /** Optional global filters */
58
+ where?: Array<[string, any, any]>;
59
+ }
60
+
61
+ /** Request schema for aggregate function */
62
+ interface AggregateRequest {
63
+ /** Optional runtime filters (merged with config filters) */
64
+ where?: Array<[string, any, any]>;
65
+ /** Optional date range filter */
66
+ dateRange?: {
67
+ field: string;
68
+ start?: string;
69
+ end?: string;
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Extracts visibility from a Valibot field schema
75
+ */
76
+ function getFieldVisibilityFromSchema(
77
+ schema: any,
78
+ fieldName: string
79
+ ): string | undefined {
80
+ if (!schema?.entries?.[fieldName]) return undefined;
81
+ const field = schema.entries[fieldName];
82
+ return field?.visibility;
83
+ }
84
+
85
+ /**
86
+ * Checks if user can access a field for aggregation
87
+ */
88
+ function canAggregateField(
89
+ fieldName: string,
90
+ schema: any,
91
+ isAdmin: boolean
92
+ ): boolean {
93
+ // '*' is always allowed (count all)
94
+ if (fieldName === '*') return true;
95
+
96
+ const visibility = getFieldVisibilityFromSchema(schema, fieldName);
97
+ return isFieldVisible(fieldName, visibility as any, isAdmin);
98
+ }
99
+
100
+ /**
101
+ * Computes a single metric on a dataset
102
+ */
103
+ function computeMetric(docs: any[], metric: MetricDefinition): number | null {
104
+ // Apply metric-level filter if specified
105
+ let filtered = docs;
106
+ if (metric.filter) {
107
+ filtered = docs.filter((doc) => {
108
+ const value = doc[metric.filter!.field];
109
+ switch (metric.filter!.operator) {
110
+ case '==':
111
+ return value === metric.filter!.value;
112
+ case '!=':
113
+ return value !== metric.filter!.value;
114
+ case '>':
115
+ return value > metric.filter!.value;
116
+ case '<':
117
+ return value < metric.filter!.value;
118
+ case '>=':
119
+ return value >= metric.filter!.value;
120
+ case '<=':
121
+ return value <= metric.filter!.value;
122
+ default:
123
+ return true;
124
+ }
125
+ });
126
+ }
127
+
128
+ switch (metric.operation) {
129
+ case 'count':
130
+ return filtered.length;
131
+
132
+ case 'sum': {
133
+ return filtered.reduce((sum, doc) => {
134
+ const val = Number(doc[metric.field]) || 0;
135
+ return sum + val;
136
+ }, 0);
137
+ }
138
+
139
+ case 'avg': {
140
+ if (filtered.length === 0) return null;
141
+ const sum = filtered.reduce((s, doc) => {
142
+ const val = Number(doc[metric.field]) || 0;
143
+ return s + val;
144
+ }, 0);
145
+ return sum / filtered.length;
146
+ }
147
+
148
+ case 'min': {
149
+ if (filtered.length === 0) return null;
150
+ return filtered.reduce(
151
+ (min, doc) => {
152
+ const val = Number(doc[metric.field]);
153
+ if (isNaN(val)) return min;
154
+ return min === null ? val : Math.min(min, val);
155
+ },
156
+ null as number | null
157
+ );
158
+ }
159
+
160
+ case 'max': {
161
+ if (filtered.length === 0) return null;
162
+ return filtered.reduce(
163
+ (max, doc) => {
164
+ const val = Number(doc[metric.field]);
165
+ if (isNaN(val)) return max;
166
+ return max === null ? val : Math.max(max, val);
167
+ },
168
+ null as number | null
169
+ );
170
+ }
171
+
172
+ default:
173
+ return null;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Generic business logic for aggregating entities
179
+ */
180
+ function aggregateEntitiesLogicFactory(
181
+ collection: string,
182
+ documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
183
+ config: AggregateConfig
184
+ ) {
185
+ return async function aggregateEntitiesLogic(
186
+ data: AggregateRequest,
187
+ context: { uid: string; request: CallableRequest<AggregateRequest> }
188
+ ) {
189
+ const db = getFirebaseAdminFirestore();
190
+ const user = context.request.auth;
191
+ const isAdmin = user?.token?.isAdmin === true;
192
+
193
+ // Validate that user can access the fields being aggregated
194
+ const allFields = new Set<string>();
195
+
196
+ config.metrics?.forEach((m) => allFields.add(m.field));
197
+ config.groupBy?.forEach((g) => {
198
+ allFields.add(g.field);
199
+ g.metrics.forEach((m) => allFields.add(m.field));
200
+ });
201
+
202
+ for (const field of allFields) {
203
+ if (!canAggregateField(field, documentSchema, isAdmin)) {
204
+ throw new DoNotDevError(
205
+ `Access denied: cannot aggregate field '${field}'`,
206
+ 'permission-denied'
207
+ );
208
+ }
209
+ }
210
+
211
+ // Build query with filters
212
+ let query: FirebaseFirestore.Query = db.collection(collection);
213
+
214
+ // Apply config-level filters
215
+ const whereFilters = [...(config.where || []), ...(data.where || [])];
216
+ for (const [field, operator, value] of whereFilters) {
217
+ query = query.where(field, operator, value);
218
+ }
219
+
220
+ // Apply date range filter if specified
221
+ if (data.dateRange) {
222
+ const { field, start, end } = data.dateRange;
223
+ if (start) {
224
+ query = query.where(field, '>=', new Date(start));
225
+ }
226
+ if (end) {
227
+ query = query.where(field, '<=', new Date(end));
228
+ }
229
+ }
230
+
231
+ // Fetch all matching documents
232
+ const snapshot = await query.get();
233
+ const docs: Record<string, any>[] = snapshot.docs.map((doc) => ({
234
+ id: doc.id,
235
+ ...doc.data(),
236
+ }));
237
+
238
+ // Compute top-level metrics
239
+ const metrics: Record<string, number | null> = {};
240
+ if (config.metrics) {
241
+ for (const metric of config.metrics) {
242
+ metrics[metric.as] = computeMetric(docs, metric);
243
+ }
244
+ }
245
+
246
+ // Compute grouped metrics
247
+ const groups: Record<
248
+ string,
249
+ Record<string, Record<string, number | null>>
250
+ > = {};
251
+ if (config.groupBy) {
252
+ for (const groupConfig of config.groupBy) {
253
+ const groupedDocs = new Map<string, any[]>();
254
+
255
+ // Group documents by field value
256
+ for (const doc of docs) {
257
+ const groupValue = String(doc[groupConfig.field] ?? 'null');
258
+ if (!groupedDocs.has(groupValue)) {
259
+ groupedDocs.set(groupValue, []);
260
+ }
261
+ groupedDocs.get(groupValue)!.push(doc);
262
+ }
263
+
264
+ // Compute metrics for each group
265
+ const groupResults: Record<string, Record<string, number | null>> = {};
266
+ for (const [groupValue, groupDocs] of groupedDocs) {
267
+ groupResults[groupValue] = {};
268
+ for (const metric of groupConfig.metrics) {
269
+ groupResults[groupValue][metric.as] = computeMetric(
270
+ groupDocs,
271
+ metric
272
+ );
273
+ }
274
+ }
275
+
276
+ groups[groupConfig.field] = groupResults;
277
+ }
278
+ }
279
+
280
+ return {
281
+ metrics,
282
+ groups,
283
+ meta: {
284
+ collection,
285
+ totalDocs: docs.length,
286
+ computedAt: new Date().toISOString(),
287
+ },
288
+ };
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Generic function to aggregate entities from any Firestore collection.
294
+ * Returns only computed metrics, never raw data.
295
+ *
296
+ * @param collection - The Firestore collection name
297
+ * @param documentSchema - The Valibot schema (used for visibility checks)
298
+ * @param config - Aggregation configuration (metrics, groupBy, filters)
299
+ * @returns Firebase callable function
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * // Define analytics for cars collection
304
+ * export const getCarsAnalytics = aggregateEntities('cars', carSchema, {
305
+ * metrics: [
306
+ * { field: '*', operation: 'count', as: 'total' },
307
+ * { field: 'price', operation: 'sum', as: 'totalValue' },
308
+ * { field: 'price', operation: 'avg', as: 'avgPrice' },
309
+ * ],
310
+ * groupBy: [
311
+ * {
312
+ * field: 'status',
313
+ * metrics: [
314
+ * { field: '*', operation: 'count', as: 'count' },
315
+ * { field: 'price', operation: 'sum', as: 'value' },
316
+ * ],
317
+ * },
318
+ * ],
319
+ * });
320
+ *
321
+ * // Returns:
322
+ * // {
323
+ * // metrics: { total: 150, totalValue: 4500000, avgPrice: 30000 },
324
+ * // groups: {
325
+ * // status: {
326
+ * // Available: { count: 80, value: 2400000 },
327
+ * // Reserved: { count: 20, value: 600000 },
328
+ * // Sold: { count: 50, value: 1500000 },
329
+ * // }
330
+ * // },
331
+ * // meta: { collection: 'cars', totalDocs: 150, computedAt: '...' }
332
+ * // }
333
+ * ```
334
+ *
335
+ * @version 0.0.1
336
+ * @since 0.0.1
337
+ * @author AMBROISE PARK Consulting
338
+ */
339
+ export const aggregateEntities = (
340
+ collection: string,
341
+ documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
342
+ config: AggregateConfig
343
+ ) => {
344
+ const requestSchema = v.object({
345
+ where: v.optional(v.array(v.tuple([v.string(), v.any(), v.any()]))),
346
+ dateRange: v.optional(
347
+ v.object({
348
+ field: v.string(),
349
+ start: v.optional(v.string()),
350
+ end: v.optional(v.string()),
351
+ })
352
+ ),
353
+ });
354
+
355
+ return createBaseFunction(
356
+ CRUD_CONFIG,
357
+ requestSchema,
358
+ 'aggregate_entities',
359
+ aggregateEntitiesLogicFactory(collection, documentSchema, config)
360
+ );
361
+ };
@@ -12,6 +12,7 @@
12
12
  import * as v from 'valibot';
13
13
 
14
14
  import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
15
+ import { DEFAULT_STATUS_VALUE } from '@donotdev/core/server';
15
16
 
16
17
  import {
17
18
  prepareForFirestore,
@@ -22,7 +23,15 @@ import { assertAdmin, validateDocument } from '../../shared/utils.js';
22
23
  import { createBaseFunction } from '../baseFunction.js';
23
24
  import { CRUD_CONFIG } from '../config/constants.js';
24
25
 
25
- import type { CallableRequest } from 'firebase-functions/v2/https';
26
+ import type {
27
+ CallableFunction,
28
+ CallableRequest,
29
+ } from 'firebase-functions/v2/https';
30
+
31
+ export type CreateEntityRequest = {
32
+ payload: Record<string, any>;
33
+ idempotencyKey?: string;
34
+ };
26
35
 
27
36
  /**
28
37
  * Generic business logic for creating entities
@@ -37,7 +46,7 @@ function createEntityLogicFactory(
37
46
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
38
47
  ) {
39
48
  return async function createEntityLogic(
40
- data: { payload: Record<string, any>; idempotencyKey?: string },
49
+ data: CreateEntityRequest,
41
50
  context: { uid: string; request: CallableRequest<any> }
42
51
  ) {
43
52
  const { payload, idempotencyKey } = data;
@@ -57,12 +66,21 @@ function createEntityLogicFactory(
57
66
  }
58
67
  }
59
68
 
69
+ // Determine status (default to draft if not provided)
70
+ const status = payload.status ?? DEFAULT_STATUS_VALUE;
71
+ const isDraft = status === 'draft';
72
+
60
73
  // Validate the document against the schema
61
- validateDocument(payload, documentSchema);
74
+ // Skip validation for drafts - required fields can be incomplete
75
+ if (!isDraft) {
76
+ validateDocument(payload, documentSchema);
77
+ }
62
78
 
63
79
  // Prepare the document for Firestore and add metadata
80
+ // Always ensure status is set
64
81
  const documentData = {
65
82
  ...prepareForFirestore(payload),
83
+ status, // Ensure status is always present
66
84
  ...createMetadata(uid),
67
85
  };
68
86
 
@@ -110,7 +128,7 @@ export const createEntity = (
110
128
  collection: string,
111
129
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
112
130
  customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
113
- ) => {
131
+ ): CallableFunction<CreateEntityRequest, Promise<any>> => {
114
132
  const requestSchema =
115
133
  customSchema ||
116
134
  v.object({
@@ -20,7 +20,12 @@ import {
20
20
  import { createBaseFunction } from '../baseFunction.js';
21
21
  import { CRUD_CONFIG } from '../config/constants.js';
22
22
 
23
- import type { CallableRequest } from 'firebase-functions/v2/https';
23
+ import type {
24
+ CallableFunction,
25
+ CallableRequest,
26
+ } from 'firebase-functions/v2/https';
27
+
28
+ export type DeleteEntityRequest = { id: string };
24
29
 
25
30
  /**
26
31
  * Generic business logic for deleting entities
@@ -32,7 +37,7 @@ import type { CallableRequest } from 'firebase-functions/v2/https';
32
37
  */
33
38
  function deleteEntityLogicFactory(collection: string) {
34
39
  return async function deleteEntityLogic(
35
- data: { id: string },
40
+ data: DeleteEntityRequest,
36
41
  context: { uid: string; request: CallableRequest<any> }
37
42
  ) {
38
43
  const { id } = data;
@@ -73,7 +78,7 @@ function deleteEntityLogicFactory(collection: string) {
73
78
  export const deleteEntity = (
74
79
  collection: string,
75
80
  customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
76
- ) => {
81
+ ): CallableFunction<DeleteEntityRequest, Promise<{ success: boolean }>> => {
77
82
  const requestSchema =
78
83
  customSchema ||
79
84
  v.object({
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import * as v from 'valibot';
13
+ import { HIDDEN_STATUSES } from '@donotdev/core/server';
13
14
 
14
15
  import { transformFirestoreData } from '../../shared/index.js';
15
16
  import { filterVisibleFields } from '../../shared/index.js';
@@ -19,7 +20,12 @@ import { DoNotDevError } from '../../shared/utils.js';
19
20
  import { createBaseFunction } from '../baseFunction.js';
20
21
  import { CRUD_CONFIG } from '../config/constants.js';
21
22
 
22
- import type { CallableRequest } from 'firebase-functions/v2/https';
23
+ import type {
24
+ CallableFunction,
25
+ CallableRequest,
26
+ } from 'firebase-functions/v2/https';
27
+
28
+ export type GetEntityRequest = { id: string };
23
29
 
24
30
  /**
25
31
  * Generic business logic for getting entities
@@ -30,7 +36,7 @@ function getEntityLogicFactory(
30
36
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
31
37
  ) {
32
38
  return async function getEntityLogic(
33
- data: { id: string },
39
+ data: GetEntityRequest,
34
40
  context: { uid: string; request: CallableRequest<any> }
35
41
  ) {
36
42
  const { id } = data;
@@ -50,9 +56,15 @@ function getEntityLogicFactory(
50
56
  // Check if the user is an admin
51
57
  const isAdmin = context.request.auth?.token?.isAdmin === true;
52
58
 
59
+ // Hide drafts/deleted from non-admin users (security: hidden statuses never reach public)
60
+ const docData = doc.data();
61
+ if (!isAdmin && (HIDDEN_STATUSES as readonly string[]).includes(docData?.status)) {
62
+ throw new DoNotDevError('Entity not found', 'not-found');
63
+ }
64
+
53
65
  // Filter fields based on visibility and user role
54
66
  const filteredData = filterVisibleFields(
55
- doc.data() || {},
67
+ docData || {},
56
68
  documentSchema,
57
69
  isAdmin
58
70
  );
@@ -76,7 +88,7 @@ export const getEntity = (
76
88
  collection: string,
77
89
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
78
90
  customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
79
- ) => {
91
+ ): CallableFunction<GetEntityRequest, Promise<any>> => {
80
92
  const requestSchema =
81
93
  customSchema ||
82
94
  v.object({
@@ -9,6 +9,7 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
+ export * from './aggregate.js';
12
13
  export * from './create.js';
13
14
  export * from './delete.js';
14
15
  export * from './get.js';
@@ -10,12 +10,13 @@
10
10
  */
11
11
 
12
12
  import { Query } from '@donotdev/firebase/server';
13
+ import { HIDDEN_STATUSES } from '@donotdev/core/server';
13
14
 
14
15
  import type { CallableRequest } from 'firebase-functions/v2/https';
15
16
 
16
17
  import * as v from 'valibot';
17
18
 
18
- interface ListEntityRequest {
19
+ export interface ListEntityRequest {
19
20
  where?: Array<[string, any, any]>;
20
21
  orderBy?: Array<[string, 'asc' | 'desc']>;
21
22
  limit?: number;
@@ -33,6 +34,8 @@ import { DoNotDevError } from '../../shared/utils.js';
33
34
  import { createBaseFunction } from '../baseFunction.js';
34
35
  import { CRUD_CONFIG } from '../config/constants.js';
35
36
 
37
+ import type { CallableFunction } from 'firebase-functions/v2/https';
38
+
36
39
  /**
37
40
  * Generic business logic for listing entities
38
41
  * Base function handles: validation, auth, rate limiting, monitoring
@@ -47,10 +50,19 @@ function listEntitiesLogicFactory(
47
50
  ) {
48
51
  const { where = [], orderBy = [], limit = 50, startAfterId, search } = data;
49
52
 
53
+ // Check if the user is an admin (needed early for draft filtering)
54
+ const user = context.request.auth;
55
+ const isAdmin = user?.token?.isAdmin === true;
56
+
50
57
  // Start with a Query (not a CollectionReference)
51
58
  const db = getFirebaseAdminFirestore();
52
59
  let query: Query = db.collection(collection);
53
60
 
61
+ // Filter out hidden statuses for non-admin users (security: drafts/deleted never reach public)
62
+ if (!isAdmin) {
63
+ query = query.where('status', 'not-in', [...HIDDEN_STATUSES]);
64
+ }
65
+
54
66
  // Apply search if provided
55
67
  if (search) {
56
68
  const { field, query: searchQuery } = search;
@@ -89,10 +101,6 @@ function listEntitiesLogicFactory(
89
101
  // Execute the query
90
102
  const snapshot = await query.get();
91
103
 
92
- // Check if the user is an admin
93
- const user = context.request.auth;
94
- const isAdmin = user?.token?.isAdmin === true;
95
-
96
104
  // Filter document fields based on visibility and user role
97
105
  const docs = snapshot.docs.map((doc: any) => ({
98
106
  id: doc.id,
@@ -120,7 +128,7 @@ export const listEntities = (
120
128
  collection: string,
121
129
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
122
130
  customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
123
- ) => {
131
+ ): CallableFunction<ListEntityRequest, Promise<any>> => {
124
132
  const requestSchema =
125
133
  customSchema ||
126
134
  v.object({
@@ -12,6 +12,7 @@
12
12
  import * as v from 'valibot';
13
13
 
14
14
  import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
15
+ import { DEFAULT_STATUS_VALUE } from '@donotdev/core/server';
15
16
 
16
17
  import {
17
18
  prepareForFirestore,
@@ -26,7 +27,16 @@ import {
26
27
  import { createBaseFunction } from '../baseFunction.js';
27
28
  import { CRUD_CONFIG } from '../config/constants.js';
28
29
 
29
- import type { CallableRequest } from 'firebase-functions/v2/https';
30
+ import type {
31
+ CallableFunction,
32
+ CallableRequest,
33
+ } from 'firebase-functions/v2/https';
34
+
35
+ export type UpdateEntityRequest = {
36
+ id: string;
37
+ payload: Record<string, any>;
38
+ idempotencyKey?: string;
39
+ };
30
40
 
31
41
  /**
32
42
  * Generic business logic for updating entities
@@ -37,7 +47,7 @@ function updateEntityLogicFactory(
37
47
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
38
48
  ) {
39
49
  return async function updateEntityLogic(
40
- data: { id: string; payload: Record<string, any>; idempotencyKey?: string },
50
+ data: UpdateEntityRequest,
41
51
  context: { uid: string; request: CallableRequest<any> }
42
52
  ) {
43
53
  const { id, payload, idempotencyKey } = data;
@@ -71,8 +81,15 @@ function updateEntityLogicFactory(
71
81
  const currentData = transformFirestoreData(doc.data());
72
82
  const mergedData = { ...currentData, ...payload };
73
83
 
84
+ // Determine resulting status (default to draft if not set)
85
+ const resultingStatus = mergedData.status ?? DEFAULT_STATUS_VALUE;
86
+ const isDraft = resultingStatus === 'draft';
87
+
74
88
  // Validate the merged document against the schema
75
- validateDocument(mergedData, documentSchema);
89
+ // Skip validation for drafts - required fields can be incomplete
90
+ if (!isDraft) {
91
+ validateDocument(mergedData, documentSchema);
92
+ }
76
93
 
77
94
  // Prepare the update data for Firestore and add metadata
78
95
  const updateData = {
@@ -119,7 +136,7 @@ export const updateEntity = (
119
136
  collection: string,
120
137
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
121
138
  customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
122
- ) => {
139
+ ): CallableFunction<UpdateEntityRequest, Promise<any>> => {
123
140
  const requestSchema =
124
141
  customSchema ||
125
142
  v.object({