@valentine-efagene/qshelter-common 2.0.22 → 2.0.25
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/dist/generated/client/internal/class.js +2 -2
- package/dist/generated/client/internal/prismaNamespace.d.ts +8 -0
- package/dist/generated/client/internal/prismaNamespace.js +8 -0
- package/dist/generated/client/internal/prismaNamespaceBrowser.d.ts +8 -0
- package/dist/generated/client/internal/prismaNamespaceBrowser.js +8 -0
- package/dist/generated/client/models/Contract.d.ts +343 -1
- package/dist/generated/client/models/PaymentPlan.d.ts +236 -3
- package/dist/generated/client/models/Property.d.ts +277 -1
- package/dist/generated/client/models/PropertyPaymentMethod.d.ts +218 -2
- package/dist/generated/client/models/Tenant.d.ts +482 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/middleware/error-handler.d.ts +6 -0
- package/dist/src/middleware/error-handler.js +26 -0
- package/dist/src/middleware/index.d.ts +3 -0
- package/dist/src/middleware/index.js +3 -0
- package/dist/src/middleware/request-logger.d.ts +6 -0
- package/dist/src/middleware/request-logger.js +17 -0
- package/dist/src/middleware/tenant.d.ts +76 -0
- package/dist/src/middleware/tenant.js +88 -0
- package/dist/src/prisma/tenant.d.ts +51 -0
- package/dist/src/prisma/tenant.js +211 -0
- package/package.json +16 -2
- package/prisma/migrations/20251230104059_add_property_variants/migration.sql +622 -0
- package/prisma/migrations/20251230113413_add_multitenancy/migration.sql +54 -0
- package/prisma/schema.prisma +23 -3
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Request logging middleware that logs method, path, status code, and duration.
|
|
4
|
+
* Logs in JSON format for easy parsing by log aggregation tools.
|
|
5
|
+
*/
|
|
6
|
+
export declare function requestLogger(req: Request, res: Response, next: NextFunction): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request logging middleware that logs method, path, status code, and duration.
|
|
3
|
+
* Logs in JSON format for easy parsing by log aggregation tools.
|
|
4
|
+
*/
|
|
5
|
+
export function requestLogger(req, res, next) {
|
|
6
|
+
const start = Date.now();
|
|
7
|
+
res.on('finish', () => {
|
|
8
|
+
const duration = Date.now() - start;
|
|
9
|
+
console.log(JSON.stringify({
|
|
10
|
+
method: req.method,
|
|
11
|
+
path: req.path,
|
|
12
|
+
statusCode: res.statusCode,
|
|
13
|
+
duration,
|
|
14
|
+
}));
|
|
15
|
+
});
|
|
16
|
+
next();
|
|
17
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { PrismaClient } from '../../generated/client/client';
|
|
3
|
+
import { TenantContext, TenantPrismaClient } from '../prisma/tenant';
|
|
4
|
+
/**
|
|
5
|
+
* Extend Express Request to include tenant context and scoped Prisma client
|
|
6
|
+
*/
|
|
7
|
+
declare global {
|
|
8
|
+
namespace Express {
|
|
9
|
+
interface Request {
|
|
10
|
+
tenantContext?: TenantContext;
|
|
11
|
+
tenantPrisma?: TenantPrismaClient | PrismaClient;
|
|
12
|
+
/**
|
|
13
|
+
* API Gateway context added by serverless-express
|
|
14
|
+
*/
|
|
15
|
+
apiGateway?: {
|
|
16
|
+
event: {
|
|
17
|
+
requestContext: {
|
|
18
|
+
authorizer?: {
|
|
19
|
+
userId?: string;
|
|
20
|
+
email?: string;
|
|
21
|
+
roles?: string;
|
|
22
|
+
tenantId?: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Options for tenant middleware
|
|
32
|
+
*/
|
|
33
|
+
export interface TenantMiddlewareOptions {
|
|
34
|
+
/**
|
|
35
|
+
* The base Prisma client to use for creating tenant-scoped clients
|
|
36
|
+
*/
|
|
37
|
+
prisma: PrismaClient;
|
|
38
|
+
/**
|
|
39
|
+
* Whether to create a tenant-scoped Prisma client automatically
|
|
40
|
+
* If false, only tenantContext is attached to the request
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
createScopedClient?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates a tenant middleware that extracts tenant context from the request
|
|
47
|
+
* and optionally creates a tenant-scoped Prisma client.
|
|
48
|
+
*
|
|
49
|
+
* The API Gateway authorizer is expected to set:
|
|
50
|
+
* - x-tenant-id: The tenant ID from the JWT
|
|
51
|
+
* - x-user-id: The user ID from the JWT
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { createTenantMiddleware } from '@valentine-efagene/qshelter-common';
|
|
56
|
+
* import { prisma } from './lib/prisma';
|
|
57
|
+
*
|
|
58
|
+
* app.use(createTenantMiddleware({ prisma }));
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function createTenantMiddleware(options: TenantMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Middleware that requires tenant context.
|
|
64
|
+
* Use this for routes that must have tenant scoping.
|
|
65
|
+
*/
|
|
66
|
+
export declare function requireTenant(req: Request, res: Response, next: NextFunction): void;
|
|
67
|
+
/**
|
|
68
|
+
* Helper to get tenant-scoped Prisma client from request.
|
|
69
|
+
* Throws if not available.
|
|
70
|
+
*/
|
|
71
|
+
export declare function getTenantPrisma(req: Request): TenantPrismaClient | PrismaClient;
|
|
72
|
+
/**
|
|
73
|
+
* Helper to get tenant context from request.
|
|
74
|
+
* Throws if not available.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getTenantContext(req: Request): TenantContext;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { AppError } from '../utils/errors';
|
|
2
|
+
import { createTenantPrisma, } from '../prisma/tenant';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a tenant middleware that extracts tenant context from the request
|
|
5
|
+
* and optionally creates a tenant-scoped Prisma client.
|
|
6
|
+
*
|
|
7
|
+
* The API Gateway authorizer is expected to set:
|
|
8
|
+
* - x-tenant-id: The tenant ID from the JWT
|
|
9
|
+
* - x-user-id: The user ID from the JWT
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createTenantMiddleware } from '@valentine-efagene/qshelter-common';
|
|
14
|
+
* import { prisma } from './lib/prisma';
|
|
15
|
+
*
|
|
16
|
+
* app.use(createTenantMiddleware({ prisma }));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function createTenantMiddleware(options) {
|
|
20
|
+
const { prisma, createScopedClient = true } = options;
|
|
21
|
+
return function tenantMiddleware(req, res, next) {
|
|
22
|
+
try {
|
|
23
|
+
// 1. Try Lambda authorizer context first (production)
|
|
24
|
+
const authorizerContext = req.apiGateway?.event?.requestContext?.authorizer;
|
|
25
|
+
// 2. Fall back to headers (local dev or alternative setups)
|
|
26
|
+
const headers = req.headers;
|
|
27
|
+
const tenantId = authorizerContext?.tenantId || headers['x-tenant-id'];
|
|
28
|
+
const userId = authorizerContext?.userId || headers['x-user-id'];
|
|
29
|
+
if (!tenantId) {
|
|
30
|
+
// For now, allow requests without tenant context for development
|
|
31
|
+
// In production, you might want to reject these
|
|
32
|
+
console.warn('Request without tenant context:', req.path);
|
|
33
|
+
return next();
|
|
34
|
+
}
|
|
35
|
+
const tenantContext = {
|
|
36
|
+
tenantId,
|
|
37
|
+
userId,
|
|
38
|
+
};
|
|
39
|
+
// Attach tenant context to request
|
|
40
|
+
req.tenantContext = tenantContext;
|
|
41
|
+
// Create tenant-scoped Prisma client if enabled
|
|
42
|
+
if (createScopedClient) {
|
|
43
|
+
req.tenantPrisma = createTenantPrisma(prisma, tenantContext);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
req.tenantPrisma = prisma;
|
|
47
|
+
}
|
|
48
|
+
next();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error('Tenant middleware error:', error);
|
|
52
|
+
next(error);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Middleware that requires tenant context.
|
|
58
|
+
* Use this for routes that must have tenant scoping.
|
|
59
|
+
*/
|
|
60
|
+
export function requireTenant(req, res, next) {
|
|
61
|
+
if (!req.tenantContext?.tenantId) {
|
|
62
|
+
return next(new AppError(400, 'Tenant context required'));
|
|
63
|
+
}
|
|
64
|
+
if (!req.tenantPrisma) {
|
|
65
|
+
return next(new AppError(500, 'Tenant Prisma client not initialized'));
|
|
66
|
+
}
|
|
67
|
+
next();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Helper to get tenant-scoped Prisma client from request.
|
|
71
|
+
* Throws if not available.
|
|
72
|
+
*/
|
|
73
|
+
export function getTenantPrisma(req) {
|
|
74
|
+
if (!req.tenantPrisma) {
|
|
75
|
+
throw new AppError(500, 'Tenant context not available');
|
|
76
|
+
}
|
|
77
|
+
return req.tenantPrisma;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Helper to get tenant context from request.
|
|
81
|
+
* Throws if not available.
|
|
82
|
+
*/
|
|
83
|
+
export function getTenantContext(req) {
|
|
84
|
+
if (!req.tenantContext) {
|
|
85
|
+
throw new AppError(500, 'Tenant context not available');
|
|
86
|
+
}
|
|
87
|
+
return req.tenantContext;
|
|
88
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PrismaClient, Prisma } from "../../generated/client/client";
|
|
2
|
+
/**
|
|
3
|
+
* Tenant context for request-scoped operations
|
|
4
|
+
*/
|
|
5
|
+
export interface TenantContext {
|
|
6
|
+
tenantId: string;
|
|
7
|
+
userId?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates a tenant-scoped Prisma client that automatically:
|
|
11
|
+
* 1. Filters all queries on tenant-scoped models by tenantId
|
|
12
|
+
* 2. Injects tenantId into all create operations on tenant-scoped models
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```ts
|
|
16
|
+
* const tenantPrisma = createTenantPrisma(prisma, { tenantId: 'tenant-123' });
|
|
17
|
+
* const properties = await tenantPrisma.property.findMany(); // Auto-filtered by tenant
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function createTenantPrisma(prisma: PrismaClient, context: TenantContext): import("@prisma/client/runtime/client").DynamicClientExtensionThis<Prisma.TypeMap<import("@prisma/client/runtime/client").InternalArgs & {
|
|
21
|
+
result: {};
|
|
22
|
+
model: {};
|
|
23
|
+
query: {};
|
|
24
|
+
client: {};
|
|
25
|
+
}, Prisma.GlobalOmitConfig | undefined>, Prisma.TypeMapCb<Prisma.GlobalOmitConfig | undefined>, {
|
|
26
|
+
result: {};
|
|
27
|
+
model: {};
|
|
28
|
+
query: {};
|
|
29
|
+
client: {};
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Type helper for the tenant-scoped Prisma client
|
|
33
|
+
*/
|
|
34
|
+
export type TenantPrismaClient = ReturnType<typeof createTenantPrisma>;
|
|
35
|
+
/**
|
|
36
|
+
* Helper to verify a record belongs to the current tenant
|
|
37
|
+
* Use this for operations that can't be filtered at query time
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertTenantOwnership<T extends {
|
|
40
|
+
tenantId?: string | null;
|
|
41
|
+
}>(record: T | null, tenantId: string, options?: {
|
|
42
|
+
allowGlobal?: boolean;
|
|
43
|
+
}): asserts record is T;
|
|
44
|
+
/**
|
|
45
|
+
* Utility to check if a record is accessible by a tenant
|
|
46
|
+
*/
|
|
47
|
+
export declare function canAccessRecord(record: {
|
|
48
|
+
tenantId?: string | null;
|
|
49
|
+
} | null, tenantId: string, options?: {
|
|
50
|
+
allowGlobal?: boolean;
|
|
51
|
+
}): boolean;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Models that require tenant scoping
|
|
3
|
+
*/
|
|
4
|
+
const TENANT_SCOPED_MODELS = [
|
|
5
|
+
"property",
|
|
6
|
+
"paymentPlan",
|
|
7
|
+
"propertyPaymentMethod",
|
|
8
|
+
"contract",
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* Models that can optionally have tenant scoping (nullable tenantId)
|
|
12
|
+
* PaymentPlan has nullable tenantId for global templates
|
|
13
|
+
*/
|
|
14
|
+
const OPTIONAL_TENANT_MODELS = ["paymentPlan"];
|
|
15
|
+
function isTenantScopedModel(model) {
|
|
16
|
+
return TENANT_SCOPED_MODELS.includes(model);
|
|
17
|
+
}
|
|
18
|
+
function isOptionalTenantModel(model) {
|
|
19
|
+
return OPTIONAL_TENANT_MODELS.includes(model);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates a tenant-scoped Prisma client that automatically:
|
|
23
|
+
* 1. Filters all queries on tenant-scoped models by tenantId
|
|
24
|
+
* 2. Injects tenantId into all create operations on tenant-scoped models
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* ```ts
|
|
28
|
+
* const tenantPrisma = createTenantPrisma(prisma, { tenantId: 'tenant-123' });
|
|
29
|
+
* const properties = await tenantPrisma.property.findMany(); // Auto-filtered by tenant
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createTenantPrisma(prisma, context) {
|
|
33
|
+
const { tenantId } = context;
|
|
34
|
+
return prisma.$extends({
|
|
35
|
+
name: "tenant-scoping",
|
|
36
|
+
query: {
|
|
37
|
+
$allModels: {
|
|
38
|
+
async findMany({ model, args, query }) {
|
|
39
|
+
if (isTenantScopedModel(model)) {
|
|
40
|
+
const tenantFilter = isOptionalTenantModel(model)
|
|
41
|
+
? { OR: [{ tenantId }, { tenantId: null }] }
|
|
42
|
+
: { tenantId };
|
|
43
|
+
args.where = {
|
|
44
|
+
...args.where,
|
|
45
|
+
...tenantFilter,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return query(args);
|
|
49
|
+
},
|
|
50
|
+
async findFirst({ model, args, query }) {
|
|
51
|
+
if (isTenantScopedModel(model)) {
|
|
52
|
+
const tenantFilter = isOptionalTenantModel(model)
|
|
53
|
+
? { OR: [{ tenantId }, { tenantId: null }] }
|
|
54
|
+
: { tenantId };
|
|
55
|
+
args.where = {
|
|
56
|
+
...args.where,
|
|
57
|
+
...tenantFilter,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return query(args);
|
|
61
|
+
},
|
|
62
|
+
async findUnique({ model, args, query }) {
|
|
63
|
+
// findUnique can only filter by unique fields, so we verify after fetch
|
|
64
|
+
const result = await query(args);
|
|
65
|
+
if (result && isTenantScopedModel(model)) {
|
|
66
|
+
const record = result;
|
|
67
|
+
if (isOptionalTenantModel(model)) {
|
|
68
|
+
// Allow null tenantId (global) or matching tenantId
|
|
69
|
+
if (record.tenantId !== null && record.tenantId !== tenantId) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
if (record.tenantId !== tenantId) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
},
|
|
81
|
+
async create({ model, args, query }) {
|
|
82
|
+
if (isTenantScopedModel(model) && !isOptionalTenantModel(model)) {
|
|
83
|
+
// Inject tenantId for required tenant models
|
|
84
|
+
args.data.tenantId = tenantId;
|
|
85
|
+
}
|
|
86
|
+
else if (isOptionalTenantModel(model)) {
|
|
87
|
+
// For optional models, inject if not explicitly set to null
|
|
88
|
+
if (args.data.tenantId === undefined) {
|
|
89
|
+
args.data.tenantId = tenantId;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return query(args);
|
|
93
|
+
},
|
|
94
|
+
async createMany({ model, args, query }) {
|
|
95
|
+
if (isTenantScopedModel(model)) {
|
|
96
|
+
const data = Array.isArray(args.data) ? args.data : [args.data];
|
|
97
|
+
args.data = data.map((item) => {
|
|
98
|
+
if (!isOptionalTenantModel(model)) {
|
|
99
|
+
return { ...item, tenantId };
|
|
100
|
+
}
|
|
101
|
+
// For optional models, inject if not explicitly set
|
|
102
|
+
if (item.tenantId === undefined) {
|
|
103
|
+
return { ...item, tenantId };
|
|
104
|
+
}
|
|
105
|
+
return item;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return query(args);
|
|
109
|
+
},
|
|
110
|
+
async update({ model, args, query }) {
|
|
111
|
+
if (isTenantScopedModel(model)) {
|
|
112
|
+
// Verify tenant ownership before update
|
|
113
|
+
const tenantFilter = isOptionalTenantModel(model)
|
|
114
|
+
? { OR: [{ tenantId }, { tenantId: null }] }
|
|
115
|
+
: { tenantId };
|
|
116
|
+
args.where = {
|
|
117
|
+
...args.where,
|
|
118
|
+
...tenantFilter,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return query(args);
|
|
122
|
+
},
|
|
123
|
+
async updateMany({ model, args, query }) {
|
|
124
|
+
if (isTenantScopedModel(model)) {
|
|
125
|
+
const tenantFilter = isOptionalTenantModel(model)
|
|
126
|
+
? { OR: [{ tenantId }, { tenantId: null }] }
|
|
127
|
+
: { tenantId };
|
|
128
|
+
args.where = {
|
|
129
|
+
...args.where,
|
|
130
|
+
...tenantFilter,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return query(args);
|
|
134
|
+
},
|
|
135
|
+
async delete({ model, args, query }) {
|
|
136
|
+
if (isTenantScopedModel(model)) {
|
|
137
|
+
const tenantFilter = isOptionalTenantModel(model)
|
|
138
|
+
? { OR: [{ tenantId }, { tenantId: null }] }
|
|
139
|
+
: { tenantId };
|
|
140
|
+
args.where = {
|
|
141
|
+
...args.where,
|
|
142
|
+
...tenantFilter,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return query(args);
|
|
146
|
+
},
|
|
147
|
+
async deleteMany({ model, args, query }) {
|
|
148
|
+
if (isTenantScopedModel(model)) {
|
|
149
|
+
const tenantFilter = isOptionalTenantModel(model)
|
|
150
|
+
? { OR: [{ tenantId }, { tenantId: null }] }
|
|
151
|
+
: { tenantId };
|
|
152
|
+
args.where = {
|
|
153
|
+
...args.where,
|
|
154
|
+
...tenantFilter,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return query(args);
|
|
158
|
+
},
|
|
159
|
+
async count({ model, args, query }) {
|
|
160
|
+
if (isTenantScopedModel(model)) {
|
|
161
|
+
const tenantFilter = isOptionalTenantModel(model)
|
|
162
|
+
? { OR: [{ tenantId }, { tenantId: null }] }
|
|
163
|
+
: { tenantId };
|
|
164
|
+
args.where = {
|
|
165
|
+
...args.where,
|
|
166
|
+
...tenantFilter,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return query(args);
|
|
170
|
+
},
|
|
171
|
+
async aggregate({ model, args, query }) {
|
|
172
|
+
if (isTenantScopedModel(model)) {
|
|
173
|
+
const tenantFilter = isOptionalTenantModel(model)
|
|
174
|
+
? { OR: [{ tenantId }, { tenantId: null }] }
|
|
175
|
+
: { tenantId };
|
|
176
|
+
args.where = {
|
|
177
|
+
...args.where,
|
|
178
|
+
...tenantFilter,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return query(args);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Helper to verify a record belongs to the current tenant
|
|
189
|
+
* Use this for operations that can't be filtered at query time
|
|
190
|
+
*/
|
|
191
|
+
export function assertTenantOwnership(record, tenantId, options) {
|
|
192
|
+
if (!record) {
|
|
193
|
+
throw new Error("Record not found");
|
|
194
|
+
}
|
|
195
|
+
if (options?.allowGlobal && record.tenantId === null) {
|
|
196
|
+
return; // Global records are accessible to all tenants
|
|
197
|
+
}
|
|
198
|
+
if (record.tenantId !== tenantId) {
|
|
199
|
+
throw new Error("Access denied: resource belongs to another tenant");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Utility to check if a record is accessible by a tenant
|
|
204
|
+
*/
|
|
205
|
+
export function canAccessRecord(record, tenantId, options) {
|
|
206
|
+
if (!record)
|
|
207
|
+
return false;
|
|
208
|
+
if (options?.allowGlobal && record.tenantId === null)
|
|
209
|
+
return true;
|
|
210
|
+
return record.tenantId === tenantId;
|
|
211
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valentine-efagene/qshelter-common",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.25",
|
|
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",
|
|
@@ -34,8 +34,22 @@
|
|
|
34
34
|
"dotenv": "^17.2.3",
|
|
35
35
|
"prisma": "^7.0.0"
|
|
36
36
|
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"express": "^4.0.0 || ^5.0.0",
|
|
39
|
+
"zod": "^3.0.0 || ^4.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"express": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"zod": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
37
49
|
"devDependencies": {
|
|
50
|
+
"@types/express": "^5.0.0",
|
|
38
51
|
"@types/node": "^25.0.3",
|
|
39
|
-
"typescript": "^5.7.3"
|
|
52
|
+
"typescript": "^5.7.3",
|
|
53
|
+
"zod": "^4.0.0"
|
|
40
54
|
}
|
|
41
55
|
}
|