@valentine-efagene/qshelter-common 2.0.60 → 2.0.63

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,3 @@
1
+ export * from './permission-cache';
2
+ export * from './policy-evaluator';
3
+ export * from './types';
@@ -0,0 +1,3 @@
1
+ export * from './permission-cache';
2
+ export * from './policy-evaluator';
3
+ export * from './types';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * In-memory permission cache for fast role → scopes resolution
3
+ *
4
+ * This module provides caching for role permissions to avoid DynamoDB calls
5
+ * on every authorization request. The cache is warmed on Lambda cold start
6
+ * and refreshed based on TTL.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { resolveScopes, warmCache } from '@valentine-efagene/qshelter-common';
11
+ *
12
+ * // Warm cache on cold start (optional but recommended)
13
+ * await warmCache();
14
+ *
15
+ * // Resolve scopes for user's roles
16
+ * const scopes = await resolveScopes(['admin', 'customer']);
17
+ * // Returns: ['contract:*', 'payment:*', 'user:read', ...]
18
+ * ```
19
+ */
20
+ /**
21
+ * Resolve all scopes for a list of roles (deduped)
22
+ *
23
+ * @param roles - Array of role IDs/names
24
+ * @returns Deduplicated array of all scopes from all roles
25
+ */
26
+ export declare function resolveScopes(roles: string[]): Promise<string[]>;
27
+ /**
28
+ * Warm the permission cache on cold start
29
+ * Loads all active roles from DynamoDB into memory
30
+ *
31
+ * @returns Promise that resolves when cache is warmed
32
+ */
33
+ export declare function warmCache(): Promise<void>;
34
+ /**
35
+ * Check if cache has been warmed
36
+ */
37
+ export declare function isCacheWarmed(): boolean;
38
+ /**
39
+ * Invalidate a specific role in the cache
40
+ * Call this when role permissions are updated
41
+ *
42
+ * @param roleId - Role ID to invalidate
43
+ */
44
+ export declare function invalidateRole(roleId: string): void;
45
+ /**
46
+ * Clear the entire permission cache
47
+ * Call this for full refresh or testing
48
+ */
49
+ export declare function clearCache(): void;
50
+ /**
51
+ * Get cache statistics for monitoring
52
+ */
53
+ export declare function getCacheStats(): {
54
+ size: number;
55
+ warmed: boolean;
56
+ };
@@ -0,0 +1,268 @@
1
+ /**
2
+ * In-memory permission cache for fast role → scopes resolution
3
+ *
4
+ * This module provides caching for role permissions to avoid DynamoDB calls
5
+ * on every authorization request. The cache is warmed on Lambda cold start
6
+ * and refreshed based on TTL.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { resolveScopes, warmCache } from '@valentine-efagene/qshelter-common';
11
+ *
12
+ * // Warm cache on cold start (optional but recommended)
13
+ * await warmCache();
14
+ *
15
+ * // Resolve scopes for user's roles
16
+ * const scopes = await resolveScopes(['admin', 'customer']);
17
+ * // Returns: ['contract:*', 'payment:*', 'user:read', ...]
18
+ * ```
19
+ */
20
+ import { DynamoDBClient, GetItemCommand, ScanCommand } from '@aws-sdk/client-dynamodb';
21
+ import { unmarshall } from '@aws-sdk/util-dynamodb';
22
+ /** Cache TTL in milliseconds (5 minutes) */
23
+ const CACHE_TTL_MS = 5 * 60 * 1000;
24
+ /** In-memory LRU cache for role → scopes */
25
+ const cache = new Map();
26
+ /** DynamoDB client (lazy initialized) */
27
+ let dynamoClient = null;
28
+ /** Flag to track if cache has been warmed */
29
+ let cacheWarmed = false;
30
+ /**
31
+ * Get or create DynamoDB client
32
+ */
33
+ function getClient() {
34
+ if (!dynamoClient) {
35
+ dynamoClient = new DynamoDBClient({
36
+ region: process.env.AWS_REGION || 'us-east-1',
37
+ endpoint: process.env.LOCALSTACK_ENDPOINT || undefined,
38
+ });
39
+ }
40
+ return dynamoClient;
41
+ }
42
+ /**
43
+ * Get the roles table name from environment
44
+ */
45
+ function getTableName() {
46
+ return process.env.ROLES_TABLE || process.env.ROLE_POLICIES_TABLE_NAME || 'qshelter-roles';
47
+ }
48
+ /**
49
+ * Get scopes for a single role (cache-first)
50
+ *
51
+ * @param roleId - Role ID or name
52
+ * @returns Array of scopes for the role
53
+ */
54
+ async function getRoleScopes(roleId) {
55
+ const now = Date.now();
56
+ const cached = cache.get(roleId);
57
+ // Return cached value if not expired
58
+ if (cached && cached.expiresAt > now) {
59
+ return cached.scopes;
60
+ }
61
+ try {
62
+ // Try to fetch from DynamoDB using the existing schema (PK/SK pattern)
63
+ const result = await getClient().send(new GetItemCommand({
64
+ TableName: getTableName(),
65
+ Key: {
66
+ PK: { S: `ROLE#${roleId}` },
67
+ SK: { S: 'POLICY' },
68
+ },
69
+ }));
70
+ if (!result.Item) {
71
+ // Role not found, cache empty scopes
72
+ cache.set(roleId, { scopes: [], expiresAt: now + CACHE_TTL_MS });
73
+ return [];
74
+ }
75
+ const item = unmarshall(result.Item);
76
+ // Extract scopes from the policy structure
77
+ // The existing schema stores policies with statements containing resources
78
+ // We need to convert this to scopes
79
+ const scopes = extractScopesFromPolicy(item);
80
+ // Update cache
81
+ cache.set(roleId, {
82
+ scopes,
83
+ expiresAt: now + CACHE_TTL_MS,
84
+ });
85
+ return scopes;
86
+ }
87
+ catch (error) {
88
+ console.error(`[PermissionCache] Error fetching role ${roleId}:`, error);
89
+ return [];
90
+ }
91
+ }
92
+ /**
93
+ * Extract scopes from the existing policy structure
94
+ *
95
+ * Converts path-based policies to scope format:
96
+ * - /contracts/* GET → contract:read
97
+ * - /contracts/* POST → contract:create
98
+ * - /contracts/* PUT,PATCH → contract:update
99
+ * - /contracts/* DELETE → contract:delete
100
+ * - /* * → * (superuser)
101
+ */
102
+ function extractScopesFromPolicy(item) {
103
+ // If the item already has scopes array, use it directly
104
+ if (item.scopes && Array.isArray(item.scopes)) {
105
+ return item.scopes;
106
+ }
107
+ // Otherwise, check if isActive is false
108
+ if (item.isActive === false) {
109
+ return [];
110
+ }
111
+ // Convert path-based policy to scopes
112
+ const scopes = new Set();
113
+ const policy = item.policy;
114
+ if (!policy?.statements) {
115
+ return [];
116
+ }
117
+ for (const statement of policy.statements) {
118
+ if (statement.effect !== 'Allow')
119
+ continue;
120
+ for (const resource of statement.resources || []) {
121
+ const path = resource.path || '';
122
+ const methods = resource.methods || [];
123
+ // Extract resource name from path
124
+ const resourceName = extractResourceName(path);
125
+ for (const method of methods) {
126
+ const action = methodToAction(method);
127
+ if (resourceName === '*' || action === '*') {
128
+ scopes.add('*');
129
+ }
130
+ else if (resourceName && action) {
131
+ scopes.add(`${resourceName}:${action}`);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ return Array.from(scopes);
137
+ }
138
+ /**
139
+ * Extract resource name from path pattern
140
+ * /contracts/* → contract
141
+ * /users/:id → user
142
+ * /* → *
143
+ */
144
+ function extractResourceName(path) {
145
+ if (path === '/*' || path === '*') {
146
+ return '*';
147
+ }
148
+ // Remove leading slash and get first segment
149
+ const segments = path.replace(/^\//, '').split('/');
150
+ const firstSegment = segments[0] || '';
151
+ // Singularize common resources
152
+ const singularMap = {
153
+ 'contracts': 'contract',
154
+ 'users': 'user',
155
+ 'properties': 'property',
156
+ 'payments': 'payment',
157
+ 'tenants': 'tenant',
158
+ 'documents': 'document',
159
+ 'phases': 'phase',
160
+ 'installments': 'installment',
161
+ };
162
+ return singularMap[firstSegment] || firstSegment.replace(/s$/, '');
163
+ }
164
+ /**
165
+ * Convert HTTP method to action
166
+ * GET → read
167
+ * POST → create
168
+ * PUT/PATCH → update
169
+ * DELETE → delete
170
+ * * → *
171
+ */
172
+ function methodToAction(method) {
173
+ const upper = method.toUpperCase();
174
+ const methodMap = {
175
+ 'GET': 'read',
176
+ 'POST': 'create',
177
+ 'PUT': 'update',
178
+ 'PATCH': 'update',
179
+ 'DELETE': 'delete',
180
+ '*': '*',
181
+ };
182
+ return methodMap[upper] || 'read';
183
+ }
184
+ /**
185
+ * Resolve all scopes for a list of roles (deduped)
186
+ *
187
+ * @param roles - Array of role IDs/names
188
+ * @returns Deduplicated array of all scopes from all roles
189
+ */
190
+ export async function resolveScopes(roles) {
191
+ if (!roles || roles.length === 0) {
192
+ return [];
193
+ }
194
+ const scopeSets = await Promise.all(roles.map(getRoleScopes));
195
+ const allScopes = scopeSets.flat();
196
+ // Dedupe and return
197
+ return [...new Set(allScopes)];
198
+ }
199
+ /**
200
+ * Warm the permission cache on cold start
201
+ * Loads all active roles from DynamoDB into memory
202
+ *
203
+ * @returns Promise that resolves when cache is warmed
204
+ */
205
+ export async function warmCache() {
206
+ if (cacheWarmed) {
207
+ return;
208
+ }
209
+ try {
210
+ const result = await getClient().send(new ScanCommand({
211
+ TableName: getTableName(),
212
+ FilterExpression: 'SK = :sk',
213
+ ExpressionAttributeValues: {
214
+ ':sk': { S: 'POLICY' },
215
+ },
216
+ }));
217
+ const now = Date.now();
218
+ for (const item of result.Items || []) {
219
+ const unmarshalled = unmarshall(item);
220
+ const roleName = unmarshalled.roleName || unmarshalled.PK?.replace('ROLE#', '');
221
+ if (roleName && unmarshalled.isActive !== false) {
222
+ const scopes = extractScopesFromPolicy(unmarshalled);
223
+ cache.set(roleName, {
224
+ scopes,
225
+ expiresAt: now + CACHE_TTL_MS,
226
+ });
227
+ }
228
+ }
229
+ cacheWarmed = true;
230
+ console.log(`[PermissionCache] Cache warmed with ${cache.size} roles`);
231
+ }
232
+ catch (error) {
233
+ console.error('[PermissionCache] Failed to warm cache:', error);
234
+ // Don't throw - allow fallback to per-request fetching
235
+ }
236
+ }
237
+ /**
238
+ * Check if cache has been warmed
239
+ */
240
+ export function isCacheWarmed() {
241
+ return cacheWarmed;
242
+ }
243
+ /**
244
+ * Invalidate a specific role in the cache
245
+ * Call this when role permissions are updated
246
+ *
247
+ * @param roleId - Role ID to invalidate
248
+ */
249
+ export function invalidateRole(roleId) {
250
+ cache.delete(roleId);
251
+ }
252
+ /**
253
+ * Clear the entire permission cache
254
+ * Call this for full refresh or testing
255
+ */
256
+ export function clearCache() {
257
+ cache.clear();
258
+ cacheWarmed = false;
259
+ }
260
+ /**
261
+ * Get cache statistics for monitoring
262
+ */
263
+ export function getCacheStats() {
264
+ return {
265
+ size: cache.size,
266
+ warmed: cacheWarmed,
267
+ };
268
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Policy evaluation utilities for scope-based authorization
3
+ *
4
+ * Scopes follow a resource:action pattern with wildcard support:
5
+ * - "contract:read" - specific action on specific resource
6
+ * - "contract:*" - all actions on contracts
7
+ * - "*" - superuser access
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { hasScope, requireScope } from '@valentine-efagene/qshelter-common';
12
+ *
13
+ * const scopes = ['contract:read', 'payment:*'];
14
+ *
15
+ * hasScope(scopes, 'contract:read'); // true
16
+ * hasScope(scopes, 'payment:create'); // true (wildcard match)
17
+ * hasScope(scopes, 'user:read'); // false
18
+ *
19
+ * requireScope(scopes, 'contract:read'); // passes
20
+ * requireScope(scopes, 'user:read'); // throws 403 error
21
+ * ```
22
+ */
23
+ /**
24
+ * Check if scopes array contains the required scope
25
+ * Supports wildcards: "resource:*" matches "resource:action"
26
+ *
27
+ * @param scopes - Array of scopes the principal has
28
+ * @param required - The scope required for the operation
29
+ * @returns true if the required scope is present (exact or wildcard match)
30
+ */
31
+ export declare function hasScope(scopes: string[], required: string): boolean;
32
+ /**
33
+ * Throws a 403 Forbidden error if the required scope is missing
34
+ *
35
+ * @param scopes - Array of scopes the principal has
36
+ * @param required - The scope required for the operation
37
+ * @throws AppError with status 403 if scope is missing
38
+ */
39
+ export declare function requireScope(scopes: string[], required: string): void;
40
+ /**
41
+ * Check if ALL required scopes are present (AND logic)
42
+ *
43
+ * @param scopes - Array of scopes the principal has
44
+ * @param required - Array of scopes required (all must be present)
45
+ * @returns true if all required scopes are present
46
+ */
47
+ export declare function hasAllScopes(scopes: string[], required: string[]): boolean;
48
+ /**
49
+ * Check if ANY required scope is present (OR logic)
50
+ *
51
+ * @param scopes - Array of scopes the principal has
52
+ * @param required - Array of scopes (any one must be present)
53
+ * @returns true if at least one required scope is present
54
+ */
55
+ export declare function hasAnyScope(scopes: string[], required: string[]): boolean;
56
+ /**
57
+ * Throws a 403 Forbidden error if none of the required scopes are present
58
+ *
59
+ * @param scopes - Array of scopes the principal has
60
+ * @param required - Array of scopes (any one must be present)
61
+ * @throws AppError with status 403 if no required scope is present
62
+ */
63
+ export declare function requireAnyScope(scopes: string[], required: string[]): void;
64
+ /**
65
+ * Throws a 403 Forbidden error if not all required scopes are present
66
+ *
67
+ * @param scopes - Array of scopes the principal has
68
+ * @param required - Array of scopes (all must be present)
69
+ * @throws AppError with status 403 if any required scope is missing
70
+ */
71
+ export declare function requireAllScopes(scopes: string[], required: string[]): void;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Policy evaluation utilities for scope-based authorization
3
+ *
4
+ * Scopes follow a resource:action pattern with wildcard support:
5
+ * - "contract:read" - specific action on specific resource
6
+ * - "contract:*" - all actions on contracts
7
+ * - "*" - superuser access
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { hasScope, requireScope } from '@valentine-efagene/qshelter-common';
12
+ *
13
+ * const scopes = ['contract:read', 'payment:*'];
14
+ *
15
+ * hasScope(scopes, 'contract:read'); // true
16
+ * hasScope(scopes, 'payment:create'); // true (wildcard match)
17
+ * hasScope(scopes, 'user:read'); // false
18
+ *
19
+ * requireScope(scopes, 'contract:read'); // passes
20
+ * requireScope(scopes, 'user:read'); // throws 403 error
21
+ * ```
22
+ */
23
+ import { AppError } from '../utils/errors';
24
+ /**
25
+ * Check if scopes array contains the required scope
26
+ * Supports wildcards: "resource:*" matches "resource:action"
27
+ *
28
+ * @param scopes - Array of scopes the principal has
29
+ * @param required - The scope required for the operation
30
+ * @returns true if the required scope is present (exact or wildcard match)
31
+ */
32
+ export function hasScope(scopes, required) {
33
+ if (!scopes || scopes.length === 0) {
34
+ return false;
35
+ }
36
+ return scopes.some((s) => {
37
+ // Superuser wildcard
38
+ if (s === '*')
39
+ return true;
40
+ // Exact match
41
+ if (s === required)
42
+ return true;
43
+ // Wildcard match: "contract:*" matches "contract:read"
44
+ if (s.endsWith(':*')) {
45
+ const prefix = s.slice(0, -1); // "contract:"
46
+ return required.startsWith(prefix);
47
+ }
48
+ return false;
49
+ });
50
+ }
51
+ /**
52
+ * Throws a 403 Forbidden error if the required scope is missing
53
+ *
54
+ * @param scopes - Array of scopes the principal has
55
+ * @param required - The scope required for the operation
56
+ * @throws AppError with status 403 if scope is missing
57
+ */
58
+ export function requireScope(scopes, required) {
59
+ if (!hasScope(scopes, required)) {
60
+ throw new AppError(403, `Forbidden: missing required scope '${required}'`);
61
+ }
62
+ }
63
+ /**
64
+ * Check if ALL required scopes are present (AND logic)
65
+ *
66
+ * @param scopes - Array of scopes the principal has
67
+ * @param required - Array of scopes required (all must be present)
68
+ * @returns true if all required scopes are present
69
+ */
70
+ export function hasAllScopes(scopes, required) {
71
+ return required.every((r) => hasScope(scopes, r));
72
+ }
73
+ /**
74
+ * Check if ANY required scope is present (OR logic)
75
+ *
76
+ * @param scopes - Array of scopes the principal has
77
+ * @param required - Array of scopes (any one must be present)
78
+ * @returns true if at least one required scope is present
79
+ */
80
+ export function hasAnyScope(scopes, required) {
81
+ return required.some((r) => hasScope(scopes, r));
82
+ }
83
+ /**
84
+ * Throws a 403 Forbidden error if none of the required scopes are present
85
+ *
86
+ * @param scopes - Array of scopes the principal has
87
+ * @param required - Array of scopes (any one must be present)
88
+ * @throws AppError with status 403 if no required scope is present
89
+ */
90
+ export function requireAnyScope(scopes, required) {
91
+ if (!hasAnyScope(scopes, required)) {
92
+ throw new AppError(403, `Forbidden: requires at least one of scopes: ${required.join(', ')}`);
93
+ }
94
+ }
95
+ /**
96
+ * Throws a 403 Forbidden error if not all required scopes are present
97
+ *
98
+ * @param scopes - Array of scopes the principal has
99
+ * @param required - Array of scopes (all must be present)
100
+ * @throws AppError with status 403 if any required scope is missing
101
+ */
102
+ export function requireAllScopes(scopes, required) {
103
+ if (!hasAllScopes(scopes, required)) {
104
+ const missing = required.filter((r) => !hasScope(scopes, r));
105
+ throw new AppError(403, `Forbidden: missing required scopes: ${missing.join(', ')}`);
106
+ }
107
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Auth types for JWT payloads and authorization context
3
+ */
4
+ export interface JwtPayload {
5
+ /** User ID or API Key ID */
6
+ sub: string;
7
+ /** User email (for user principals) */
8
+ email?: string;
9
+ /** Tenant ID for multi-tenancy */
10
+ tenantId: string;
11
+ /** Type of principal: user or apiKey */
12
+ principalType: 'user' | 'apiKey';
13
+ /** Role IDs/names (resolved to scopes via cache) */
14
+ roles: string[];
15
+ /** Issued at timestamp */
16
+ iat: number;
17
+ /** Expiration timestamp */
18
+ exp: number;
19
+ }
20
+ /**
21
+ * Authorization context passed to downstream services via API Gateway
22
+ * All values must be strings for API Gateway context
23
+ *
24
+ * @deprecated Use AuthContext from middleware/auth-context.ts for Express services
25
+ */
26
+ export interface AuthorizerResultContext {
27
+ /** User ID or API Key ID */
28
+ principalId: string;
29
+ /** Tenant ID */
30
+ tenantId: string;
31
+ /** Type of principal: user or apiKey */
32
+ principalType: 'user' | 'apiKey';
33
+ /** JSON stringified array of role names */
34
+ roles: string;
35
+ /** JSON stringified array of resolved scopes */
36
+ scopes: string;
37
+ /** User email (optional) */
38
+ email?: string;
39
+ }
40
+ /**
41
+ * Parsed auth context with scopes (after JSON parsing)
42
+ */
43
+ export interface ParsedAuthContextWithScopes {
44
+ principalId: string;
45
+ tenantId: string;
46
+ principalType: 'user' | 'apiKey';
47
+ roles: string[];
48
+ scopes: string[];
49
+ email?: string;
50
+ }
51
+ /**
52
+ * Role definition stored in DynamoDB
53
+ */
54
+ export interface RoleDefinition {
55
+ /** Role ID (partition key) */
56
+ id: string;
57
+ /** Human-readable role name */
58
+ name: string;
59
+ /** Description of the role */
60
+ description?: string;
61
+ /** Scopes granted by this role */
62
+ scopes: string[];
63
+ /** Whether the role is active */
64
+ isActive: boolean;
65
+ /** Tenant ID (null for global roles) */
66
+ tenantId?: string;
67
+ /** Last updated timestamp */
68
+ updatedAt: string;
69
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Auth types for JWT payloads and authorization context
3
+ */
4
+ export {};
@@ -8,3 +8,4 @@ export * from './prisma/user';
8
8
  export * from './prisma/tenant';
9
9
  export * from './middleware';
10
10
  export * from './events';
11
+ export * from './auth';
package/dist/src/index.js CHANGED
@@ -8,3 +8,4 @@ export * from './prisma/user';
8
8
  export * from './prisma/tenant';
9
9
  export * from './middleware';
10
10
  export * from './events';
11
+ export * from './auth';
@@ -1,42 +1,47 @@
1
1
  /**
2
- * Models that require tenant scoping.
2
+ * INVERTED APPROACH: All models are tenant-scoped by default.
3
+ * Only list models that are explicitly GLOBAL (no tenant scoping).
3
4
  *
4
- * IMPORTANT: When adding a new model with tenantId to the schema,
5
- * add it to this list so queries are automatically filtered by tenant.
6
- *
7
- * Models with nullable tenantId (for global templates) should also be
8
- * added to OPTIONAL_TENANT_MODELS below.
5
+ * This reduces the risk of accidentally omitting a new model from tenant scoping.
6
+ */
7
+ /**
8
+ * Models that are intentionally GLOBAL and should NOT be tenant-scoped.
9
+ * These models either:
10
+ * - Don't have a tenantId field (system tables)
11
+ * - Have optional tenantId but are designed to work across tenants (User)
9
12
  */
10
- const TENANT_SCOPED_MODELS = [
11
- // Property domain
12
- "property",
13
- "propertyPaymentMethod",
14
- // Payment plan domain
15
- "paymentPlan",
16
- // Contract domain
17
- "contract",
18
- "contractTermination",
19
- // Document domain
20
- "documentTemplate",
21
- "offerLetter",
22
- "documentRequirementRule",
23
- // Prequalification & underwriting domain
24
- "prequalification",
25
- "underwritingDecision",
26
- // Payment method changes
27
- "paymentMethodChangeRequest",
13
+ const GLOBAL_MODELS = [
14
+ // User can exist across tenants or without a tenant
15
+ "user",
16
+ // System/infrastructure tables without tenantId
17
+ "tenant",
18
+ "role",
19
+ "permission",
20
+ "rolePermission",
21
+ "userRole",
22
+ "refreshToken",
23
+ "passwordReset",
24
+ "wallet",
25
+ "domainEvent",
28
26
  ];
29
27
  /**
30
- * Models that can optionally have tenant scoping (nullable tenantId)
31
- * PaymentPlan has nullable tenantId for global templates
28
+ * Models that have OPTIONAL tenant scoping (nullable tenantId).
29
+ * These can be global templates (tenantId = null) or tenant-specific.
30
+ * Queries will return both global AND tenant-specific records.
32
31
  */
33
32
  const OPTIONAL_TENANT_MODELS = ["paymentPlan"];
34
- function isTenantScopedModel(model) {
35
- return TENANT_SCOPED_MODELS.includes(model);
33
+ function isGlobalModel(model) {
34
+ return GLOBAL_MODELS.includes(model);
36
35
  }
37
36
  function isOptionalTenantModel(model) {
38
37
  return OPTIONAL_TENANT_MODELS.includes(model);
39
38
  }
39
+ /**
40
+ * A model is tenant-scoped by default unless explicitly listed as global.
41
+ */
42
+ function isTenantScopedModel(model) {
43
+ return !isGlobalModel(model);
44
+ }
40
45
  /**
41
46
  * Creates a tenant-scoped Prisma client that automatically:
42
47
  * 1. Filters all queries on tenant-scoped models by tenantId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentine-efagene/qshelter-common",
3
- "version": "2.0.60",
3
+ "version": "2.0.63",
4
4
  "description": "Shared database schemas and utilities for QShelter services",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -28,9 +28,11 @@
28
28
  "prisma"
29
29
  ],
30
30
  "dependencies": {
31
+ "@aws-sdk/client-dynamodb": "^3.962.0",
31
32
  "@aws-sdk/client-secrets-manager": "^3.500.0",
32
33
  "@aws-sdk/client-sns": "^3.500.0",
33
34
  "@aws-sdk/client-ssm": "^3.500.0",
35
+ "@aws-sdk/util-dynamodb": "^3.962.0",
34
36
  "@prisma/client": "^7.0.0",
35
37
  "dotenv": "^17.2.3",
36
38
  "prisma": "^7.0.0"