@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.
|
|
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.
|
|
46
|
-
"@donotdev/firebase": "^0.0.
|
|
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 */
|
package/src/firebase/crud/get.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
package/src/shared/schema.ts
CHANGED
|
@@ -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;
|