@donotdev/functions 0.0.9 → 0.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donotdev/functions",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "private": false,
5
5
  "description": "Backend functions for DoNotDev Framework - Firebase, Vercel, and platform-agnostic implementations for auth, billing, CRUD, and OAuth",
6
6
  "main": "./lib/firebase/index.js",
@@ -42,8 +42,8 @@
42
42
  "serve": "firebase emulators:start --only functions"
43
43
  },
44
44
  "dependencies": {
45
- "@donotdev/core": "^0.0.19",
46
- "@donotdev/firebase": "^0.0.8"
45
+ "@donotdev/core": "^0.0.23",
46
+ "@donotdev/firebase": "^0.0.10"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@sentry/node": "^10.38.0",
@@ -30,15 +30,12 @@ export const FUNCTION_CONFIG = {
30
30
  cors: true, // Enable CORS by default for all functions (required for web apps)
31
31
  } as const;
32
32
 
33
- import { stripeSecretKey, stripeWebhookSecret } from './secrets.js';
34
-
35
33
  /** Stripe/billing functions */
36
34
  export const STRIPE_CONFIG = {
37
35
  ...BASE_CONFIG,
38
36
  memory: '512MiB' as const,
39
37
  timeoutSeconds: 30,
40
38
  cors: true,
41
- secrets: [stripeSecretKey, stripeWebhookSecret],
42
39
  };
43
40
 
44
41
  /** Auth functions */
@@ -16,7 +16,7 @@ import {
16
16
  hasRoleAccess,
17
17
  HIDDEN_STATUSES,
18
18
  } from '@donotdev/core/server';
19
- import type { UserRole } from '@donotdev/core/server';
19
+ import type { EntityOwnershipConfig, UserRole } from '@donotdev/core/server';
20
20
  import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
21
21
 
22
22
  import { transformFirestoreData } from '../../shared/index.js';
@@ -37,14 +37,15 @@ export type GetEntityRequest = { id: string };
37
37
  */
38
38
  function getEntityLogicFactory(
39
39
  collection: string,
40
- documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
40
+ documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
41
+ ownership?: EntityOwnershipConfig
41
42
  ) {
42
43
  return async function getEntityLogic(
43
44
  data: GetEntityRequest,
44
45
  context: { uid: string; userRole: UserRole; request: CallableRequest<any> }
45
46
  ) {
46
47
  const { id } = data;
47
- const { userRole } = context;
48
+ const { userRole, uid } = context;
48
49
 
49
50
  // Get the document reference
50
51
  const db = getFirebaseAdminFirestore();
@@ -69,11 +70,16 @@ function getEntityLogicFactory(
69
70
  throw new DoNotDevError('Entity not found', 'not-found');
70
71
  }
71
72
 
72
- // Filter fields based on visibility and user role
73
+ const rawData = docData || {};
74
+ const visibilityOptions =
75
+ ownership && uid ? { documentData: rawData, uid, ownership } : undefined;
76
+
77
+ // Filter fields based on visibility and user role (and ownership for visibility: 'owner')
73
78
  const filteredData = filterVisibleFields(
74
- docData || {},
79
+ rawData,
75
80
  documentSchema,
76
- userRole
81
+ userRole,
82
+ visibilityOptions
77
83
  );
78
84
 
79
85
  // Transform the document data back to the application format
@@ -90,13 +96,15 @@ function getEntityLogicFactory(
90
96
  * @param documentSchema - The Valibot schema for document validation
91
97
  * @param requiredRole - Minimum role required for this operation
92
98
  * @param customSchema - Optional custom request schema
99
+ * @param ownership - Optional ownership config for visibility: 'owner' field masking
93
100
  * @returns Firebase callable function
94
101
  */
95
102
  export const getEntity = (
96
103
  collection: string,
97
104
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
98
105
  requiredRole: UserRole,
99
- customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
106
+ customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
107
+ ownership?: EntityOwnershipConfig
100
108
  ): CallableFunction<GetEntityRequest, Promise<any>> => {
101
109
  const requestSchema =
102
110
  customSchema ||
@@ -108,7 +116,7 @@ export const getEntity = (
108
116
  CRUD_READ_CONFIG,
109
117
  requestSchema,
110
118
  'get_entity',
111
- getEntityLogicFactory(collection, documentSchema),
119
+ getEntityLogicFactory(collection, documentSchema, ownership),
112
120
  requiredRole
113
121
  );
114
122
  };
@@ -16,7 +16,11 @@ import {
16
16
  hasRoleAccess,
17
17
  HIDDEN_STATUSES,
18
18
  } from '@donotdev/core/server';
19
- import type { UserRole } from '@donotdev/core/server';
19
+ import type {
20
+ EntityOwnershipConfig,
21
+ EntityOwnershipPublicCondition,
22
+ UserRole,
23
+ } from '@donotdev/core/server';
20
24
  import { getFirebaseAdminFirestore, Query } from '@donotdev/firebase/server';
21
25
 
22
26
  import { transformFirestoreData } from '../../shared/index.js';
@@ -40,6 +44,20 @@ export interface ListEntityRequest {
40
44
  };
41
45
  }
42
46
 
47
+ /**
48
+ * Apply public-condition where clauses to a query (for listCard when ownership is set).
49
+ */
50
+ function applyPublicCondition(
51
+ query: Query,
52
+ publicCondition: EntityOwnershipPublicCondition[]
53
+ ): Query {
54
+ let q = query;
55
+ for (const c of publicCondition) {
56
+ q = q.where(c.field, c.op as any, c.value);
57
+ }
58
+ return q;
59
+ }
60
+
43
61
  /**
44
62
  * Generic business logic for listing entities
45
63
  * Base function handles: validation, auth, rate limiting, monitoring
@@ -47,7 +65,9 @@ export interface ListEntityRequest {
47
65
  function listEntitiesLogicFactory(
48
66
  collection: string,
49
67
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
50
- listFields?: string[]
68
+ listFields?: string[],
69
+ ownership?: EntityOwnershipConfig,
70
+ isListCard?: boolean
51
71
  ) {
52
72
  return async function listEntitiesLogic(
53
73
  data: ListEntityRequest,
@@ -58,7 +78,8 @@ function listEntitiesLogicFactory(
58
78
  }
59
79
  ) {
60
80
  const { where = [], orderBy = [], limit, startAfterId, search } = data;
61
- const { userRole } = context;
81
+ // Extract uid and userRole once (reused for all document masking)
82
+ const { userRole, uid } = context;
62
83
 
63
84
  const isAdmin = hasRoleAccess(userRole, 'admin'); // Uses role hierarchy
64
85
 
@@ -71,6 +92,26 @@ function listEntitiesLogicFactory(
71
92
  query = query.where('status', 'not-in', [...HIDDEN_STATUSES]);
72
93
  }
73
94
 
95
+ // Ownership: when set and not admin, listCard = public condition, list = mine filter
96
+ if (ownership && !isAdmin) {
97
+ if (
98
+ isListCard &&
99
+ ownership.publicCondition &&
100
+ ownership.publicCondition.length > 0
101
+ ) {
102
+ query = applyPublicCondition(query, ownership.publicCondition);
103
+ } else if (!isListCard && ownership.ownerFields.length > 0) {
104
+ // "Mine" filter: Firestore does not support OR across different fields in one query.
105
+ // Use first owner field only for single query; recommend ownerIds array-contains for multiple.
106
+ const firstOwnerField = ownership.ownerFields[0];
107
+ query = query.where(firstOwnerField, '==', uid);
108
+ if (ownership.ownerFields.length > 1) {
109
+ // Optional: run second query and merge (complex pagination). For now, single field only.
110
+ // Document ownerIds pattern for multiple stakeholders.
111
+ }
112
+ }
113
+ }
114
+
74
115
  // Apply search if provided
75
116
  if (search) {
76
117
  const { field, query: searchQuery } = search;
@@ -158,13 +199,18 @@ function listEntitiesLogicFactory(
158
199
  return value;
159
200
  };
160
201
 
161
- // Filter document fields based on visibility and user role
202
+ const visibilityOptions = ownership && uid ? { uid, ownership } : undefined;
203
+
204
+ // Filter document fields based on visibility and user role (uid/userRole from context, once)
162
205
  const docs = snapshot.docs.map((doc: any) => {
163
206
  const rawData = doc.data() || {};
164
207
  const visibleData = filterVisibleFields(
165
208
  rawData,
166
209
  documentSchema,
167
- userRole
210
+ userRole,
211
+ visibilityOptions
212
+ ? { ...visibilityOptions, documentData: rawData }
213
+ : undefined
168
214
  );
169
215
 
170
216
  // If listFields specified, filter to only those fields (plus id always)
@@ -206,6 +252,8 @@ function listEntitiesLogicFactory(
206
252
  * @param requiredRole - Minimum role required for this operation
207
253
  * @param customSchema - Optional custom request schema
208
254
  * @param listFields - Optional array of field names to include (plus id). If not provided, all visible fields are returned.
255
+ * @param ownership - Optional ownership config for list constraints and visibility: 'owner' masking
256
+ * @param isListCard - When true and ownership is set, applies public condition; when false, applies "mine" filter
209
257
  * @returns Firebase callable function
210
258
  */
211
259
  export const listEntities = (
@@ -213,7 +261,9 @@ export const listEntities = (
213
261
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
214
262
  requiredRole: UserRole,
215
263
  customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
216
- listFields?: string[]
264
+ listFields?: string[],
265
+ ownership?: EntityOwnershipConfig,
266
+ isListCard?: boolean
217
267
  ): CallableFunction<ListEntityRequest, Promise<any>> => {
218
268
  const requestSchema =
219
269
  customSchema ||
@@ -236,7 +286,13 @@ export const listEntities = (
236
286
  CRUD_READ_CONFIG,
237
287
  requestSchema,
238
288
  'list_entities',
239
- listEntitiesLogicFactory(collection, documentSchema, listFields),
289
+ listEntitiesLogicFactory(
290
+ collection,
291
+ documentSchema,
292
+ listFields,
293
+ ownership,
294
+ isListCard
295
+ ),
240
296
  requiredRole
241
297
  );
242
298
  };
@@ -67,14 +67,23 @@ export function createCrudFunctions(
67
67
  schemas.create,
68
68
  access.create
69
69
  );
70
- functions[`${prefix}get_${col}`] = getEntity(col, schemas.get, access.read);
70
+ functions[`${prefix}get_${col}`] = getEntity(
71
+ col,
72
+ schemas.get,
73
+ access.read,
74
+ undefined,
75
+ entity.ownership
76
+ );
71
77
  // Use schemas.get for visibility filtering, entity.listFields for field selection
78
+ // When ownership is set: list = "mine" filter, listCard = public condition
72
79
  functions[`${prefix}list_${col}`] = listEntities(
73
80
  col,
74
81
  schemas.get,
75
82
  access.read,
76
83
  undefined,
77
- entity.listFields
84
+ entity.listFields,
85
+ entity.ownership,
86
+ false
78
87
  );
79
88
  // Always create listCard - uses same schemas.get, field selection via listCardFields ?? listFields ?? undefined
80
89
  functions[`${prefix}listCard_${col}`] = listEntities(
@@ -82,7 +91,9 @@ export function createCrudFunctions(
82
91
  schemas.get,
83
92
  access.read,
84
93
  undefined,
85
- entity.listCardFields ?? entity.listFields ?? undefined
94
+ entity.listCardFields ?? entity.listFields ?? undefined,
95
+ entity.ownership,
96
+ true
86
97
  );
87
98
  functions[`${prefix}update_${col}`] = updateEntity(
88
99
  col,
@@ -12,7 +12,7 @@
12
12
  import * as v from 'valibot';
13
13
 
14
14
  /** Visibility levels matching @donotdev/core */
15
- type Visibility = 'guest' | 'user' | 'admin' | 'technical' | 'hidden';
15
+ type Visibility = 'guest' | 'user' | 'admin' | 'technical' | 'hidden' | 'owner';
16
16
 
17
17
  // Define a type for the custom visibility property we might add to Valibot schemas
18
18
  interface ValibotSchemaWithVisibility extends v.BaseSchema<
@@ -52,6 +52,7 @@ function getFieldVisibility(field: any): Visibility | undefined {
52
52
  * - 'admin': Visible only to admins
53
53
  * - 'technical': Visible to admins only (shown as read-only in edit forms)
54
54
  * - 'hidden': Never visible (passwords, tokens, API keys - only in DB)
55
+ * - 'owner': Visible only when uid matches one of entity.ownership.ownerFields (requires document context; here treated as not visible for aggregate)
55
56
  * - undefined: Defaults to 'user' behavior (visible to authenticated users)
56
57
  *
57
58
  * @param key - The name (key) of the field.
@@ -90,6 +91,11 @@ export function isFieldVisible(
90
91
  return isAdmin;
91
92
  }
92
93
 
94
+ // Owner: requires per-document check (documentData, uid, ownership); not available in aggregate context
95
+ if (visibility === 'owner') {
96
+ return false;
97
+ }
98
+
93
99
  // User fields (or undefined/default) are visible to authenticated users
94
100
  if (visibility === 'user' || visibility === undefined) {
95
101
  return isAuthenticated;